/** * Tests for zone detector */ 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 { closeDb, createDbClient } from '../db/client'; import { runMigrations } from '../db/migrate'; import { systemConfig } from '../db/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { detectZoneFromIp, isValidIp } from './zone-detector'; describe('zone-detector', () => { 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); // Insert test network configuration const db = createDbClient({ path: testDbPath }); await db.insert(systemConfig).values([ { key: 'network.internal.subnet', value: '192.168.0.0/24' }, { key: 'network.dmz.subnet', value: '10.0.10.0/24' }, { key: 'network.app.subnet', value: '10.0.20.0/24' }, { key: 'network.secure.subnet', value: '10.0.30.0/24' }, ]); }); afterEach(() => { // Close database connection closeDb(); // Clean up test directory if (testDir) { rmSync(testDir, { recursive: true, force: true }); } // Clear environment variables resetTestDbPath(); }); describe('detectZoneFromIp', () => { it('detects internal zone', async () => { const zone = await detectZoneFromIp('192.168.0.100'); expect(zone).toBe('internal'); }); it('detects dmz zone', async () => { const zone = await detectZoneFromIp('10.0.10.50'); expect(zone).toBe('dmz'); }); it('detects app zone', async () => { const zone = await detectZoneFromIp('10.0.20.100'); expect(zone).toBe('app'); }); it('detects secure zone', async () => { const zone = await detectZoneFromIp('10.0.30.25'); expect(zone).toBe('secure'); }); it("returns 'unknown' for a PUBLIC address in no declared subnet", async () => { // This asserted `external` before. It is now `'unknown'` because that is // the question this function answers: containment. Whether 167.99.123.45 // is an external EDGE is `isPubliclyRoutable`'s question, and the caller // resolves the two — see machine-add. Conflating them is the defect this // change exists to remove (design D1). const zone = await detectZoneFromIp('167.99.123.45'); expect(zone).toBe('unknown'); }); it('matches first IP in subnet', async () => { const zone = await detectZoneFromIp('192.168.0.1'); expect(zone).toBe('internal'); }); it('matches last IP in subnet', async () => { const zone = await detectZoneFromIp('192.168.0.254'); expect(zone).toBe('internal'); }); it('does not match IP outside subnet', async () => { const zone = await detectZoneFromIp('192.168.1.100'); expect(zone).toBe('unknown'); }); it("a PRIVATE address in no declared subnet is 'unknown', never 'external'", async () => { // The heart of it. 172.16.5.5 is RFC 1918 — the internet cannot route to // it under any circumstances — and it matched no declared subnet. Calling // that `external` is the claim that broke `machine add`. const zone = await detectZoneFromIp('172.16.5.5'); expect(zone).toBe('unknown'); }); /** * §5.7 — PROOF THE OLD BEHAVIOUR IS GONE. * * The proposal's opening example, against the real subnet declarations in * this fixture. Three RFC1918 gateway legs that no declared subnet contains. * Pre-change, `detectZoneFromIp` returned `'external'` for every one of * them, so `machine add` printed three private addresses as facing the * internet. This test fails against that code and passes against this. */ it('§5.7: three RFC1918 legs in no declared subnet are NOT external', async () => { const undeclaredPrivateLegs = ['172.16.5.1', '10.99.0.1', '192.168.77.1']; const zones = await Promise.all(undeclaredPrivateLegs.map((ip) => detectZoneFromIp(ip))); expect(zones).toEqual(['unknown', 'unknown', 'unknown']); // Stated separately so a failure says WHICH property broke. expect(zones.filter((z) => z === 'external')).toEqual([]); }); }); describe('isValidIp', () => { it('validates correct IPv4 addresses', () => { expect(isValidIp('192.168.1.1')).toBe(true); expect(isValidIp('10.0.0.1')).toBe(true); expect(isValidIp('255.255.255.255')).toBe(true); expect(isValidIp('0.0.0.0')).toBe(true); }); it('rejects invalid IPv4 addresses', () => { expect(isValidIp('192.168.1.256')).toBe(false); // Number > 255 expect(isValidIp('192.168.1')).toBe(false); // Too few octets expect(isValidIp('192.168.1.1.1')).toBe(false); // Too many octets expect(isValidIp('not.an.ip.address')).toBe(false); // Non-numeric expect(isValidIp('')).toBe(false); // Empty string }); }); });