/** * supplied-code.test.ts — the supplied-on-create seam (coded entities). * * The entité.md line declares « surchargeable à la création » → the CREATE * surface gains an OPTIONAL `string? Code = null`, validated by the socle's * ISuppliedCodeGuard and applied BEFORE SaveChanges (HasCode idempotency — * imports/reprises keep their number). Opt-in STRICT: without the facet the * output is byte-identical to before the feature. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { generate } from '../generate.js' import { validateCodedEntity } from '../validate.js' import type { ScaffoldBusinessInput } from '../types.js' function fixture(overrides: Partial = {}): ScaffoldBusinessInput { return { name: 'Budget', pluralName: 'Budgets', module: 'budgets', appCode: 'TestV2', applicationCode: 'crm', namespace: 'TestV2', fields: [ { name: 'Label', type: 'string', required: true, isKey: false }, { name: 'InitialAmount', type: 'decimal', required: true, isKey: false }, ], businessRules: [], customActions: [], projectPath: '/tmp/project', ...overrides, } } const SUPPLIED = { supplied: true } describe('scaffold-business / supplied-on-create — the create surface', () => { it('CreateDto and CreateCommand gain a TERMINAL `string? Code = null`', () => { const files = generate(fixture({ codedEntity: SUPPLIED })) const dto = files.find((f) => f.path.endsWith('DTOs/BudgetDtos.cs'))! expect(dto.content).toMatch(/record CreateBudgetDto\([\s\S]*?string\? Code = null\n\);/) const cmd = files.find((f) => f.path.endsWith('Commands/CreateBudgetCommand.cs'))! expect(cmd.content).toMatch(/record CreateBudgetCommand\(.*string\? Code = null\) : IRequest;/) // Update NEVER carries Code — supplied or not. const upd = files.find((f) => f.path.endsWith('Commands/UpdateBudgetCommand.cs'))! expect(upd.content).not.toContain('Code') }) it('the service injects ISuppliedCodeGuard and applies the code BEFORE Add/SaveChanges', () => { const svc = generate(fixture({ codedEntity: SUPPLIED })) .find((f) => f.path.endsWith('Services/Crm/Budgets/BudgetService.cs'))! expect(svc.content).toContain('using SmartStack.Application.Common.CodeGeneration;') expect(svc.content).toContain('using SmartStack.Domain.CodeGeneration;') expect(svc.content).toMatch(/private readonly ISuppliedCodeGuard _codeGuard;/) expect(svc.content).toContain( 'entity.ApplyCode(await _codeGuard.EnsureAvailableAsync(((ICodedEntity)entity).CodeKey, command.Code, ct));', ) const applyAt = svc.content.indexOf('entity.ApplyCode') expect(applyAt).toBeGreaterThan(-1) expect(applyAt).toBeLessThan(svc.content.indexOf('.Add(entity)')) // Deliberately NO validator rule on Code — the guard's DomainException is // the contract (duplicating its charset here would drift from the socle). const validator = generate(fixture({ codedEntity: SUPPLIED })) .find((f) => f.path.endsWith('Validators/CreateBudgetCommandValidator.cs'))! expect(validator.content).not.toContain('Code') }) it('OPT-IN STRICT: boolean flag and supplied:false stay byte-identical to the legacy output', () => { const before = generate(fixture({ codedEntity: false })) for (const flag of [true, { supplied: false }, { label: { fr: 'Référence' } }] as const) { const after = generate(fixture({ codedEntity: flag as never })) expect(after.map((f) => f.content)).toEqual(before.map((f) => f.content)) } }) }) describe('scaffold-business / supplied-on-create — socle floor (fail-closed)', () => { let root: string beforeEach(() => { root = mkdtempSync(path.join(tmpdir(), 'ssbizfloor-')) }) afterEach(() => rmSync(root, { recursive: true, force: true })) function writeCsproj(version: string): void { const dir = path.join(root, 'src', 'TestV2.Api') mkdirSync(dir, { recursive: true }) writeFileSync(path.join(dir, 'TestV2.Api.csproj'), ``) } it('errs below 3.66.0 (the generated guard call would not compile — CS0246 is not actionable)', () => { writeCsproj('3.65.0') const r = validateCodedEntity(fixture({ codedEntity: SUPPLIED, projectPath: root })) expect(r.valid).toBe(false) expect(r.errors[0]).toContain('3.66.0') expect(r.errors[0]).toContain('ss upgrade') }) it('passes at/above the floor; an unreadable socle version also fail-closes', () => { writeCsproj('3.66.0') expect(validateCodedEntity(fixture({ codedEntity: SUPPLIED, projectPath: root })).valid).toBe(true) expect(validateCodedEntity(fixture({ codedEntity: SUPPLIED, projectPath: '/nowhere' })).valid).toBe(false) }) it('the floor only arms on supplied — a plain coded entity never reads the csproj', () => { expect(validateCodedEntity(fixture({ codedEntity: true, projectPath: '/nowhere' })).valid).toBe(true) }) })