baktainer/app/spec/unit/baktainer_spec.rb
James Paterni 8db5004eea
Some checks are pending
Test and Build Docker Image / test (push) Waiting to run
Test and Build Docker Image / build (push) Blocked by required conditions
Add comprehensive RSpec testing infrastructure and enhance CI/CD pipeline
- Implement complete test suite with 63 examples (49 unit + 14 integration tests)
- Add RSpec, FactoryBot, WebMock, and SimpleCov testing dependencies
- Create mocked integration tests eliminating need for real Docker containers
- Fix SQLite method signature to accept login/password parameters
- Enhance container discovery to handle nil labels gracefully
- Add test coverage reporting and JUnit XML output for CI
- Update GitHub Actions workflow to run tests before Docker builds
- Add Ruby 3.3 setup with gem caching for faster CI execution
- Create CI test script and comprehensive testing documentation
- Ensure Docker builds only proceed when all tests pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 23:12:59 -04:00

218 lines
No EOL
6.5 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Baktainer::Runner do
let(:default_options) do
{
url: 'unix:///var/run/docker.sock',
ssl: false,
ssl_options: {},
threads: 5
}
end
let(:runner) { described_class.new(**default_options) }
describe '#initialize' do
it 'sets default values' do
expect(runner.instance_variable_get(:@url)).to eq('unix:///var/run/docker.sock')
expect(runner.instance_variable_get(:@ssl)).to be false
expect(runner.instance_variable_get(:@ssl_options)).to eq({})
end
it 'configures Docker URL' do
expect(Docker).to receive(:url=).with('unix:///var/run/docker.sock')
described_class.new(**default_options)
end
it 'creates fixed thread pool with specified size' do
pool = runner.instance_variable_get(:@pool)
expect(pool).to be_a(Concurrent::FixedThreadPool)
end
it 'sets up SSL when enabled' do
ssl_options = {
url: 'https://docker.example.com:2376',
ssl: true,
ssl_options: { ca_file: 'ca.pem', client_cert: 'cert.pem', client_key: 'key.pem' }
}
# Generate a valid test certificate
require 'openssl'
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 1
cert.subject = OpenSSL::X509::Name.parse('/CN=test')
cert.issuer = cert.subject
cert.public_key = key.public_key
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.sign(key, OpenSSL::Digest::SHA256.new)
cert_pem = cert.to_pem
with_env('BT_CA' => cert_pem, 'BT_CERT' => 'cert-content', 'BT_KEY' => 'key-content') do
expect { described_class.new(**ssl_options) }.not_to raise_error
end
end
it 'sets log level from environment' do
with_env('LOG_LEVEL' => 'debug') do
described_class.new(**default_options)
expect(LOGGER.level).to eq(Logger::DEBUG)
end
end
end
describe '#perform_backup' do
let(:mock_container) { instance_double(Baktainer::Container, name: 'test-container', engine: 'postgres') }
before do
allow(Baktainer::Containers).to receive(:find_all).and_return([mock_container])
allow(mock_container).to receive(:backup)
end
it 'finds all containers and backs them up' do
expect(Baktainer::Containers).to receive(:find_all).and_return([mock_container])
expect(mock_container).to receive(:backup)
runner.perform_backup
# Allow time for thread execution
sleep(0.1)
end
it 'handles backup errors gracefully' do
allow(mock_container).to receive(:backup).and_raise(StandardError.new('Test error'))
expect { runner.perform_backup }.not_to raise_error
# Allow time for thread execution
sleep(0.1)
end
it 'uses thread pool for concurrent backups' do
containers = Array.new(3) { |i|
instance_double(Baktainer::Container, name: "container-#{i}", engine: 'postgres', backup: nil)
}
allow(Baktainer::Containers).to receive(:find_all).and_return(containers)
containers.each do |container|
expect(container).to receive(:backup)
end
runner.perform_backup
# Allow time for thread execution
sleep(0.1)
end
end
describe '#run' do
let(:mock_cron) { double('CronCalc') }
before do
allow(CronCalc).to receive(:new).and_return(mock_cron)
allow(mock_cron).to receive(:next).and_return(Time.now + 1)
allow(runner).to receive(:sleep)
allow(runner).to receive(:perform_backup)
end
it 'uses default cron schedule when BT_CRON not set' do
expect(CronCalc).to receive(:new).with('0 0 * * *').and_return(mock_cron)
# Stop the infinite loop after first iteration
allow(runner).to receive(:loop).and_yield
runner.run
end
it 'uses BT_CRON environment variable when set' do
with_env('BT_CRON' => '0 12 * * *') do
expect(CronCalc).to receive(:new).with('0 12 * * *').and_return(mock_cron)
allow(runner).to receive(:loop).and_yield
runner.run
end
end
it 'handles invalid cron format gracefully' do
with_env('BT_CRON' => 'invalid-cron') do
expect(CronCalc).to receive(:new).with('invalid-cron').and_raise(StandardError)
allow(runner).to receive(:loop).and_yield
expect { runner.run }.not_to raise_error
end
end
it 'calculates sleep duration correctly' do
future_time = Time.now + 3600 # 1 hour from now
allow(Time).to receive(:now).and_return(Time.now)
allow(mock_cron).to receive(:next).and_return(future_time)
allow(runner).to receive(:loop).and_yield
expect(runner).to receive(:sleep) do |duration|
expect(duration).to be_within(1).of(3600)
end
runner.run
end
end
describe '#setup_ssl (private)' do
context 'when SSL is disabled' do
it 'does not configure SSL options' do
expect(Docker).not_to receive(:options=)
described_class.new(**default_options)
end
end
context 'when SSL is enabled' do
let(:ssl_options) do
{
url: 'https://docker.example.com:2376',
ssl: true,
ssl_options: {}
}
end
it 'configures Docker SSL options' do
# Generate a valid test certificate
require 'openssl'
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 1
cert.subject = OpenSSL::X509::Name.parse('/CN=test')
cert.issuer = cert.subject
cert.public_key = key.public_key
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.sign(key, OpenSSL::Digest::SHA256.new)
cert_pem = cert.to_pem
with_env('BT_CA' => cert_pem, 'BT_CERT' => 'cert-content', 'BT_KEY' => 'key-content') do
expect(Docker).to receive(:options=).with(hash_including(
client_cert_data: 'cert-content',
client_key_data: 'key-content',
scheme: 'https'
))
described_class.new(**ssl_options)
end
end
it 'handles missing SSL environment variables' do
# Test with missing environment variables
expect { described_class.new(**ssl_options) }.to raise_error
end
end
end
end