/** * uat-plan/validate — structure + connection resolution against a real (temp) project. */ import { describe, it, expect, afterAll } from 'vitest'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { validate } from '../validate.js'; function makeProject(opts: { withConn?: boolean; withWeb?: boolean } = {}): string { const root = mkdtempSync(join(tmpdir(), 'uat-validate-')); const apiDir = join(root, 'src', 'Foo.Api'); mkdirSync(apiDir, { recursive: true }); writeFileSync(join(apiDir, 'Foo.Api.csproj'), ''); if (opts.withConn !== false) { writeFileSync( join(apiDir, 'appsettings.Local.json'), JSON.stringify({ ConnectionStrings: { DefaultConnection: 'Server=(local);Database=TestDb;Integrated Security=true' } }), ); } if (opts.withWeb !== false) { const ext = join(root, 'web', 'foo-web', 'src', 'extensions'); mkdirSync(ext, { recursive: true }); writeFileSync(join(root, 'web', 'foo-web', 'package.json'), JSON.stringify({ name: 'foo-web', dependencies: { react: '^18' } })); writeFileSync(join(ext, 'fooRegistry.ts'), `PageRegistry.register('administration.users', X);`); } return root; } describe('validate', () => { const created: string[] = []; const track = (r: string): string => { created.push(r); return r; }; afterAll(() => { for (const r of created) rmSync(r, { recursive: true, force: true }); }); it('resolves api/web dirs, connection and derived name/outDir', async () => { const root = track(makeProject()); const v = await validate({ projectPath: root, path: 'administration/users', includeApi: true }); expect(v.valid).toBe(true); expect(v.layout?.apiDir.replace(/\\/g, '/')).toMatch(/src\/Foo\.Api$/); expect(v.layout?.webDir.replace(/\\/g, '/')).toMatch(/web\/foo-web$/); expect(v.layout?.name).toBe('users'); expect(v.layout?.application).toBe('administration'); expect(v.layout?.outDir).toBe('.application-test/uat/administration'); expect(v.connection?.database).toBe('TestDb'); expect(v.connectionSource).toBe('appsettings.Local.json'); }); it('honours an explicit name override and connectionString', async () => { const root = track(makeProject({ withConn: false })); const v = await validate({ projectPath: root, path: 'administration/users', name: 'custom', includeApi: true, connectionString: 'Server=x;Database=Override;User Id=sa;Password=p', }); expect(v.valid).toBe(true); expect(v.layout?.name).toBe('custom'); expect(v.connectionSource).toBe('spec.connectionString'); expect(v.connection?.database).toBe('Override'); }); it('warns (not fails) when the web app is absent', async () => { const root = track(makeProject({ withWeb: false })); const v = await validate({ projectPath: root, path: 'administration', includeApi: true }); expect(v.valid).toBe(true); expect(v.layout?.webDir).toBe(''); expect(v.warnings.some((w) => /Web app not located/.test(w))).toBe(true); }); it('fails with a clear error when no connection can be resolved', async () => { const root = track(makeProject({ withConn: false })); const v = await validate({ projectPath: root, path: 'administration', includeApi: true }); expect(v.valid).toBe(false); expect(v.errors.join(' ')).toMatch(/No SQL connection/); }); it('rejects a schema-invalid spec', async () => { const v = await validate({ projectPath: '', path: '' }); expect(v.valid).toBe(false); }); it('fails when the API project is absent', async () => { const root = track(mkdtempSync(join(tmpdir(), 'uat-empty-'))); const v = await validate({ projectPath: root, path: 'administration', includeApi: true }); expect(v.valid).toBe(false); expect(v.errors.join(' ')).toMatch(/API project/); }); });