/** * reference-data.test.ts — scaffold-seed `referenceData` (§25). * An entity whose API exposes no POST and that nothing seeds is DEFINITIVELY * empty (the 9 business-fixed alert types incident). The generated provider * keeps the IClientSeedDataProvider contract but writes through a * constructor-injected IExtensionsDbContext, per tenant for tenant-scoped * entities (the Create() factory requires a tenantId). */ import { describe, it, expect } from 'vitest' import { generate, seedDiPatches, SEED_PROVIDERS_DI_BEGIN } from '../generate.js' import { validate } from '../validate.js' import { ScaffoldSeedInputSchema } from '../types.js' function baseSpec(referenceData: unknown[] = []): unknown { return { module: 'parametrage', appCode: 'Flotte', applicationCode: 'flotte', projectPath: '/tmp/x', referenceData, } } const ALERT_ROWS = [ { Key: 'expertise', Label: 'Expertise périodique', ThresholdDays: 30, Enabled: true }, { Key: 'vignette', Label: 'Vignette autoroutière', ThresholdDays: 15, Enabled: true }, ] describe('scaffold-seed / referenceData generation', () => { it('emits a dedicated {Module}ReferenceDataSeedDataProvider with IExtensionsDbContext ctor injection', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec([ { entity: 'AlertRule', keyField: 'Key', rows: ALERT_ROWS }, ])) const files = generate(spec) const provider = files.find(f => f.path.endsWith('ParametrageReferenceDataSeedDataProvider.cs'))! expect(provider).toBeDefined() expect(provider.path).toBe( 'src/Flotte.Infrastructure/Persistence/Seeding/Applications/Flotte/Modules/Parametrage/ParametrageReferenceDataSeedDataProvider.cs', ) // Platform contract kept — but the writes go through IExtensionsDbContext. expect(provider.content).toContain(': IClientSeedDataProvider') expect(provider.content).toContain('private readonly IExtensionsDbContext _extensions;') expect(provider.content).toContain('public ParametrageReferenceDataSeedDataProvider(IExtensionsDbContext extensions)') expect(provider.content).toContain('using Flotte.Application.Common.Interfaces;') }) it('tenant-mode rows are seeded PER TENANT with an idempotent upsert on the natural key', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec([ { entity: 'AlertRule', keyField: 'Key', rows: ALERT_ROWS }, ])) const provider = generate(spec).find(f => f.path.includes('ReferenceData'))! expect(provider.content).toContain('var tenantIds = await context.Tenants.Select(t => t.Id).ToListAsync(ct);') expect(provider.content).toContain('foreach (var tenantId in tenantIds)') expect(provider.content).toContain('x.Key == "expertise" && x.TenantId == tenantId') // Named-args factory call, camelized, with the tenant forwarded. expect(provider.content).toContain( 'AlertRule.Create(key: "expertise", label: "Expertise périodique", thresholdDays: 30, enabled: true, tenantId: tenantId)', ) }) it('tenantMode none seeds one global row set (no tenant loop)', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec([ { entity: 'Canton', keyField: 'Code', tenantMode: 'none', rows: [{ Code: 'VD', Label: 'Vaud' }] }, ])) const provider = generate(spec).find(f => f.path.includes('ReferenceData'))! expect(provider.content).not.toContain('foreach (var tenantId') expect(provider.content).not.toContain('context.Tenants') expect(provider.content).toContain('x.Code == "VD"') expect(provider.content).toContain('Canton.Create(code: "VD", label: "Vaud")') }) it('the cs: prefix escapes to a verbatim C# expression (enum members)', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec([ { entity: 'AlertRule', keyField: 'Key', rows: [{ Key: 'assurance', Kind: 'cs:AlertKind.Insurance' }] }, ])) const provider = generate(spec).find(f => f.path.includes('ReferenceData'))! expect(provider.content).toContain('kind: AlertKind.Insurance') expect(provider.content).not.toContain('"cs:') }) it('emits NO reference provider when referenceData is empty (default)', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec()) expect(generate(spec).some(f => f.path.includes('ReferenceData'))).toBe(false) }) }) describe('scaffold-seed / referenceData validation', () => { it('errors when a row misses its natural key', () => { const r = validate(baseSpec([{ entity: 'AlertRule', keyField: 'Key', rows: [{ Label: 'Sans clé' }] }])) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('missing its natural key'))).toBe(true) }) it('errors on duplicate key values (silent-skip trap)', () => { const r = validate(baseSpec([ { entity: 'AlertRule', keyField: 'Key', rows: [{ Key: 'a' }, { Key: 'a' }] }, ])) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('twice'))).toBe(true) }) it('warns when upserting by Code (coded-entity allocation conflict)', () => { const r = validate(baseSpec([ { entity: 'Canton', rows: [{ Code: 'VD' }] }, // keyField defaults to Code ])) expect(r.valid).toBe(true) expect(r.warnings.some(w => w.includes('engine-allocated'))).toBe(true) }) }) describe('scaffold-seed / referenceData — typed literals (review fix)', () => { const gen = (entry: object) => generate(ScaffoldSeedInputSchema.parse(baseSpec([entry]))).find(f => f.path.includes('ReferenceData'))!.content it('a declared enum type turns a plain string into a real member (no cs: dialect needed)', () => { const c = gen({ entity: 'AlertRule', keyField: 'Key', tenantMode: 'none', types: { Kind: 'AlertKind' }, rows: [{ Key: 'assurance', Kind: 'Insurance' }], }) expect(c).toContain('kind: AlertKind.Insurance') }) it('numeric suffixes follow the DECLARED type — a double is never emitted as decimal', () => { const c = gen({ entity: 'Threshold', keyField: 'Key', tenantMode: 'none', types: { Ratio: 'double', Amount: 'decimal', Count: 'int' }, rows: [{ Key: 'k', Ratio: 2.5, Amount: 12.5, Count: 3 }], }) expect(c).toContain('ratio: 2.5d') expect(c).toContain('amount: 12.5m') expect(c).toContain('count: 3') }) it('Guid / DateOnly / DateTime types parse instead of shipping a bare string', () => { const c = gen({ entity: 'Slot', keyField: 'Key', tenantMode: 'none', types: { OwnerId: 'Guid', StartsOn: 'DateOnly', CreatedAt: 'DateTime' }, rows: [{ Key: 'k', OwnerId: '3f2504e0-4f89-11d3-9a0c-0305e82c3301', StartsOn: '2026-01-01', CreatedAt: '2026-01-01T08:00:00Z' }], }) expect(c).toContain('ownerId: Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301")') expect(c).toContain('startsOn: DateOnly.Parse("2026-01-01")') expect(c).toContain('createdAt: DateTime.Parse("2026-01-01T08:00:00Z")') }) it('a declared string type keeps the quoted literal (never mistaken for an enum)', () => { const c = gen({ entity: 'Canton', keyField: 'Code', tenantMode: 'none', types: { Code: 'string', Label: 'string' }, rows: [{ Code: 'VD', Label: 'Vaud' }], }) expect(c).toContain('code: "VD", label: "Vaud"') }) it('validate WARNS on a fractional value with no declared type (the silent decimal guess)', () => { const r = validate(baseSpec([ { entity: 'Threshold', keyField: 'Key', rows: [{ Key: 'k', Ratio: 2.5 }] }, ])) expect(r.valid).toBe(true) expect(r.warnings.some(w => w.includes('Ratio') && w.includes('double/float'))).toBe(true) }) it('the cs: escape still wins for anything the type map cannot express', () => { const c = gen({ entity: 'AlertRule', keyField: 'Key', tenantMode: 'none', types: { Window: 'TimeSpan' }, rows: [{ Key: 'k', Window: 'cs:TimeSpan.FromDays(30)' }], }) expect(c).toContain('window: TimeSpan.FromDays(30)') }) }) describe('scaffold-seed / DI registration is LANDED, not suggested (audit #6)', () => { it('seedDiPatches registers every emitted provider through the shared marker block', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec([ { entity: 'AlertRule', keyField: 'Key', rows: ALERT_ROWS }, ])) const patches = seedDiPatches(spec) const lines = patches.map(p => p.line) expect(lines).toContain( 'services.AddScoped();', ) // The reference-data provider is the one whose absence left the table at 0 // rows while DEV-API-030 reported ok. expect(lines).toContain( 'services.AddScoped();', ) for (const p of patches) { expect(p.begin).toBe(SEED_PROVIDERS_DI_BEGIN) expect(p.hostCandidates[0]).toBe('src/Flotte.Infrastructure/DependencyInjection.cs') } }) it('no referenceData ⇒ no reference-data registration (nothing spurious)', () => { const spec = ScaffoldSeedInputSchema.parse(baseSpec()) expect(seedDiPatches(spec).some(p => p.line.includes('ReferenceData'))).toBe(false) }) })