Validate component and piece requirements

This commit is contained in:
MatthieuTD
2025-09-22 10:20:40 +02:00
parent b6ca9ae54b
commit 3a614bab72
7 changed files with 486 additions and 17 deletions

View File

@@ -1,19 +1,84 @@
import { BadRequestException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ComposantsService } from './composants.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreateComposantDto } from '../shared/dto/composant.dto';
describe('ComposantsService', () => {
let service: ComposantsService;
let prisma: any;
beforeEach(async () => {
prisma = {
composant: {
create: jest.fn(),
findUnique: jest.fn(),
},
machine: {
findUnique: jest.fn(),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [ComposantsService, PrismaService],
providers: [ComposantsService, { provide: PrismaService, useValue: prisma }],
}).compile();
service = module.get<ComposantsService>(ComposantsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
afterEach(() => {
jest.clearAllMocks();
});
it('should create a component when requirement matches the machine skeleton', async () => {
const dto: CreateComposantDto = {
name: 'Comp A',
machineId: 'machine-1',
typeComposantId: 'type-comp-1',
typeMachineComponentRequirementId: 'req-1',
};
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
],
},
});
const created = { id: 'component-1' };
prisma.composant.create.mockResolvedValue(created);
await expect(service.create(dto)).resolves.toEqual(created);
expect(prisma.composant.create).toHaveBeenCalled();
expect(
prisma.composant.create.mock.calls[0][0].data.typeComposantId,
).toBe('type-comp-1');
});
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
const dto: CreateComposantDto = {
name: 'Comp A',
machineId: 'machine-1',
typeComposantId: 'type-comp-1',
typeMachineComponentRequirementId: 'req-2',
};
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
],
},
});
await expect(service.create(dto)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(prisma.composant.create).not.toHaveBeenCalled();
});
});

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
@@ -7,8 +7,75 @@ export class ComposantsService {
constructor(private prisma: PrismaService) {}
async create(createComposantDto: CreateComposantDto) {
const requirementId = createComposantDto.typeMachineComponentRequirementId;
let machineId = createComposantDto.machineId;
if (createComposantDto.parentComposantId) {
const parentMachineId = await this.resolveMachineIdFromComposant(
createComposantDto.parentComposantId,
);
if (machineId && machineId !== parentMachineId) {
throw new BadRequestException(
'Le composant parent ne correspond pas à la machine ciblée.',
);
}
machineId = parentMachineId;
}
if (!machineId) {
throw new BadRequestException(
'Un machineId ou un parentComposantId valide est requis pour créer un composant.',
);
}
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
componentRequirements: true,
},
},
},
});
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
}
const requirement = machine.typeMachine.componentRequirements.find(
(componentRequirement) => componentRequirement.id === requirementId,
);
if (!requirement) {
throw new BadRequestException(
'Le requirement de composant fourni ne correspond pas au squelette de la machine.',
);
}
if (
createComposantDto.typeComposantId &&
createComposantDto.typeComposantId !== requirement.typeComposantId
) {
throw new BadRequestException(
'Le type de composant fourni ne correspond pas au requirement pour cette machine.',
);
}
const data = {
...createComposantDto,
machineId,
typeComposantId:
createComposantDto.typeComposantId ?? requirement.typeComposantId,
};
return this.prisma.composant.create({
data: createComposantDto,
data,
include: {
machine: true,
parentComposant: true,
@@ -437,6 +504,37 @@ export class ComposantsService {
});
}
private async resolveMachineIdFromComposant(
composantId: string,
): Promise<string> {
const composant = await this.prisma.composant.findUnique({
where: { id: composantId },
select: {
id: true,
machineId: true,
parentComposantId: true,
},
});
if (!composant) {
throw new BadRequestException(
'Le composant parent spécifié est introuvable.',
);
}
if (composant.machineId) {
return composant.machineId;
}
if (composant.parentComposantId) {
return this.resolveMachineIdFromComposant(composant.parentComposantId);
}
throw new BadRequestException(
'Impossible de déterminer la machine associée au composant parent.',
);
}
async remove(id: string) {
return this.prisma.composant.delete({
where: { id },

View File

@@ -1,19 +1,85 @@
import { BadRequestException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { PiecesService } from './pieces.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto } from '../shared/dto/piece.dto';
describe('PiecesService', () => {
let service: PiecesService;
let prisma: any;
beforeEach(async () => {
prisma = {
piece: {
create: jest.fn(),
},
machine: {
findUnique: jest.fn(),
},
composant: {
findUnique: jest.fn(),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [PiecesService, PrismaService],
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
}).compile();
service = module.get<PiecesService>(PiecesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
afterEach(() => {
jest.clearAllMocks();
});
it('should create a piece when requirement matches the machine skeleton', async () => {
const dto: CreatePieceDto = {
name: 'Piece A',
machineId: 'machine-1',
typePieceId: 'type-piece-1',
typeMachinePieceRequirementId: 'req-1',
};
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
},
});
const created = { id: 'piece-1' };
prisma.piece.create.mockResolvedValue(created);
await expect(service.create(dto)).resolves.toEqual(created);
expect(prisma.piece.create).toHaveBeenCalled();
expect(prisma.piece.create.mock.calls[0][0].data.machineId).toBe(
'machine-1',
);
});
it('should refuse creation when requirement does not belong to machine skeleton', async () => {
const dto: CreatePieceDto = {
name: 'Piece A',
machineId: 'machine-1',
typePieceId: 'type-piece-1',
typeMachinePieceRequirementId: 'req-2',
};
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
},
});
await expect(service.create(dto)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(prisma.piece.create).not.toHaveBeenCalled();
});
});

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
@@ -7,8 +7,74 @@ export class PiecesService {
constructor(private prisma: PrismaService) {}
async create(createPieceDto: CreatePieceDto) {
const requirementId = createPieceDto.typeMachinePieceRequirementId;
let machineId = createPieceDto.machineId;
if (createPieceDto.composantId) {
const composantMachineId = await this.resolveMachineIdFromComposant(
createPieceDto.composantId,
);
if (machineId && machineId !== composantMachineId) {
throw new BadRequestException(
'Le composant ciblé appartient à une autre machine que celle fournie.',
);
}
machineId = composantMachineId;
}
if (!machineId) {
throw new BadRequestException(
'Un machineId ou un composantId valide est requis pour créer une pièce.',
);
}
const machine = await this.prisma.machine.findUnique({
where: { id: machineId },
include: {
typeMachine: {
include: {
pieceRequirements: true,
},
},
},
});
if (!machine || !machine.typeMachine) {
throw new BadRequestException(
'La machine ciblée doit être associée à un type de machine pour valider les requirements.',
);
}
const requirement = machine.typeMachine.pieceRequirements.find(
(pieceRequirement) => pieceRequirement.id === requirementId,
);
if (!requirement) {
throw new BadRequestException(
'Le requirement de pièce fourni ne correspond pas au squelette de la machine.',
);
}
if (
createPieceDto.typePieceId &&
createPieceDto.typePieceId !== requirement.typePieceId
) {
throw new BadRequestException(
'Le type de pièce fourni ne correspond pas au requirement pour cette machine.',
);
}
const data = {
...createPieceDto,
machineId,
typePieceId: createPieceDto.typePieceId ?? requirement.typePieceId,
};
return this.prisma.piece.create({
data: createPieceDto,
data,
include: {
machine: true,
composant: true,
@@ -101,6 +167,37 @@ export class PiecesService {
});
}
private async resolveMachineIdFromComposant(
composantId: string,
): Promise<string> {
const composant = await this.prisma.composant.findUnique({
where: { id: composantId },
select: {
id: true,
machineId: true,
parentComposantId: true,
},
});
if (!composant) {
throw new BadRequestException(
'Le composant spécifié est introuvable.',
);
}
if (composant.machineId) {
return composant.machineId;
}
if (composant.parentComposantId) {
return this.resolveMachineIdFromComposant(composant.parentComposantId);
}
throw new BadRequestException(
'Impossible de déterminer la machine associée à ce composant.',
);
}
async findByComposant(composantId: string) {
return this.prisma.piece.findMany({
where: { composantId },

View File

@@ -1,15 +1,15 @@
import { IsString, IsOptional, IsNumber } from 'class-validator';
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateComposantDto {
@IsString()
name: string;
@IsOptional()
@ValidateIf((dto) => !dto.parentComposantId)
@IsString()
machineId?: string;
@IsOptional()
@ValidateIf((dto) => !dto.machineId)
@IsString()
parentComposantId?: string;
@@ -34,6 +34,9 @@ export class CreateComposantDto {
@IsString()
typeComposantId?: string;
@IsString()
typeMachineComponentRequirementId: string;
@IsOptional()
@IsString()
composantModelId?: string;

View File

@@ -1,15 +1,15 @@
import { IsString, IsOptional, IsNumber } from 'class-validator';
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreatePieceDto {
@IsString()
name: string;
@IsOptional()
@ValidateIf((dto) => !dto.composantId)
@IsString()
machineId?: string;
@IsOptional()
@ValidateIf((dto) => !dto.machineId)
@IsString()
composantId?: string;
@@ -34,6 +34,9 @@ export class CreatePieceDto {
@IsString()
typePieceId?: string;
@IsString()
typeMachinePieceRequirementId: string;
@IsOptional()
@IsString()
pieceModelId?: string;

View File

@@ -3,23 +3,160 @@ import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
function createMockPrismaService() {
return {
composant: {
create: jest.fn(),
findUnique: jest.fn(),
},
piece: {
create: jest.fn(),
},
machine: {
findUnique: jest.fn(),
},
profile: {
count: jest.fn().mockResolvedValue(0),
create: jest.fn().mockResolvedValue({ id: 'profile-1' }),
},
onModuleInit: jest.fn(),
onModuleDestroy: jest.fn(),
};
}
let prisma: ReturnType<typeof createMockPrismaService>;
beforeEach(async () => {
prisma = createMockPrismaService();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
})
.overrideProvider(PrismaService)
.useValue(prisma)
.compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
describe('POST /composants', () => {
it('accepts creation when requirement matches the machine skeleton', async () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
],
},
});
const created = { id: 'component-1' };
prisma.composant.create.mockResolvedValue(created);
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(prisma.composant.create).toHaveBeenCalled();
});
it('refuses creation when requirement is not part of the machine skeleton', async () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
componentRequirements: [
{ id: 'req-1', typeComposantId: 'type-comp-1' },
],
},
});
await request(app.getHttpServer())
.post('/composants')
.send({
name: 'Comp A',
machineId: 'machine-1',
typeComposantId: 'type-comp-1',
typeMachineComponentRequirementId: 'req-2',
})
.expect(400);
expect(prisma.composant.create).not.toHaveBeenCalled();
});
});
describe('POST /pieces', () => {
it('accepts creation when requirement matches the machine skeleton', async () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
},
});
const created = { id: 'piece-1' };
prisma.piece.create.mockResolvedValue(created);
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(prisma.piece.create).toHaveBeenCalled();
});
it('refuses creation when requirement is not part of the machine skeleton', async () => {
prisma.machine.findUnique.mockResolvedValue({
id: 'machine-1',
typeMachine: {
pieceRequirements: [
{ id: 'req-1', typePieceId: 'type-piece-1' },
],
},
});
await request(app.getHttpServer())
.post('/pieces')
.send({
name: 'Piece A',
machineId: 'machine-1',
typePieceId: 'type-piece-1',
typeMachinePieceRequirementId: 'req-2',
})
.expect(400);
expect(prisma.piece.create).not.toHaveBeenCalled();
});
});
});