import { describe, expect, test } from 'bun:test'; import { applyIndex, hasVariables, isValidVariableFormat, parsePath, parseVariables, } from './parser'; describe('parseVariables', () => { test('should parse self variables', () => { const content = 'ip: $self:target_ip'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'self', path: 'target_ip', raw: '$self:target_ip', }); }); test('should parse system variables', () => { const content = 'server: $system:management.ip'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'system', path: 'management.ip', raw: '$system:management.ip', }); }); test('should parse secret variables', () => { const content = 'key: $secret:api_key'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'secret', path: 'api_key', raw: '$secret:api_key', }); }); test('should parse capability variables', () => { const content = 'dns: $capability:dns_external.nameserver'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'capability', path: 'dns_external.nameserver', raw: '$capability:dns_external.nameserver', }); }); test('should parse multiple variables in same content', () => { const content = ` ip: $self:target_ip dns: $capability:dns_external.nameserver key: $secret:api_key `; const variables = parseVariables(content); expect(variables).toHaveLength(3); expect(variables[0]?.type).toBe('self'); expect(variables[1]?.type).toBe('capability'); expect(variables[2]?.type).toBe('secret'); }); test('should handle nested paths in capability variables', () => { const content = '$capability:dns_external.config.primary.nameserver'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]?.path).toBe('dns_external.config.primary.nameserver'); }); test('should handle underscores and numbers in paths', () => { const content = '$self:vlan_id_10'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]?.path).toBe('vlan_id_10'); }); test('should not parse paths starting with numbers', () => { const content = '$self:123invalid'; const variables = parseVariables(content); expect(variables).toHaveLength(0); }); test('should return empty array for content without variables', () => { const content = 'no variables here'; const variables = parseVariables(content); expect(variables).toHaveLength(0); }); test('should handle variables in complex template', () => { const content = ` resource "proxmox_lxc" "homebridge" { hostname = "$self:hostname" cores = $self:cores memory = $self:memory network { ip = "$self:target_ip/24" gw = "$system:management.ip" } } `; const variables = parseVariables(content); expect(variables.length).toBeGreaterThan(3); }); test('should parse braced variables for concatenation', () => { const content = 'size = "${self:disk}G"'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'self', path: 'disk', raw: '${self:disk}', }); }); test('should parse multiple braced variables', () => { const content = 'url = "${system:protocol}://${system:host}:${system:port}"'; const variables = parseVariables(content); expect(variables).toHaveLength(3); expect(variables[0]?.raw).toBe('${system:protocol}'); expect(variables[1]?.raw).toBe('${system:host}'); expect(variables[2]?.raw).toBe('${system:port}'); }); test('should handle mix of braced and simple variables', () => { const content = 'size = "${self:disk}G" and vmid = $self:vmid'; const variables = parseVariables(content); expect(variables).toHaveLength(2); expect(variables[0]?.raw).toBe('${self:disk}'); expect(variables[1]?.raw).toBe('$self:vmid'); }); test('should handle braced variables with nested paths', () => { const content = 'api = "${capability:auth.endpoint.url}/v1"'; const variables = parseVariables(content); expect(variables).toHaveLength(1); expect(variables[0]).toEqual({ type: 'capability', path: 'auth.endpoint.url', raw: '${capability:auth.endpoint.url}', }); }); test('should NOT match Terraform variables (no colon after type)', () => { const content = ` hostname = "\${var.hostname}" ip = "\${local.ip_address}" id = "\${proxmox_lxc.caddy.id}" cidr = "\${var.network.cidr}" `; const variables = parseVariables(content); // Should find zero variables because Terraform uses dots, not colons expect(variables).toHaveLength(0); }); test('should match our syntax but not Terraform syntax in same file', () => { const content = ` # Our syntax (with colon) size = "\${self:disk}G" # Terraform syntax (with dots) hostname = "\${var.hostname}" # Our syntax again vmid = $self:vmid `; const variables = parseVariables(content); // Should only find our 2 variables expect(variables).toHaveLength(2); expect(variables[0]?.raw).toBe('${self:disk}'); expect(variables[1]?.raw).toBe('$self:vmid'); }); }); describe('hasVariables', () => { test('should return true for content with variables', () => { expect(hasVariables('ip: $self:target_ip')).toBe(true); expect(hasVariables('$secret:key')).toBe(true); }); test('should return true for content with braced variables', () => { expect(hasVariables('size: "${self:disk}G"')).toBe(true); expect(hasVariables('${system:url}')).toBe(true); }); test('should return false for content without variables', () => { expect(hasVariables('no variables')).toBe(false); expect(hasVariables('dollar sign $ but not a variable')).toBe(false); }); }); describe('isValidVariableFormat', () => { test('should validate correct variable formats', () => { expect(isValidVariableFormat('$self:target_ip')).toBe(true); expect(isValidVariableFormat('$system:management.ip')).toBe(true); expect(isValidVariableFormat('$secret:api_key')).toBe(true); expect(isValidVariableFormat('$capability:dns_external.nameserver')).toBe(true); }); test('should validate correct braced variable formats', () => { expect(isValidVariableFormat('${self:disk}')).toBe(true); expect(isValidVariableFormat('${system:url}')).toBe(true); expect(isValidVariableFormat('${capability:auth.endpoint}')).toBe(true); }); test('should reject invalid variable formats', () => { expect(isValidVariableFormat('self:target_ip')).toBe(false); // missing $ expect(isValidVariableFormat('$unknown:value')).toBe(false); // invalid type expect(isValidVariableFormat('$self:')).toBe(false); // missing path expect(isValidVariableFormat('$self')).toBe(false); // missing colon and path expect(isValidVariableFormat('$self:123')).toBe(false); // path starts with number expect(isValidVariableFormat('$self:path with spaces')).toBe(false); // spaces not allowed expect(isValidVariableFormat('${self:test')).toBe(false); // missing closing brace expect(isValidVariableFormat('$self:test}')).toBe(false); // mismatched brace }); test('should accept [N] array-index suffix on $self:', () => { expect(isValidVariableFormat('$self:domains[0]')).toBe(true); expect(isValidVariableFormat('$self:domains[42]')).toBe(true); expect(isValidVariableFormat('${self:domains[0]}')).toBe(true); }); test('should reject malformed [N] indexes', () => { expect(isValidVariableFormat('$self:domains[]')).toBe(false); expect(isValidVariableFormat('$self:domains[a]')).toBe(false); expect(isValidVariableFormat('$self:domains[-1]')).toBe(false); expect(isValidVariableFormat('$self:domains[0')).toBe(false); }); }); describe('parsePath', () => { test('returns name without index when no brackets', () => { expect(parsePath('domains')).toEqual({ name: 'domains' }); expect(parsePath('a.b.c')).toEqual({ name: 'a.b.c' }); }); test('separates trailing [N] into index field', () => { expect(parsePath('domains[0]')).toEqual({ name: 'domains', index: 0 }); expect(parsePath('domains[7]')).toEqual({ name: 'domains', index: 7 }); expect(parsePath('a.b.c[2]')).toEqual({ name: 'a.b.c', index: 2 }); }); }); describe('applyIndex', () => { test('returns the value unchanged when index is undefined', () => { expect(applyIndex('hello', undefined)).toBe('hello'); expect(applyIndex(['a', 'b'], undefined)).toEqual(['a', 'b']); }); test('drills into array at the given index', () => { expect(applyIndex(['a', 'b', 'c'], 0)).toBe('a'); expect(applyIndex(['a', 'b', 'c'], 2)).toBe('c'); }); test('returns undefined for out-of-bounds indexes', () => { expect(applyIndex(['a'], 5)).toBeUndefined(); expect(applyIndex([], 0)).toBeUndefined(); }); test('returns undefined when applying an index to a non-array', () => { expect(applyIndex('not an array', 0)).toBeUndefined(); expect(applyIndex(42, 0)).toBeUndefined(); expect(applyIndex(null, 0)).toBeUndefined(); }); });