import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; import { PrismaService } from '../src/prisma/prisma.service'; type Nullable = T | null; type SiteRecord = { id: string; name: string; contactName: string; contactPhone: string; contactAddress: string; contactPostalCode: string; contactCity: string; createdAt: Date; updatedAt: Date; }; type TypeComposantRecord = { id: string; name: string; description: Nullable; createdAt: Date; updatedAt: Date; }; type TypePieceRecord = { id: string; name: string; description: Nullable; createdAt: Date; updatedAt: Date; }; type TypeMachineRecord = { id: string; name: string; description: Nullable; category: Nullable; maintenanceFrequency: Nullable; components: any; machinePieces: any; specifications: any; createdAt: Date; updatedAt: Date; }; type TypeMachineComponentRequirementRecord = { id: string; typeMachineId: string; typeComposantId: string; label: Nullable; minCount: number; maxCount: Nullable; required: boolean; allowNewModels: boolean; }; type TypeMachinePieceRequirementRecord = { id: string; typeMachineId: string; typePieceId: string; label: Nullable; minCount: number; maxCount: Nullable; required: boolean; allowNewModels: boolean; }; type MachineRecord = { id: string; name: string; reference: Nullable; constructeurId: Nullable; prix: Nullable; siteId: string; typeMachineId: Nullable; createdAt: Date; updatedAt: Date; }; type ComposantRecord = { id: string; name: string; reference: Nullable; prix: Nullable; machineId: Nullable; parentComposantId: Nullable; typeComposantId: Nullable; composantModelId: Nullable; typeMachineComponentRequirementId: Nullable; constructeurId: Nullable; createdAt: Date; updatedAt: Date; }; type PieceRecord = { id: string; name: string; reference: Nullable; prix: Nullable; machineId: Nullable; composantId: Nullable; typePieceId: Nullable; pieceModelId: Nullable; typeMachinePieceRequirementId: Nullable; constructeurId: Nullable; createdAt: Date; updatedAt: Date; }; type CustomFieldRecord = { id: string; name: string; type: string; required: boolean; options: string[]; typeMachineId: Nullable; typeComposantId: Nullable; typePieceId: Nullable; createdAt: Date; updatedAt: Date; }; type CustomFieldValueRecord = { id: string; value: string; customFieldId: string; machineId: Nullable; composantId: Nullable; pieceId: Nullable; createdAt: Date; updatedAt: Date; }; type ProfileRecord = { id: string; firstName: string; lastName: string; isActive: boolean; createdAt: Date; updatedAt: Date; }; function generateId(prefix: string): string { return `${prefix}_${Math.random().toString(36).slice(2, 12)}`; } class InMemoryPrismaService { private sites: SiteRecord[] = []; private typeComposants: TypeComposantRecord[] = []; private typePieces: TypePieceRecord[] = []; private modelTypeCodeCounter = 0; private typeMachines: TypeMachineRecord[] = []; private typeMachineComponentRequirements: TypeMachineComponentRequirementRecord[] = []; private typeMachinePieceRequirements: TypeMachinePieceRequirementRecord[] = []; private machines: MachineRecord[] = []; private composants: ComposantRecord[] = []; private pieces: PieceRecord[] = []; private customFields: CustomFieldRecord[] = []; private customFieldValues: CustomFieldValueRecord[] = []; private profiles: ProfileRecord[] = []; async onModuleInit() {} async onModuleDestroy() {} async $connect() {} async $disconnect() {} async $transaction(fn: (tx: this) => Promise): Promise { return fn(this); } reset() { this.sites = []; this.typeComposants = []; this.typePieces = []; this.modelTypeCodeCounter = 0; this.typeMachines = []; this.typeMachineComponentRequirements = []; this.typeMachinePieceRequirements = []; this.machines = []; this.composants = []; this.pieces = []; this.customFields = []; this.customFieldValues = []; this.profiles = []; } site = { create: async ({ data }: any) => { const now = new Date(); const record: SiteRecord = { id: generateId('site'), name: data.name, contactName: data.contactName, contactPhone: data.contactPhone, contactAddress: data.contactAddress, contactPostalCode: data.contactPostalCode, contactCity: data.contactCity, createdAt: now, updatedAt: now, }; this.sites.push(record); return { ...record }; }, findMany: async ({ include }: any = {}) => { return this.sites.map((site) => this.buildSite(site, include)); }, findUnique: async ({ where, include }: any) => { const site = this.sites.find((item) => item.id === where.id); if (!site) { return null; } return this.buildSite(site, include); }, update: async ({ where, data }: any) => { const site = this.sites.find((item) => item.id === where.id); if (!site) { throw new Error('Site not found'); } Object.assign(site, data, { updatedAt: new Date() }); return { ...site }; }, delete: async ({ where }: any) => { const index = this.sites.findIndex((item) => item.id === where.id); if (index === -1) { throw new Error('Site not found'); } const [deleted] = this.sites.splice(index, 1); return { ...deleted }; }, }; typeComposant = { create: async ({ data, include }: any) => { const now = new Date(); const record: TypeComposantRecord = { id: generateId('type_comp'), name: data.name, description: data.description ?? null, createdAt: now, updatedAt: now, }; this.typeComposants.push(record); if (data.customFields?.create) { for (const field of data.customFields.create) { this.customFields.push({ id: generateId('cf'), name: field.name, type: field.type, required: field.required ?? false, options: field.options ?? [], typeMachineId: null, typeComposantId: record.id, typePieceId: null, createdAt: now, updatedAt: now, }); } } return include?.customFields ? { ...record, customFields: this.customFields.filter( (field) => field.typeComposantId === record.id, ), } : { ...record }; }, findFirst: async ({ where }: any) => { if (!where) { return this.typeComposants[0] ?? null; } if (where.id) { return this.typeComposants.find((item) => item.id === where.id) ?? null; } if (where.name) { return ( this.typeComposants.find((item) => item.name === where.name) ?? null ); } return null; }, findMany: async () => { return this.typeComposants.map((item) => ({ ...item })); }, findUnique: async ({ where }: any) => { return this.typeComposants.find((item) => item.id === where.id) ?? null; }, }; typePiece = { create: async ({ data, include }: any) => { const now = new Date(); const record: TypePieceRecord = { id: generateId('type_piece'), name: data.name, description: data.description ?? null, createdAt: now, updatedAt: now, }; this.typePieces.push(record); if (data.customFields?.create) { for (const field of data.customFields.create) { this.customFields.push({ id: generateId('cf'), name: field.name, type: field.type, required: field.required ?? false, options: field.options ?? [], typeMachineId: null, typeComposantId: null, typePieceId: record.id, createdAt: now, updatedAt: now, }); } } return include?.customFields ? { ...record, customFields: this.customFields.filter( (field) => field.typePieceId === record.id, ), } : { ...record }; }, findFirst: async ({ where }: any) => { if (!where) { return this.typePieces[0] ?? null; } if (where.id) { return this.typePieces.find((item) => item.id === where.id) ?? null; } if (where.name) { return this.typePieces.find((item) => item.name === where.name) ?? null; } return null; }, findUnique: async ({ where }: any) => { return this.typePieces.find((item) => item.id === where.id) ?? null; }, }; typeMachine = { create: async ({ data, include }: any) => { const now = new Date(); const record: TypeMachineRecord = { id: generateId('type_machine'), name: data.name, description: data.description ?? null, category: data.category ?? null, maintenanceFrequency: data.maintenanceFrequency ?? null, components: data.components ?? null, machinePieces: data.machinePieces ?? null, specifications: data.specifications ?? null, createdAt: now, updatedAt: now, }; this.typeMachines.push(record); if (data.customFields?.create) { for (const field of data.customFields.create) { this.customFields.push({ id: generateId('cf'), name: field.name, type: field.type, required: field.required ?? false, options: field.options ?? [], typeMachineId: record.id, typeComposantId: null, typePieceId: null, createdAt: now, updatedAt: now, }); } } let componentRequirements: TypeMachineComponentRequirementRecord[] = []; if (data.componentRequirements?.create) { componentRequirements = data.componentRequirements.create.map( (requirement) => { const req: TypeMachineComponentRequirementRecord = { id: generateId('tmc_req'), typeMachineId: record.id, typeComposantId: requirement.typeComposant.connect.id, label: requirement.label ?? null, minCount: requirement.minCount ?? 1, maxCount: requirement.maxCount ?? null, required: requirement.required ?? true, allowNewModels: requirement.allowNewModels ?? true, }; this.typeMachineComponentRequirements.push(req); return req; }, ); } let pieceRequirements: TypeMachinePieceRequirementRecord[] = []; if (data.pieceRequirements?.create) { pieceRequirements = data.pieceRequirements.create.map((requirement) => { const req: TypeMachinePieceRequirementRecord = { id: generateId('tmp_req'), typeMachineId: record.id, typePieceId: requirement.typePiece.connect.id, label: requirement.label ?? null, minCount: requirement.minCount ?? 0, maxCount: requirement.maxCount ?? null, required: requirement.required ?? false, allowNewModels: requirement.allowNewModels ?? true, }; this.typeMachinePieceRequirements.push(req); return req; }); } return this.buildTypeMachine( record, include ?? {}, componentRequirements, pieceRequirements, ); }, findUnique: async ({ where, include }: any) => { const typeMachine = this.typeMachines.find( (item) => item.id === where.id, ); if (!typeMachine) { return null; } const componentRequirements = this.typeMachineComponentRequirements.filter( (item) => item.typeMachineId === typeMachine.id, ); const pieceRequirements = this.typeMachinePieceRequirements.filter( (item) => item.typeMachineId === typeMachine.id, ); return this.buildTypeMachine( typeMachine, include ?? {}, componentRequirements, pieceRequirements, ); }, findMany: async ({ include }: any = {}) => { return this.typeMachines.map((item) => { const componentRequirements = this.typeMachineComponentRequirements.filter( (req) => req.typeMachineId === item.id, ); const pieceRequirements = this.typeMachinePieceRequirements.filter( (req) => req.typeMachineId === item.id, ); return this.buildTypeMachine( item, include ?? {}, componentRequirements, pieceRequirements, ); }); }, }; machine = { create: async ({ data, include }: any) => { const now = new Date(); const record: MachineRecord = { id: generateId('machine'), name: data.name, reference: data.reference ?? null, constructeurId: data.constructeurId ?? null, prix: data.prix ?? null, siteId: data.siteId, typeMachineId: data.typeMachineId ?? null, createdAt: now, updatedAt: now, }; this.machines.push(record); return this.buildMachine(record, include ?? {}); }, findUnique: async ({ where, include }: any) => { const machine = this.machines.find((item) => item.id === where.id); if (!machine) { return null; } return this.buildMachine(machine, include ?? {}); }, findMany: async ({ include, where }: any = {}) => { let machines = this.machines; if (where?.siteId) { machines = machines.filter( (machine) => machine.siteId === where.siteId, ); } return machines.map((machine) => this.buildMachine(machine, include ?? {}), ); }, delete: async ({ where }: any) => { const index = this.machines.findIndex((item) => item.id === where.id); if (index === -1) { throw new Error('Machine not found'); } const [deleted] = this.machines.splice(index, 1); return { ...deleted }; }, }; composant = { create: async ({ data }: any) => { const now = new Date(); const record: ComposantRecord = { id: generateId('component'), name: data.name, reference: data.reference ?? null, prix: data.prix ?? null, machineId: data.machineId ?? null, parentComposantId: data.parentComposantId ?? null, typeComposantId: data.typeComposantId ?? null, composantModelId: data.composantModelId ?? null, typeMachineComponentRequirementId: data.typeMachineComponentRequirementId ?? null, constructeurId: data.constructeurId ?? null, createdAt: now, updatedAt: now, }; this.composants.push(record); return { ...record }; }, findMany: async ({ where }: any) => { let composants = this.composants; if (where?.machineId !== undefined) { composants = composants.filter( (item) => item.machineId === where.machineId, ); } if (where?.parentComposantId !== undefined) { composants = composants.filter( (item) => item.parentComposantId === where.parentComposantId, ); } return composants.map((item) => ({ ...item })); }, }; piece = { create: async ({ data }: any) => { const now = new Date(); const record: PieceRecord = { id: generateId('piece'), name: data.name, reference: data.reference ?? null, prix: data.prix ?? null, machineId: data.machineId ?? null, composantId: data.composantId ?? null, typePieceId: data.typePieceId ?? null, pieceModelId: data.pieceModelId ?? null, typeMachinePieceRequirementId: data.typeMachinePieceRequirementId ?? null, constructeurId: data.constructeurId ?? null, createdAt: now, updatedAt: now, }; this.pieces.push(record); return { ...record }; }, findMany: async ({ where }: any) => { let pieces = this.pieces; if (where?.machineId !== undefined) { pieces = pieces.filter((item) => item.machineId === where.machineId); } if (where?.composantId !== undefined) { pieces = pieces.filter( (item) => item.composantId === where.composantId, ); } return pieces.map((item) => ({ ...item })); }, }; customField = { create: async ({ data }: any) => { const now = new Date(); const record: CustomFieldRecord = { id: generateId('cf'), name: data.name, type: data.type, required: data.required ?? false, options: data.options ?? [], typeMachineId: data.typeMachineId ?? null, typeComposantId: data.typeComposantId ?? null, typePieceId: data.typePieceId ?? null, createdAt: now, updatedAt: now, }; this.customFields.push(record); return { ...record }; }, findMany: async ({ where }: any) => { return this.customFields .filter((field) => { if (!where) return true; return Object.entries(where).every( ([key, value]) => (field as any)[key] === value, ); }) .map((field) => ({ ...field })); }, deleteMany: async ({ where }: any) => { const before = this.customFields.length; this.customFields = this.customFields.filter((field) => { return !Object.entries(where).every( ([key, value]) => (field as any)[key] === value, ); }); return { count: before - this.customFields.length }; }, }; customFieldValue = { create: async ({ data, include }: any) => { const now = new Date(); const record: CustomFieldValueRecord = { id: generateId('cfv'), value: data.value, customFieldId: data.customFieldId, machineId: data.machineId ?? null, composantId: data.composantId ?? null, pieceId: data.pieceId ?? null, createdAt: now, updatedAt: now, }; this.customFieldValues.push(record); return this.buildCustomFieldValue(record, include ?? {}); }, findMany: async ({ where, include }: any) => { const records = this.customFieldValues.filter((value) => { if (!where) return true; return Object.entries(where).every( ([key, v]) => (value as any)[key] === v, ); }); return records.map((record) => this.buildCustomFieldValue(record, include ?? {}), ); }, findUnique: async ({ where, include }: any) => { const record = this.customFieldValues.find( (value) => value.id === where.id, ); if (!record) { return null; } return this.buildCustomFieldValue(record, include ?? {}); }, findFirst: async ({ where }: any) => { return ( this.customFieldValues.find((value) => Object.entries(where).every(([key, v]) => (value as any)[key] === v), ) ?? null ); }, update: async ({ where, data, include }: any) => { const record = this.customFieldValues.find( (value) => value.id === where.id, ); if (!record) { throw new Error('Custom field value not found'); } if (data.value !== undefined) { record.value = data.value; } record.updatedAt = new Date(); return this.buildCustomFieldValue(record, include ?? {}); }, delete: async ({ where }: any) => { const index = this.customFieldValues.findIndex( (value) => value.id === where.id, ); if (index === -1) { throw new Error('Custom field value not found'); } const [deleted] = this.customFieldValues.splice(index, 1); return { ...deleted }; }, }; profile = { count: async ({ where }: any) => { return this.profiles.filter((profile) => { if (!where) return true; return Object.entries(where).every( ([key, value]) => (profile as any)[key] === value, ); }).length; }, findMany: async ({ where, orderBy, select }: any) => { let profiles = this.profiles; if (where) { profiles = profiles.filter((profile) => Object.entries(where).every( ([key, value]) => (profile as any)[key] === value, ), ); } if (orderBy?.createdAt === 'asc') { profiles = [...profiles].sort( (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), ); } return profiles.map((profile) => this.applySelect(profile, select)); }, findFirst: async ({ where, select }: any) => { const profile = this.profiles.find((item) => Object.entries(where).every( ([key, value]) => (item as any)[key] === value, ), ); return profile ? this.applySelect(profile, select) : null; }, findUnique: async ({ where, select }: any) => { const profile = this.profiles.find((item) => item.id === where.id); return profile ? this.applySelect(profile, select) : null; }, create: async ({ data, select }: any) => { const now = new Date(); const record: ProfileRecord = { id: generateId('profile'), firstName: data.firstName, lastName: data.lastName, isActive: data.isActive ?? true, createdAt: now, updatedAt: now, }; this.profiles.push(record); return this.applySelect(record, select); }, update: async ({ where, data, select }: any) => { const profile = this.profiles.find((item) => item.id === where.id); if (!profile) { throw new Error('Profile not found'); } Object.assign(profile, data, { updatedAt: new Date() }); return this.applySelect(profile, select); }, }; private applySelect>(record: T, select?: any) { if (!select) { return { ...record }; } const result: any = {}; for (const [key, enabled] of Object.entries(select)) { if (enabled) { result[key] = record[key as keyof T]; } } return result; } private buildSite(site: SiteRecord, include: any) { const base: any = { ...site }; if (include?.machines) { const machines = this.machines.filter( (machine) => machine.siteId === site.id, ); base.machines = machines.map((machine) => this.buildMachine(machine, include.machines.include ?? {}), ); } if (include?.documents) { base.documents = []; } return base; } private buildTypeMachine( record: TypeMachineRecord, include: any, componentRequirements: TypeMachineComponentRequirementRecord[], pieceRequirements: TypeMachinePieceRequirementRecord[], ) { const base: any = { ...record }; if (include?.customFields) { base.customFields = this.customFields.filter( (field) => field.typeMachineId === record.id, ); } if (include?.componentRequirements) { base.componentRequirements = componentRequirements.map((requirement) => ({ ...requirement, typeComposant: include.componentRequirements.include?.typeComposant ? (this.typeComposants.find( (item) => item.id === requirement.typeComposantId, ) ?? null) : undefined, })); } if (include?.pieceRequirements) { base.pieceRequirements = pieceRequirements.map((requirement) => ({ ...requirement, typePiece: include.pieceRequirements.include?.typePiece ? (this.typePieces.find( (item) => item.id === requirement.typePieceId, ) ?? null) : undefined, })); } if (include?.machines) { base.machines = this.machines .filter((machine) => machine.typeMachineId === record.id) .map((machine) => this.buildMachine(machine, include.machines.include ?? {}), ); } return base; } private buildMachine(machine: MachineRecord, include: any) { const base: any = { ...machine }; if (include?.site) { base.site = this.sites.find((site) => site.id === machine.siteId) ?? null; } if (include?.typeMachine) { const typeMachine = machine.typeMachineId ? this.typeMachines.find((item) => item.id === machine.typeMachineId) : null; if (typeMachine) { const componentRequirements = this.typeMachineComponentRequirements.filter( (req) => req.typeMachineId === typeMachine.id, ); const pieceRequirements = this.typeMachinePieceRequirements.filter( (req) => req.typeMachineId === typeMachine.id, ); base.typeMachine = this.buildTypeMachine( typeMachine, include.typeMachine.include ?? {}, componentRequirements, pieceRequirements, ); } else { base.typeMachine = null; } } if (include?.constructeur) { base.constructeur = null; } if (include?.composants) { const composants = this.composants.filter( (component) => component.machineId === machine.id, ); base.composants = composants .filter((component) => component.parentComposantId === null) .map((component) => this.buildComponent(component, include.composants.include ?? {}), ); } if (include?.pieces) { const machinePieces = this.pieces.filter( (piece) => piece.machineId === machine.id && piece.composantId === null, ); base.pieces = machinePieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {}), ); } if (include?.customFieldValues) { const values = this.customFieldValues.filter( (value) => value.machineId === machine.id, ); base.customFieldValues = values.map((value) => this.buildCustomFieldValue( value, include.customFieldValues.include ?? {}, ), ); } if (include?.documents) { base.documents = []; } return base; } private buildComponent(component: ComposantRecord, include: any) { const base: any = { ...component }; if (include?.typeComposant) { base.typeComposant = component.typeComposantId ? (this.typeComposants.find( (item) => item.id === component.typeComposantId, ) ?? null) : null; } if (include?.composantModel) { base.composantModel = null; } if (include?.typeMachineComponentRequirement) { const requirement = component.typeMachineComponentRequirementId ? (this.typeMachineComponentRequirements.find( (item) => item.id === component.typeMachineComponentRequirementId, ) ?? null) : null; base.typeMachineComponentRequirement = requirement ? { ...requirement, typeComposant: include.typeMachineComponentRequirement.include ?.typeComposant ? (this.typeComposants.find( (item) => item.id === requirement.typeComposantId, ) ?? null) : undefined, } : null; } if (include?.sousComposants) { const children = this.composants.filter( (item) => item.parentComposantId === component.id, ); base.sousComposants = children.map((child) => this.buildComponent(child, include.sousComposants.include ?? {}), ); } if (include?.customFieldValues) { const values = this.customFieldValues.filter( (value) => value.composantId === component.id, ); base.customFieldValues = values.map((value) => this.buildCustomFieldValue( value, include.customFieldValues.include ?? {}, ), ); } if (include?.constructeur) { base.constructeur = null; } if (include?.pieces) { const relatedPieces = this.pieces.filter( (piece) => piece.composantId === component.id, ); base.pieces = relatedPieces.map((piece) => this.buildPiece(piece, include.pieces.include ?? {}), ); } return base; } private buildPiece(piece: PieceRecord, include: any) { const base: any = { ...piece }; if (include?.customFieldValues) { const values = this.customFieldValues.filter( (value) => value.pieceId === piece.id, ); base.customFieldValues = values.map((value) => this.buildCustomFieldValue( value, include.customFieldValues.include ?? {}, ), ); } if (include?.constructeur) { base.constructeur = null; } if (include?.pieceModel) { base.pieceModel = null; } if (include?.typeMachinePieceRequirement) { const requirement = piece.typeMachinePieceRequirementId ? (this.typeMachinePieceRequirements.find( (item) => item.id === piece.typeMachinePieceRequirementId, ) ?? null) : null; base.typeMachinePieceRequirement = requirement ? { ...requirement, typePiece: include.typeMachinePieceRequirement.include?.typePiece ? (this.typePieces.find( (item) => item.id === requirement.typePieceId, ) ?? null) : undefined, } : null; } if (include?.typePiece) { base.typePiece = piece.typePieceId ? (this.typePieces.find((item) => item.id === piece.typePieceId) ?? null) : null; } return base; } private buildCustomFieldValue(record: CustomFieldValueRecord, include: any) { const base: any = { ...record }; if (include?.customField) { base.customField = this.customFields.find((field) => field.id === record.customFieldId) ?? null; } return base; } } describe('Inventory flow (e2e)', () => { let app: INestApplication; let prismaStub: InMemoryPrismaService; let prisma: InMemoryPrismaService; beforeAll(async () => { prismaStub = new InMemoryPrismaService(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(PrismaService) .useValue(prismaStub) .compile(); app = moduleFixture.createNestApplication(); await app.init(); prisma = prismaStub; }); afterAll(async () => { await app.close(); }); beforeEach(() => { prisma.reset(); }); afterEach(() => { jest.restoreAllMocks(); }); it('should create a type, create a machine with new component/piece selections, and edit technical fields', async () => { const siteResponse = await request(app.getHttpServer()) .post('/sites') .send({ name: 'Site Principal', contactName: 'Responsable Maintenance', contactPhone: '+33 1 23 45 67 89', contactAddress: '1 rue de la Paix', contactPostalCode: '75000', contactCity: 'Paris', }); expect(siteResponse.status).toBe(201); const siteId = siteResponse.body.id; expect(siteId).toBeDefined(); const typeComposantResponse = await request(app.getHttpServer()) .post('/types/composants') .send({ name: 'Bloc moteur', description: 'Sous-ensemble principal', customFields: [ { name: 'Puissance nominale', type: 'text', required: true, }, ], }); expect(typeComposantResponse.status).toBe(201); const typeComposantId = typeComposantResponse.body.id; expect(typeComposantId).toBeDefined(); const typePieceResponse = await request(app.getHttpServer()) .post('/types/pieces') .send({ name: 'Kit maintenance', description: 'Kit standard', customFields: [ { name: 'Référence fournisseur', type: 'text', }, ], }); expect(typePieceResponse.status).toBe(201); const typePieceId = typePieceResponse.body.id; expect(typePieceId).toBeDefined(); const typeMachineResponse = await request(app.getHttpServer()) .post('/types/machines') .send({ name: 'Presse hydraulique', description: 'Presse pour la ligne A', componentRequirements: [ { typeComposantId, label: 'Bloc moteur obligatoire', minCount: 1, required: true, allowNewModels: true, }, ], pieceRequirements: [ { typePieceId, label: 'Kit de maintenance', minCount: 1, required: true, allowNewModels: true, }, ], }); expect(typeMachineResponse.status).toBe(201); const typeMachine = typeMachineResponse.body; const componentRequirementId = typeMachine.componentRequirements[0].id; const pieceRequirementId = typeMachine.pieceRequirements[0].id; const machineResponse = await request(app.getHttpServer()) .post('/machines') .send({ name: 'Presse HP-2000', siteId, typeMachineId: typeMachine.id, componentSelections: [ { requirementId: componentRequirementId, definition: { name: 'Bloc moteur série X', reference: 'COMP-001', customFields: [ { name: 'Puissance nominale', type: 'text', required: true, value: '7 kW', }, ], }, }, ], pieceSelections: [ { requirementId: pieceRequirementId, definition: { name: 'Kit maintenance niveau 1', reference: 'KIT-001', customFields: [ { name: 'Référence fournisseur', type: 'text', value: 'STD-002', }, ], }, }, ], }); expect(machineResponse.status).toBe(201); const machineId = machineResponse.body.id; const machineDetailsResponse = await request(app.getHttpServer()).get( `/machines/${machineId}`, ); expect(machineDetailsResponse.status).toBe(200); const machine = machineDetailsResponse.body; expect(machine.composants).toHaveLength(1); expect(machine.pieces).toHaveLength(1); const component = machine.composants[0]; expect(component.name).toBe('Bloc moteur série X'); expect(component.customFieldValues[0].value).toBe('7 kW'); const piece = machine.pieces[0]; expect(piece.name).toBe('Kit maintenance niveau 1'); expect(piece.customFieldValues[0].value).toBe('STD-002'); const customFieldValueId = component.customFieldValues[0].id; const updateResponse = await request(app.getHttpServer()) .patch(`/custom-fields/values/${customFieldValueId}`) .send({ value: '8 kW' }); expect(updateResponse.status).toBe(200); expect(updateResponse.body.value).toBe('8 kW'); const refreshedMachineResponse = await request(app.getHttpServer()).get( `/machines/${machine.id}`, ); expect(refreshedMachineResponse.status).toBe(200); const refreshedComponent = refreshedMachineResponse.body.composants[0]; expect(refreshedComponent.customFieldValues[0].value).toBe('8 kW'); }); describe('POST /composants', () => { it('accepts creation when requirement matches the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { componentRequirements: [ { id: 'req-1', typeComposantId: 'type-comp-1' }, ], }, } as any); const created = { id: 'component-1' }; const createSpy = jest .spyOn(prisma.composant, 'create') .mockResolvedValue(created as any); const response = await request(app.getHttpServer()) .post('/composants') .send({ name: 'Comp A', machineId: 'machine-1', typeComposantId: 'type-comp-1', typeMachineComponentRequirementId: 'req-1', }) .expect(201); expect(response.body).toEqual(created); expect(createSpy).toHaveBeenCalled(); }); it('refuses creation when requirement is not part of the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { componentRequirements: [ { id: 'req-1', typeComposantId: 'type-comp-1' }, ], }, } as any); const createSpy = jest.spyOn(prisma.composant, 'create'); await request(app.getHttpServer()) .post('/composants') .send({ name: 'Comp A', machineId: 'machine-1', typeComposantId: 'type-comp-1', typeMachineComponentRequirementId: 'req-2', }) .expect(400); expect(createSpy).not.toHaveBeenCalled(); }); }); describe('POST /pieces', () => { it('accepts creation when requirement matches the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], }, } as any); const created = { id: 'piece-1' }; const createSpy = jest .spyOn(prisma.piece, 'create') .mockResolvedValue(created as any); const response = await request(app.getHttpServer()) .post('/pieces') .send({ name: 'Piece A', machineId: 'machine-1', typePieceId: 'type-piece-1', typeMachinePieceRequirementId: 'req-1', }) .expect(201); expect(response.body).toEqual(created); expect(createSpy).toHaveBeenCalled(); }); it('refuses creation when requirement is not part of the machine skeleton', async () => { jest.spyOn(prisma.machine, 'findUnique').mockResolvedValue({ id: 'machine-1', typeMachine: { pieceRequirements: [{ id: 'req-1', typePieceId: 'type-piece-1' }], }, } as any); const createSpy = jest.spyOn(prisma.piece, 'create'); await request(app.getHttpServer()) .post('/pieces') .send({ name: 'Piece A', machineId: 'machine-1', typePieceId: 'type-piece-1', typeMachinePieceRequirementId: 'req-2', }) .expect(400); expect(createSpy).not.toHaveBeenCalled(); }); }); });