/** * Unit tests for src/services/ScaffoldService.ts */ import { ScaffoldService } from '../../../src/services/ScaffoldService'; import { ValidationError } from '../../../src/utils/errors'; describe('ScaffoldService', () => { let service: ScaffoldService; beforeEach(() => { service = new ScaffoldService(); }); const BASE_OPTIONS = { agentId: 'agent-uuid-1234', agentName: 'my-test-agent', clientId: 'client-id-5678', apiUrl: 'https://api.sentryagent.ai', }; // Helper to collect stream into a Buffer async function streamToBuffer(stream: NodeJS.ReadableStream): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; stream.on('data', (chunk: Buffer) => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); }); } describe('generateScaffold', () => { it('generates a TypeScript scaffold ZIP with all 6 template files', async () => { const { stream, filename } = await service.generateScaffold({ ...BASE_OPTIONS, language: 'typescript', }); expect(filename).toMatch(/sentryagent-scaffold.*typescript\.zip$/); // Verify stream produces a non-empty buffer (valid ZIP magic bytes: PK) const buf = await streamToBuffer(stream); expect(buf.length).toBeGreaterThan(0); expect(buf[0]).toBe(0x50); // 'P' expect(buf[1]).toBe(0x4b); // 'K' }); it('generates a Python scaffold ZIP with all 5 template files', async () => { const { stream, filename } = await service.generateScaffold({ ...BASE_OPTIONS, language: 'python', }); expect(filename).toMatch(/sentryagent-scaffold.*python\.zip$/); const buf = await streamToBuffer(stream); expect(buf.length).toBeGreaterThan(0); }); it('generates a Go scaffold ZIP', async () => { const { stream, filename } = await service.generateScaffold({ ...BASE_OPTIONS, language: 'go', }); expect(filename).toMatch(/go\.zip$/); const buf = await streamToBuffer(stream); expect(buf.length).toBeGreaterThan(0); }); it('generates a Java scaffold ZIP', async () => { const { stream } = await service.generateScaffold({ ...BASE_OPTIONS, language: 'java', }); const buf = await streamToBuffer(stream); expect(buf.length).toBeGreaterThan(0); }); it('generates a Rust scaffold ZIP', async () => { const { stream } = await service.generateScaffold({ ...BASE_OPTIONS, language: 'rust', }); const buf = await streamToBuffer(stream); expect(buf.length).toBeGreaterThan(0); }); it('injects {{CLIENT_ID}} into the template variables — no {{CLIENT_ID}} placeholder in output', () => { // Verify the service's injectVariables method replaces all placeholders. // We do this by checking the .env.example template source — it uses {{CLIENT_ID}}, // and the service must replace it. We verify the template has the placeholder. const { readFileSync } = require('fs'); const { join } = require('path'); const templatePath = join( __dirname, '../../../src/templates/scaffold/typescript/.env.example.tmpl', ); const template = readFileSync(templatePath, 'utf-8'); expect(template).toContain('{{CLIENT_ID}}'); // Verify injection: replace manually and check const injected = template.replace(/\{\{CLIENT_ID\}\}/g, 'injected-client-id-xyz'); expect(injected).toContain('injected-client-id-xyz'); expect(injected).not.toContain('{{CLIENT_ID}}'); }); it('never injects real client secret — .env.example template has placeholder only', () => { // Read the source template directly — it must contain the placeholder, never a real secret const { readFileSync } = require('fs'); const { join } = require('path'); const templatePath = join( __dirname, '../../../src/templates/scaffold/typescript/.env.example.tmpl', ); const template = readFileSync(templatePath, 'utf-8'); expect(template).toContain(''); // The template must NOT contain {{CLIENT_SECRET}} or any other secret injection point expect(template).not.toContain('{{CLIENT_SECRET}}'); }); it('throws ValidationError for an unsupported language', async () => { await expect( service.generateScaffold({ ...BASE_OPTIONS, language: 'cobol' as never, }), ).rejects.toThrow(ValidationError); }); }); });