import { describe, expect, test } from 'bun:test'; import { ComputedEvalError, type LookupFn, evaluateComputed } from './evaluate'; import { ComputedParseError, parseComputed } from './parse'; /** * A lookup over a fixed fixture, mirroring how the resolver will map * roots (self/system/secret/...) onto live data. Returns undefined for * anything absent so the evaluator's missing-ref path is exercised. */ function makeLookup(data: Record): LookupFn { return (root, path) => { let current: unknown = data[root]; for (const seg of path) { if (current && typeof current === 'object' && seg in (current as Record)) { current = (current as Record)[seg]; } else { return undefined; } } return current; }; } const FIXTURE = { secret: { ddns_passwords: { 'example.net': 'pw1', 'celilo.computer': 'pw2' }, }, self: { zone_names: { dmz: 'DMZ', app: 'App' }, upstreams: [{ ip: '1.1.1.1' }, { ip: '8.8.8.8' }], primary_domains: ['a.net', 'b.net'], extra_domains: ['b.net', 'c.net'], hostname: 'dns-int', zones_with_dupes: ['x', 'y', 'x', 'z', 'y'], }, system: { primary_domain: 'example.net', }, }; function evalOk(expr: string): unknown { return evaluateComputed(expr, makeLookup(FIXTURE)); } describe('computed DSL — keys', () => { test('keys of the ddns_passwords secret map (the domain_list case)', () => { expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['example.net', 'celilo.computer']); }); test('keys of a non-secret object', () => { expect(evalOk('keys(self.zone_names)')).toEqual(['dmz', 'app']); }); }); describe('computed DSL — values', () => { test('values of a NON-secret map is allowed', () => { expect(evalOk('values(self.zone_names)')).toEqual(['DMZ', 'App']); }); test('values of a secret map is REJECTED (projection rule)', () => { expect(() => evalOk('values(secret.ddns_passwords)')).toThrow(ComputedEvalError); expect(() => evalOk('values(secret.ddns_passwords)')).toThrow(/leak/i); }); test('keys of a secret map is allowed (key names are non-sensitive)', () => { expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['example.net', 'celilo.computer']); }); }); describe('computed DSL — map', () => { test('projects a field from a list of objects', () => { expect(evalOk('map(self.upstreams, ip)')).toEqual(['1.1.1.1', '8.8.8.8']); }); test('errors when an element is not an object', () => { expect(() => evalOk('map(self.primary_domains, ip)')).toThrow(ComputedEvalError); }); test('errors when the field arg is not a bare identifier', () => { expect(() => evalOk("map(self.upstreams, 'ip')")).toThrow(/bare field name/); }); }); describe('computed DSL — concat + unique (nesting/chaining)', () => { test('concat flattens multiple arrays', () => { expect(evalOk('concat(self.primary_domains, self.extra_domains)')).toEqual([ 'a.net', 'b.net', 'b.net', 'c.net', ]); }); test('unique dedupes', () => { expect(evalOk('unique(self.zones_with_dupes)')).toEqual(['x', 'y', 'z']); }); test('nested calls chain: unique(concat(...))', () => { expect(evalOk('unique(concat(self.primary_domains, self.extra_domains))')).toEqual([ 'a.net', 'b.net', 'c.net', ]); }); }); describe('computed DSL — format', () => { test('interpolates named parts', () => { expect(evalOk("format('{host}.{zone}', host=self.hostname, zone=system.primary_domain)")).toBe( 'dns-int.example.net', ); }); test('errors when template references an unsupplied part', () => { expect(() => evalOk("format('{host}.{missing}', host=self.hostname)")).toThrow( /no such named argument/, ); }); test('errors when a non-template arg is positional', () => { expect(() => evalOk("format('{a}', self.hostname)")).toThrow(/must be named/); }); }); describe('computed DSL — reference & arity errors', () => { test('missing reference throws', () => { expect(() => evalOk('keys(secret.nonexistent)')).toThrow(/could not be resolved/); }); test('keys on a non-object throws', () => { expect(() => evalOk('keys(self.hostname)')).toThrow(/expects an object/); }); test('unknown function is rejected', () => { expect(() => evalOk('frobnicate(self.hostname)')).toThrow(/Unknown function/); }); test('wrong arity is rejected', () => { expect(() => evalOk('keys(self.zone_names, self.upstreams)')).toThrow(/expects 1 argument/); }); test('named arg to a function that does not take them is rejected', () => { expect(() => evalOk('keys(x=self.zone_names)')).toThrow(/does not take named arguments/); }); }); describe('computed DSL — parser', () => { test('parses a simple call to a ref-arg AST', () => { const ast = parseComputed('keys(secret.ddns_passwords)'); expect(ast).toEqual({ kind: 'call', fn: 'keys', args: [{ value: { kind: 'ref', root: 'secret', path: ['ddns_passwords'] } }], }); }); test('parses nested calls', () => { const ast = parseComputed('unique(concat(self.a, self.b))'); expect(ast.kind).toBe('call'); }); test('empty expression is a parse error', () => { expect(() => parseComputed('')).toThrow(ComputedParseError); }); test('unterminated string is a parse error', () => { expect(() => parseComputed("format('{a}")).toThrow(/unterminated string/); }); test('trailing input is a parse error', () => { expect(() => parseComputed('keys(self.x) extra')).toThrow(/trailing input/); }); test('unexpected character is a parse error', () => { expect(() => parseComputed('keys(self.x) + 1')).toThrow(ComputedParseError); }); });