/** * Unit tests for machine pool 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, getDb } from '../db/client'; import { runMigrations } from '../db/migrate'; import { machines, moduleInfrastructure, modules } from '../db/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { addMachine, getMachine, getMachineByHostname, getMachineSshKey, getModulesOnMachine, listMachines, removeMachine, } from './machine-pool'; describe('machine-pool', () => { 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('addMachine', () => { it('creates a machine with hardware specs', async () => { const machine = await addMachine({ hostname: 'rpi4-lr', zone: 'internal', ipAddress: '192.168.1.100', sshUser: 'ubuntu', sshKey: 'ssh-ed25519 AAAA...', hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128, }, role: 'host', interfaces: [], }); expect(machine.id).toBeDefined(); expect(machine.hostname).toBe('rpi4-lr'); expect(machine.zone).toBe('internal'); expect(machine.ipAddress).toBe('192.168.1.100'); expect(machine.hardware.cpu_cores).toBe(4); expect(machine.createdAt).toBeInstanceOf(Date); }); it('encrypts SSH key', async () => { const machine = await addMachine({ hostname: 'test-machine', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'ssh-ed25519 AAAA_SECRET_KEY_12345', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); // Verify key is encrypted in database const db = createDbClient({ path: testDbPath }); const dbRecord = await db.select().from(machines).where(eq(machines.id, machine.id)).get(); expect(dbRecord?.sshKeyEncrypted).toBeDefined(); expect(dbRecord?.sshKeyEncrypted).not.toContain('AAAA_SECRET_KEY_12345'); }); it('supports different zones', async () => { const dmzMachine = await addMachine({ hostname: 'dmz-server', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); const externalMachine = await addMachine({ hostname: 'vps-external', zone: 'external', ipAddress: '167.99.123.45', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 }, role: 'host', interfaces: [], }); expect(dmzMachine.zone).toBe('dmz'); expect(externalMachine.zone).toBe('external'); }); }); describe('getMachine', () => { it('retrieves machine by ID', async () => { const created = await addMachine({ hostname: 'test-machine', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); const retrieved = await getMachine(created.id); expect(retrieved).toBeDefined(); expect(retrieved?.id).toBe(created.id); expect(retrieved?.hostname).toBe('test-machine'); }); it('returns null for non-existent machine', async () => { const result = await getMachine('non-existent-id'); expect(result).toBeNull(); }); }); describe('getMachineByHostname', () => { it('retrieves machine by hostname', async () => { await addMachine({ hostname: 'unique-hostname', zone: 'internal', ipAddress: '192.168.1.100', sshUser: 'ubuntu', sshKey: 'test-key', hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 }, role: 'host', interfaces: [], }); const retrieved = await getMachineByHostname('unique-hostname'); expect(retrieved).toBeDefined(); expect(retrieved?.hostname).toBe('unique-hostname'); }); it('returns null for non-existent hostname', async () => { const result = await getMachineByHostname('does-not-exist'); expect(result).toBeNull(); }); }); describe('listMachines', () => { it('returns empty array when no machines', async () => { const result = await listMachines(); expect(result).toEqual([]); }); it('lists all machines without filters', async () => { await addMachine({ hostname: 'machine-1', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); await addMachine({ hostname: 'machine-2', zone: 'internal', ipAddress: '192.168.1.100', sshUser: 'ubuntu', sshKey: 'test-key', hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 }, role: 'host', interfaces: [], }); const result = await listMachines(); expect(result).toHaveLength(2); }); it('filters machines by zone', async () => { await addMachine({ hostname: 'dmz-machine', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); await addMachine({ hostname: 'external-machine', zone: 'external', ipAddress: '167.99.123.45', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 }, role: 'host', interfaces: [], }); const dmzMachines = await listMachines({ zone: 'dmz' }); expect(dmzMachines).toHaveLength(1); expect(dmzMachines[0].hostname).toBe('dmz-machine'); const externalMachines = await listMachines({ zone: 'external' }); expect(externalMachines).toHaveLength(1); expect(externalMachines[0].hostname).toBe('external-machine'); }); }); describe('getMachineSshKey', () => { it('decrypts and returns SSH key', async () => { const machine = await addMachine({ hostname: 'test-machine', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'ssh-ed25519 AAAA_SECRET_KEY_12345', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); const decryptedKey = await getMachineSshKey(machine.id); expect(decryptedKey).toBe('ssh-ed25519 AAAA_SECRET_KEY_12345'); }); it('throws error for non-existent machine', async () => { await expect(getMachineSshKey('non-existent-id')).rejects.toThrow(/Machine not found/); }); }); /** * celilo#773. `assignModuleToMachine` / `unassignModuleFromMachine` are gone * along with the column they wrote — both were exported, neither had a single * caller, and the column they maintained had an append-only writer elsewhere * with no removal path at all. * * Occupancy is derived now, so these assert the two divergences that actually * bit the fleet rather than that a setter sets. */ describe('getModulesOnMachine', () => { async function machineWithModuleDeployed(hostname: string, moduleId: string) { const machine = await addMachine({ hostname, zone: 'internal', ipAddress: '192.168.1.100', sshUser: 'ubuntu', sshKey: 'test-key', hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 }, role: 'host', interfaces: [], }); const db = getDb(); db.insert(modules) .values({ id: moduleId, name: moduleId, version: '1.0.0', manifestData: {}, sourcePath: `/tmp/${moduleId}`, }) .run(); db.insert(moduleInfrastructure) .values({ id: `infra-${moduleId}`, moduleId, infrastructureType: 'machine', machineId: machine.id, }) .run(); return machine; } it('reports a machine that is hosting a module as occupied', async () => { // The live case: briq hosted a VERIFIED iptables and reported // "None (available)", so a second module could be placed on top of it. const machine = await machineWithModuleDeployed('briq', 'iptables'); expect(getModulesOnMachine(machine.id)).toEqual(['iptables']); }); it('frees the machine when the module is removed, with no manual step', async () => { // The other direction: the id used to survive the module forever, so // placement rejected an empty box citing a module that no longer existed // and `machine remove` refused to remove it. Nothing could clear it. const machine = await machineWithModuleDeployed('rpi4-lr', 'homebridge'); expect(getModulesOnMachine(machine.id)).toEqual(['homebridge']); getDb().delete(modules).where(eq(modules.id, 'homebridge')).run(); expect(getModulesOnMachine(machine.id)).toEqual([]); }); it('reports an empty machine as empty', async () => { const machine = await addMachine({ hostname: 'spare', zone: 'internal', ipAddress: '192.168.1.101', sshUser: 'ubuntu', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); expect(getModulesOnMachine(machine.id)).toEqual([]); }); }); describe('removeMachine', () => { it('deletes machine from database', async () => { const machine = await addMachine({ hostname: 'machine-to-delete', zone: 'dmz', ipAddress: '10.0.10.100', sshUser: 'root', sshKey: 'test-key', hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 }, role: 'host', interfaces: [], }); await removeMachine(machine.id); const retrieved = await getMachine(machine.id); expect(retrieved).toBeNull(); }); it('does not throw when removing non-existent machine', async () => { // Should complete without error await removeMachine('non-existent-id'); // If we get here, no error was thrown expect(true).toBe(true); }); }); });