/** * Unit tests for container service operations */ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { eq } from 'drizzle-orm'; import { closeDb, createDbClient } from '../db/client'; import { runMigrations } from '../db/migrate'; import { containerServices } from '../db/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { addContainerService, getContainerService, getContainerServiceByName, getServiceCredentials, listContainerServices, removeContainerService, updateServiceCredentials, updateVerificationStatus, } from './container-service'; describe('container-service', () => { let testDbPath: string; let testDir: string; beforeEach(async () => { // Create temp directory for test database testDir = mkdtempSync(join(tmpdir(), 'celilo-test-')); testDbPath = join(testDir, 'test.db'); // Set environment variable for database path process.env.CELILO_DB_PATH = testDbPath; // Initialize database and run migrations await runMigrations(testDbPath); // Create a dummy master key for encryption const masterKeyPath = join(testDir, 'master.key'); process.env.CELILO_MASTER_KEY_PATH = masterKeyPath; const fs = await import('node:fs/promises'); await fs.writeFile(masterKeyPath, 'a'.repeat(64), 'utf8'); }); afterEach(() => { // Close database connection closeDb(); // Clean up test directory if (testDir) { rmSync(testDir, { recursive: true, force: true }); } // Clear environment variables resetTestDbPath(); delete process.env.CELILO_MASTER_KEY_PATH; }); describe('addContainerService', () => { it('creates a Proxmox service', async () => { const service = await addContainerService({ name: 'Test Proxmox', providerName: 'proxmox', zones: ['dmz', 'app', 'secure'], providerConfig: { default_target_node: 'pve', lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst', storage: 'local-lvm', }, apiCredentials: { api_url: 'https://proxmox.local:8006/api2/json', api_token_id: 'root@pam!celilo', api_token_secret: 'test-token-secret', }, }); expect(service.id).toBeDefined(); expect(service.name).toBe('Test Proxmox'); expect(service.providerName).toBe('proxmox'); expect(service.zones).toEqual(['dmz', 'app', 'secure']); expect(service.apiCredentialsEncrypted).toBeDefined(); expect(service.createdAt).toBeInstanceOf(Date); }); it('creates a Digital Ocean service', async () => { const service = await addContainerService({ name: 'Digital Ocean NYC3', providerName: 'digitalocean', zones: ['external'], providerConfig: { region: 'nyc3', default_size: 's-1vcpu-1gb', default_image: 'ubuntu-22-04-x64', }, apiCredentials: { api_token: 'dop_test_token_12345', }, }); expect(service.id).toBeDefined(); expect(service.name).toBe('Digital Ocean NYC3'); expect(service.providerName).toBe('digitalocean'); expect(service.zones).toEqual(['external']); }); it('encrypts API credentials', async () => { const service = await addContainerService({ name: 'Test Service', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://test.local', api_token_id: 'root@pam!celilo', api_token_secret: 'secret-token-123', }, }); // Verify credentials are encrypted in database const db = createDbClient({ path: testDbPath }); const dbRecord = await db .select() .from(containerServices) .where(eq(containerServices.id, service.id)) .get(); expect(dbRecord?.apiCredentialsEncrypted).toBeDefined(); expect(dbRecord?.apiCredentialsEncrypted).not.toContain('secret-token-123'); }); }); describe('getContainerService', () => { it('retrieves service by ID', async () => { const created = await addContainerService({ name: 'Test Service', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://test' }, }); const retrieved = await getContainerService(created.id); expect(retrieved).toBeDefined(); expect(retrieved?.id).toBe(created.id); expect(retrieved?.name).toBe('Test Service'); }); it('returns null for non-existent service', async () => { const result = await getContainerService('non-existent-id'); expect(result).toBeNull(); }); }); describe('getContainerServiceByName', () => { it('retrieves service by name', async () => { await addContainerService({ name: 'Unique Service Name', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://test' }, }); const retrieved = await getContainerServiceByName('Unique Service Name'); expect(retrieved).toBeDefined(); expect(retrieved?.name).toBe('Unique Service Name'); }); it('returns null for non-existent service name', async () => { const result = await getContainerServiceByName('Does Not Exist'); expect(result).toBeNull(); }); }); describe('listContainerServices', () => { it('returns empty array when no services', async () => { const services = await listContainerServices(); expect(services).toEqual([]); }); it('lists all services without filters', async () => { await addContainerService({ name: 'Service 1', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://test1' }, }); await addContainerService({ name: 'Service 2', providerName: 'digitalocean', zones: ['external'], providerConfig: {}, apiCredentials: { api_token: 'test2' }, }); const services = await listContainerServices(); expect(services).toHaveLength(2); }); it('filters services by zone', async () => { await addContainerService({ name: 'Proxmox Internal', providerName: 'proxmox', zones: ['dmz', 'app', 'secure'], providerConfig: {}, apiCredentials: { api_url: 'https://test1' }, }); await addContainerService({ name: 'Digital Ocean External', providerName: 'digitalocean', zones: ['external'], providerConfig: {}, apiCredentials: { api_token: 'test2' }, }); const externalServices = await listContainerServices({ zones: ['external'], }); expect(externalServices).toHaveLength(1); expect(externalServices[0].name).toBe('Digital Ocean External'); }); it('returns services matching any zone in filter', async () => { await addContainerService({ name: 'Multi-Zone Service', providerName: 'proxmox', zones: ['dmz', 'app'], providerConfig: {}, apiCredentials: { api_url: 'https://test' }, }); const dmzServices = await listContainerServices({ zones: ['dmz'] }); const appServices = await listContainerServices({ zones: ['app'] }); expect(dmzServices).toHaveLength(1); expect(appServices).toHaveLength(1); }); }); describe('getServiceCredentials', () => { it('decrypts and returns service credentials', async () => { const service = await addContainerService({ name: 'Test Service', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://proxmox.local:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'secret-token-123', }, }); const credentials = await getServiceCredentials(service.id); expect((credentials as { api_url: string }).api_url).toBe('https://proxmox.local:8006'); expect((credentials as { api_token_id: string }).api_token_id).toBe('root@pam!celilo'); expect((credentials as { api_token_secret: string }).api_token_secret).toBe( 'secret-token-123', ); }); it('throws error for non-existent service', async () => { await expect(getServiceCredentials('non-existent-id')).rejects.toThrow( /Container service not found/, ); }); it('re-encrypts replacements and clears stale verification state', async () => { const service = await addContainerService({ name: 'Moving Proxmox', providerName: 'proxmox', zones: ['internal'], providerConfig: {}, apiCredentials: { api_url: 'https://192.168.0.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }, }); await updateVerificationStatus(service.id, { success: true, message: 'Connected' }); await updateServiceCredentials(service.id, { api_url: 'https://10.77.20.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }); const credentials = await getServiceCredentials(service.id); const updated = await getContainerService(service.id); expect(credentials).toEqual({ api_url: 'https://10.77.20.50:8006', api_token_id: 'root@pam!celilo', api_token_secret: 'existing-secret', }); expect(updated?.verified).toBe(false); expect(updated?.verifiedAt).toBeNull(); expect(updated?.verificationError).toBeNull(); }); }); describe('removeContainerService', () => { it('deletes service from database', async () => { const service = await addContainerService({ name: 'Service To Delete', providerName: 'proxmox', zones: ['dmz'], providerConfig: {}, apiCredentials: { api_url: 'https://test' }, }); await removeContainerService(service.id); const retrieved = await getContainerService(service.id); expect(retrieved).toBeNull(); }); it('does not throw when removing non-existent service', async () => { // Should complete without error await removeContainerService('non-existent-id'); // If we get here, no error was thrown expect(true).toBe(true); }); }); });