/** * IPAM Auto-Allocator Tests */ import { Database } from 'bun:sqlite'; import { beforeEach, describe, expect, test } from 'bun:test'; import { drizzle } from 'drizzle-orm/bun-sqlite'; import type { DbClient } from '../db/client'; import * as schema from '../db/schema'; import { allocateForModule, deallocateForModule, getAllocation } from './auto-allocator'; describe('IPAM Auto-Allocator', () => { let db: Database; let drizzleDb: DbClient; beforeEach(() => { // Create in-memory database for testing db = new Database(':memory:'); drizzleDb = drizzle(db, { schema }); // Create required tables db.exec(` CREATE TABLE ip_allocations ( id INTEGER PRIMARY KEY AUTOINCREMENT, module_id TEXT NOT NULL REFERENCES modules(id) ON DELETE CASCADE, vmid INTEGER NOT NULL UNIQUE, container_ip TEXT NOT NULL UNIQUE, zone TEXT NOT NULL, allocated_at INTEGER NOT NULL ); CREATE TABLE vmid_reservations ( vmid INTEGER PRIMARY KEY NOT NULL, reason TEXT, created_at TEXT NOT NULL ); CREATE TABLE ip_reservations ( id INTEGER PRIMARY KEY AUTOINCREMENT, zone TEXT NOT NULL, ip_start TEXT NOT NULL, ip_end TEXT, reason TEXT, created_at TEXT NOT NULL ); CREATE TABLE system_config ( key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); `); // Insert test zone subnet configuration db.prepare( `INSERT INTO system_config (key, value, created_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))`, ).run('network.dmz.subnet', '10.0.10.0/24'); db.prepare( `INSERT INTO system_config (key, value, created_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))`, ).run('network.app.subnet', '10.0.20.0/24'); }); describe('allocateForModule', () => { test('allocates first VMID starting at 200', async () => { const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.vmid).toBe(200); expect(allocation.moduleId).toBe('test-module'); expect(allocation.zone).toBe('dmz'); }); test('allocates first IP starting at .10 in CIDR format', async () => { const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.containerIp).toBe('10.0.10.10/24'); }); test('allocates sequential VMIDs for multiple modules', async () => { const alloc1 = await allocateForModule('module-1', 'dmz', drizzleDb, db); const alloc2 = await allocateForModule('module-2', 'dmz', drizzleDb, db); const alloc3 = await allocateForModule('module-3', 'app', drizzleDb, db); expect(alloc1.vmid).toBe(200); expect(alloc2.vmid).toBe(201); expect(alloc3.vmid).toBe(202); }); test('allocates sequential IPs within same zone', async () => { const alloc1 = await allocateForModule('module-1', 'dmz', drizzleDb, db); const alloc2 = await allocateForModule('module-2', 'dmz', drizzleDb, db); expect(alloc1.containerIp).toBe('10.0.10.10/24'); expect(alloc2.containerIp).toBe('10.0.10.11/24'); }); test('allocates IPs from different zone subnets', async () => { const dmzAlloc = await allocateForModule('module-dmz', 'dmz', drizzleDb, db); const appAlloc = await allocateForModule('module-app', 'app', drizzleDb, db); expect(dmzAlloc.containerIp).toBe('10.0.10.10/24'); expect(appAlloc.containerIp).toBe('10.0.20.10/24'); }); test('skips reserved VMIDs', async () => { // Reserve VMID 200 db.prepare( `INSERT INTO vmid_reservations (vmid, reason, created_at) VALUES (?, ?, datetime('now'))`, ).run(200, 'Test reservation'); const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.vmid).toBe(201); }); test('skips reserved IP addresses', async () => { // Reserve IP 10.0.10.10 db.prepare( `INSERT INTO ip_reservations (zone, ip_start, ip_end, reason, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, ).run('dmz', '10.0.10.10', null, 'Test reservation'); const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.containerIp).toBe('10.0.10.11/24'); }); test('skips reserved IP ranges', async () => { // Reserve range 10.0.10.10-10.0.10.12 db.prepare( `INSERT INTO ip_reservations (zone, ip_start, ip_end, reason, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, ).run('dmz', '10.0.10.10', '10.0.10.12', 'Test range reservation'); const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.containerIp).toBe('10.0.10.13/24'); }); test('reuses existing allocation for same module', async () => { const alloc1 = await allocateForModule('test-module', 'dmz', drizzleDb, db); const alloc2 = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(alloc2.vmid).toBe(alloc1.vmid); expect(alloc2.containerIp).toBe(alloc1.containerIp); }); test('throws error when zone subnet not configured', async () => { await expect(allocateForModule('test-module', 'invalid-zone', drizzleDb, db)).rejects.toThrow( 'Zone subnet not configured', ); }); }); describe('getAllocation', () => { test('returns null when no allocation exists', () => { const allocation = getAllocation('nonexistent-module', drizzleDb); expect(allocation).toBeNull(); }); test('returns existing allocation', async () => { await allocateForModule('test-module', 'dmz', drizzleDb, db); const allocation = getAllocation('test-module', drizzleDb); expect(allocation).not.toBeNull(); expect(allocation?.moduleId).toBe('test-module'); expect(allocation?.vmid).toBe(200); expect(allocation?.containerIp).toBe('10.0.10.10/24'); expect(allocation?.zone).toBe('dmz'); }); }); describe('deallocateForModule', () => { test('removes allocation for module', async () => { await allocateForModule('test-module', 'dmz', drizzleDb, db); const removed = await deallocateForModule('test-module', drizzleDb); expect(removed).toBe(true); const allocation = getAllocation('test-module', drizzleDb); expect(allocation).toBeNull(); }); test('returns false when no allocation exists', async () => { const removed = await deallocateForModule('nonexistent-module', drizzleDb); expect(removed).toBe(false); }); test('makes VMID and IP available for reallocation', async () => { await allocateForModule('module-1', 'dmz', drizzleDb, db); await allocateForModule('module-2', 'dmz', drizzleDb, db); // Remove first module await deallocateForModule('module-1', drizzleDb); // Allocate new module - should get VMID 200 (first available) const newAlloc = await allocateForModule('module-3', 'dmz', drizzleDb, db); expect(newAlloc.vmid).toBe(200); expect(newAlloc.containerIp).toBe('10.0.10.10/24'); }); }); describe('VMID allocation edge cases', () => { test('handles large VMID gaps with reservations', async () => { // Reserve range 200-299 for (let i = 200; i < 300; i++) { db.prepare( `INSERT INTO vmid_reservations (vmid, reason, created_at) VALUES (?, ?, datetime('now'))`, ).run(i, 'Reserved'); } const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); expect(allocation.vmid).toBe(300); }); }); describe('IP allocation edge cases', () => { test('handles subnet with many allocations', async () => { // Allocate first 10 IPs for (let i = 0; i < 10; i++) { await allocateForModule(`module-${i}`, 'dmz', drizzleDb, db); } const allocation = await allocateForModule('module-10', 'dmz', drizzleDb, db); expect(allocation.containerIp).toBe('10.0.10.20/24'); // .10-.19 taken, .20 is next }); test('skips infrastructure range (.1-.9)', async () => { const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db); // Should start at .10, not .1 or .2 expect(allocation.containerIp).toBe('10.0.10.10/24'); }); }); });