import { describe, it, expect } from 'vitest'; import yaml from 'js-yaml'; import { generateComposeFile, generateCaddyfile, generateLabels, } from './compose-generator.js'; import type { PreviewConfig } from './types.js'; describe('generateComposeFile', () => { const baseOpts = { projectPath: '/workspace/repos/my-project-abc123', port: 9100, deploymentId: 'test123', labels: { 'cw.preview': 'true', 'cw.initiative-id': 'init-1', 'cw.port': '9100', }, }; it('generates valid compose YAML with user services and Caddy proxy', () => { const config: PreviewConfig = { version: 1, services: { app: { name: 'app', build: '.', port: 3000, }, }, }; const result = generateComposeFile(config, baseOpts); const parsed = yaml.load(result) as Record; // Has both user service and caddy expect(parsed.services.app).toBeDefined(); expect(parsed.services['caddy-proxy']).toBeDefined(); // Network present expect(parsed.networks.preview).toBeDefined(); // Caddy publishes port expect(parsed.services['caddy-proxy'].ports).toContain('9100:80'); // Labels propagated expect(parsed.services.app.labels['cw.preview']).toBe('true'); }); it('handles object build config with context path joining', () => { const config: PreviewConfig = { version: 1, services: { api: { name: 'api', build: { context: 'packages/api', dockerfile: 'Dockerfile.prod' }, port: 8080, }, }, }; const result = generateComposeFile(config, baseOpts); const parsed = yaml.load(result) as Record; expect(parsed.services.api.build.context).toBe( '/workspace/repos/my-project-abc123/packages/api', ); expect(parsed.services.api.build.dockerfile).toBe('Dockerfile.prod'); }); it('handles image-based services', () => { const config: PreviewConfig = { version: 1, services: { db: { name: 'db', image: 'postgres:16', port: 5432, internal: true, env: { POSTGRES_PASSWORD: 'test' }, }, }, }; const result = generateComposeFile(config, baseOpts); const parsed = yaml.load(result) as Record; expect(parsed.services.db.image).toBe('postgres:16'); expect(parsed.services.db.environment.POSTGRES_PASSWORD).toBe('test'); }); it('caddy depends on all user services', () => { const config: PreviewConfig = { version: 1, services: { frontend: { name: 'frontend', build: '.', port: 3000 }, backend: { name: 'backend', build: '.', port: 8080 }, }, }; const result = generateComposeFile(config, baseOpts); const parsed = yaml.load(result) as Record; expect(parsed.services['caddy-proxy'].depends_on).toContain('frontend'); expect(parsed.services['caddy-proxy'].depends_on).toContain('backend'); }); }); describe('generateCaddyfile', () => { it('generates simple single-service Caddyfile', () => { const config: PreviewConfig = { version: 1, services: { app: { name: 'app', build: '.', port: 3000 }, }, }; const caddyfile = generateCaddyfile(config); expect(caddyfile).toContain(':80 {'); expect(caddyfile).toContain('reverse_proxy app:3000'); expect(caddyfile).toContain('}'); }); it('generates multi-service Caddyfile with handle_path for non-root routes', () => { const config: PreviewConfig = { version: 1, services: { frontend: { name: 'frontend', build: '.', port: 3000, route: '/' }, backend: { name: 'backend', build: '.', port: 8080, route: '/api' }, }, }; const caddyfile = generateCaddyfile(config); expect(caddyfile).toContain('handle_path /api/*'); expect(caddyfile).toContain('reverse_proxy backend:8080'); expect(caddyfile).toContain('handle {'); expect(caddyfile).toContain('reverse_proxy frontend:3000'); }); it('excludes internal services from Caddyfile', () => { const config: PreviewConfig = { version: 1, services: { app: { name: 'app', build: '.', port: 3000 }, db: { name: 'db', image: 'postgres', port: 5432, internal: true }, }, }; const caddyfile = generateCaddyfile(config); expect(caddyfile).not.toContain('postgres'); expect(caddyfile).not.toContain('db:5432'); }); it('sorts routes by specificity (longer paths first)', () => { const config: PreviewConfig = { version: 1, services: { app: { name: 'app', build: '.', port: 3000, route: '/' }, api: { name: 'api', build: '.', port: 8080, route: '/api' }, auth: { name: 'auth', build: '.', port: 9090, route: '/api/auth' }, }, }; const caddyfile = generateCaddyfile(config); const apiAuthIdx = caddyfile.indexOf('/api/auth'); const apiIdx = caddyfile.indexOf('handle_path /api/*'); const handleIdx = caddyfile.indexOf('handle {'); // /api/auth should come before /api which should come before / expect(apiAuthIdx).toBeLessThan(apiIdx); expect(apiIdx).toBeLessThan(handleIdx); }); }); describe('generateLabels', () => { it('generates correct labels', () => { const labels = generateLabels({ initiativeId: 'init-1', phaseId: 'phase-1', projectId: 'proj-1', branch: 'feature/test', port: 9100, previewId: 'abc123', }); expect(labels['cw.preview']).toBe('true'); expect(labels['cw.initiative-id']).toBe('init-1'); expect(labels['cw.phase-id']).toBe('phase-1'); expect(labels['cw.project-id']).toBe('proj-1'); expect(labels['cw.branch']).toBe('feature/test'); expect(labels['cw.port']).toBe('9100'); expect(labels['cw.preview-id']).toBe('abc123'); }); it('omits phaseId label when not provided', () => { const labels = generateLabels({ initiativeId: 'init-1', projectId: 'proj-1', branch: 'main', port: 9100, previewId: 'abc123', }); expect(labels['cw.phase-id']).toBeUndefined(); }); });