import { describe, expect, test } from 'bun:test'; import { resolveTemplate, resolveVariable } from './resolver'; import type { ResolutionContext, VariableReference } from './types'; // Mock database for tests (capability secrets not tested here - see integration tests) const mockDb = { $client: { prepare: (_query: string) => ({ get: () => { // Return null for capability/module lookups (non-secret in unit tests) // This makes isCapabilityFieldSecret() return false return null; }, all: () => [], run: () => ({ changes: 0 }), }), }, select: () => ({ from: () => ({ where: () => ({ get: () => null, all: () => [], }), }), }), // biome-ignore lint/suspicious/noExplicitAny: mock database for testing, full type not needed } as any; const createContext = (overrides?: Partial): ResolutionContext => ({ moduleId: 'test-module', selfConfig: { target_ip: '192.168.0.50', hostname: 'homebridge', cores: '2', 'resources.machine.cpu': '2', 'resources.machine.memory': '2048', 'resources.machine.disk': '20', 'resources.machine.zone': 'app', // Auto-derived inventory variables 'inventory.ansible_host': '192.168.0.50', 'inventory.ansible_user': 'root', 'inventory.groups': 'homebridge', }, systemConfig: { 'management.ip': '192.168.0.10', 'network.domain': 'homelab.local', }, systemSecrets: {}, secrets: { api_key: 'secret123', db_password: 'pass456', }, capabilities: { dns_external: { nameserver: 'ns1.example.com', zone: 'example.com', config: { port: 53, }, }, idp: { issuer: 'https://auth.example.com', }, }, ...overrides, }); describe('resolveVariable', () => { test('should resolve self variable', async () => { const variable: VariableReference = { type: 'self', path: 'target_ip', raw: '$self:target_ip', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.value).toBe('192.168.0.50'); } }); describe('$infra: deployed-system selector', () => { const infraContext = () => createContext({ systems: { main: { name: 'main', hostname: 'homebridge', ipv4_address: '10.0.20.20', zone: 'app', cidr: '10.0.20.20/24', vmid: '2100', }, }, }); test('resolves $infra:.ipv4_address', async () => { const result = await resolveVariable( { type: 'infra', path: 'main.ipv4_address', raw: '$infra:main.ipv4_address' }, infraContext(), mockDb, ); expect(result.success).toBe(true); if (result.success) expect(result.value).toBe('10.0.20.20'); }); test('resolves $infra:.cidr and .vmid', async () => { const ctx = infraContext(); const cidr = await resolveVariable( { type: 'infra', path: 'main.cidr', raw: '$infra:main.cidr' }, ctx, mockDb, ); const vmid = await resolveVariable( { type: 'infra', path: 'main.vmid', raw: '$infra:main.vmid' }, ctx, mockDb, ); expect(cidr.success && cidr.value).toBe('10.0.20.20/24'); expect(vmid.success && vmid.value).toBe('2100'); }); test('fails clearly for an unknown system name', async () => { const result = await resolveVariable( { type: 'infra', path: 'db.ipv4_address', raw: '$infra:db.ipv4_address' }, infraContext(), mockDb, ); expect(result.success).toBe(false); if (!result.success) expect(result.error).toContain("No deployed system named 'db'"); }); test('fails when no systems recorded (API-only / pre-deploy)', async () => { const result = await resolveVariable( { type: 'infra', path: 'main.ipv4_address', raw: '$infra:main.ipv4_address' }, createContext(), mockDb, ); expect(result.success).toBe(false); }); }); test('should resolve system variable', async () => { const variable: VariableReference = { type: 'system', path: 'management.ip', raw: '$system:management.ip', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.value).toBe('192.168.0.10'); } }); test('should resolve secret variable', async () => { const variable: VariableReference = { type: 'secret', path: 'api_key', raw: '$secret:api_key', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.value).toBe('secret123'); } }); test('should resolve capability variable', async () => { const variable: VariableReference = { type: 'capability', path: 'dns_external.nameserver', raw: '$capability:dns_external.nameserver', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.value).toBe('ns1.example.com'); } }); test('should resolve nested capability variable', async () => { const variable: VariableReference = { type: 'capability', path: 'dns_external.config.port', raw: '$capability:dns_external.config.port', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.value).toBe('53'); } }); test('should return error for missing self variable', async () => { const variable: VariableReference = { type: 'self', path: 'missing_var', raw: '$self:missing_var', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('not found in module configuration'); } }); test('should return error for missing system variable', async () => { const variable: VariableReference = { type: 'system', path: 'missing.var', raw: '$system:missing.var', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('not found'); } }); test('should return error for missing secret', async () => { const variable: VariableReference = { type: 'secret', path: 'missing_secret', raw: '$secret:missing_secret', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('not found'); } }); test('should return error for missing capability', async () => { const variable: VariableReference = { type: 'capability', path: 'missing_capability.value', raw: '$capability:missing_capability.value', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('not found or not registered'); } }); test('should return error for capability variable without path', async () => { const variable: VariableReference = { type: 'capability', path: 'dns_external', raw: '$capability:dns_external', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('must specify data path'); } }); test('should return error for missing capability data path', async () => { const variable: VariableReference = { type: 'capability', path: 'dns_external.missing_field', raw: '$capability:dns_external.missing_field', }; const context = createContext(); const result = await resolveVariable(variable, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain('not found in'); } }); }); describe('resolveTemplate', () => { test('should resolve template with single variable', async () => { const template = 'ip: $self:target_ip'; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toBe('ip: 192.168.0.50'); } }); test('should resolve template with multiple variables', async () => { const template = ` hostname: $self:hostname ip: $self:target_ip domain: $system:network.domain `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('hostname: homebridge'); expect(result.content).toContain('ip: 192.168.0.50'); expect(result.content).toContain('domain: homelab.local'); } }); test('should resolve template with mixed variable types', async () => { const template = ` target_ip: $self:target_ip management_ip: $system:management.ip dns_server: $capability:dns_external.nameserver api_key: $secret:api_key `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('target_ip: 192.168.0.50'); expect(result.content).toContain('management_ip: 192.168.0.10'); expect(result.content).toContain('dns_server: ns1.example.com'); expect(result.content).toContain('api_key: secret123'); } }); test('should resolve VM resource variables from manifest', async () => { const template = ` resource "proxmox_lxc" "grafana" { cores = $self:resources.machine.cpu memory = $self:resources.machine.memory rootfs { size = "$self:resources.machine.disk" storage = "$self:resources.machine.zone" } } `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('cores = 2'); expect(result.content).toContain('memory = 2048'); expect(result.content).toContain('size = "20"'); expect(result.content).toContain('storage = "app"'); } }); test('should resolve template with variables in complex structure', async () => { const template = ` resource "proxmox_lxc" "container" { hostname = "$self:hostname" cores = $self:cores network { ip = "$self:target_ip/24" gateway = "$system:management.ip" } environment = { DNS_SERVER = "$capability:dns_external.nameserver" API_KEY = "$secret:api_key" } } `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('hostname = "homebridge"'); expect(result.content).toContain('cores = 2'); expect(result.content).toContain('ip = "192.168.0.50/24"'); expect(result.content).toContain('gateway = "192.168.0.10"'); expect(result.content).toContain('DNS_SERVER = "ns1.example.com"'); expect(result.content).toContain('API_KEY = "secret123"'); } }); test('should return template unchanged if no variables', async () => { const template = 'no variables here'; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toBe(template); } }); test('should return errors for missing variables', async () => { const template = ` ip: $self:target_ip missing: $self:missing_var another_missing: $secret:missing_secret `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(false); if (!result.success) { expect(result.errors).toHaveLength(2); expect(result.errors[0]?.variable).toBe('$self:missing_var'); expect(result.errors[1]?.variable).toBe('$secret:missing_secret'); } }); test('should handle repeated variables', async () => { const template = ` ip1: $self:target_ip ip2: $self:target_ip ip3: $self:target_ip `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('ip1: 192.168.0.50'); expect(result.content).toContain('ip2: 192.168.0.50'); expect(result.content).toContain('ip3: 192.168.0.50'); expect(result.content).not.toContain('$self:target_ip'); } }); test('should resolve auto-derived inventory variables', async () => { const template = ` [all] $self:hostname ansible_host=$self:inventory.ansible_host ansible_user=$self:inventory.ansible_user [homebridge] $self:hostname `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('homebridge ansible_host=192.168.0.50 ansible_user=root'); expect(result.content).toContain('[homebridge]'); } }); test('should use stripped IP in ansible_host', async () => { const template = 'ansible_host: $self:inventory.ansible_host'; const context = createContext({ selfConfig: { target_ip: '10.0.10.10/24', 'inventory.ansible_host': '10.0.10.10', // Auto-derived (CIDR stripped) 'inventory.ansible_user': 'root', 'inventory.groups': 'test', }, }); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('ansible_host: 10.0.10.10'); expect(result.content).not.toContain('/24'); } }); test('should resolve inventory.groups from module ID', async () => { const template = ` --- all: children: $self:inventory.groups: hosts: $self:hostname: `; const context = createContext(); const result = await resolveTemplate(template, context, mockDb); expect(result.success).toBe(true); if (result.success) { expect(result.content).toContain('homebridge:'); expect(result.content).toContain('hosts:'); } }); });