Merge pull request #2 from MatthieuTD/codex/exiger-machineid-ou-composantid-dans-dtos
Validate component and piece creation against machine requirements
This commit is contained in:
@@ -1,19 +1,84 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ComposantsService } from './composants.service';
|
import { ComposantsService } from './composants.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { CreateComposantDto } from '../shared/dto/composant.dto';
|
||||||
|
|
||||||
describe('ComposantsService', () => {
|
describe('ComposantsService', () => {
|
||||||
let service: ComposantsService;
|
let service: ComposantsService;
|
||||||
|
let prisma: any;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
prisma = {
|
||||||
|
composant: {
|
||||||
|
create: jest.fn(),
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
},
|
||||||
|
machine: {
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [ComposantsService, PrismaService],
|
providers: [ComposantsService, { provide: PrismaService, useValue: prisma }],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<ComposantsService>(ComposantsService);
|
service = module.get<ComposantsService>(ComposantsService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
afterEach(() => {
|
||||||
expect(service).toBeDefined();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
|
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
|
||||||
|
|
||||||
@@ -7,8 +7,75 @@ export class ComposantsService {
|
|||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
async create(createComposantDto: CreateComposantDto) {
|
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({
|
return this.prisma.composant.create({
|
||||||
data: createComposantDto,
|
data,
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
parentComposant: 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) {
|
async remove(id: string) {
|
||||||
return this.prisma.composant.delete({
|
return this.prisma.composant.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -1,19 +1,85 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { PiecesService } from './pieces.service';
|
import { PiecesService } from './pieces.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { CreatePieceDto } from '../shared/dto/piece.dto';
|
||||||
|
|
||||||
describe('PiecesService', () => {
|
describe('PiecesService', () => {
|
||||||
let service: PiecesService;
|
let service: PiecesService;
|
||||||
|
let prisma: any;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
prisma = {
|
||||||
|
piece: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
machine: {
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
},
|
||||||
|
composant: {
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [PiecesService, PrismaService],
|
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<PiecesService>(PiecesService);
|
service = module.get<PiecesService>(PiecesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
afterEach(() => {
|
||||||
expect(service).toBeDefined();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||||
|
|
||||||
@@ -7,8 +7,74 @@ export class PiecesService {
|
|||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
async create(createPieceDto: CreatePieceDto) {
|
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({
|
return this.prisma.piece.create({
|
||||||
data: createPieceDto,
|
data,
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
composant: 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) {
|
async findByComposant(composantId: string) {
|
||||||
return this.prisma.piece.findMany({
|
return this.prisma.piece.findMany({
|
||||||
where: { composantId },
|
where: { composantId },
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class CreateComposantDto {
|
export class CreateComposantDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf((dto) => !dto.parentComposantId)
|
||||||
@IsString()
|
@IsString()
|
||||||
machineId?: string;
|
machineId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf((dto) => !dto.machineId)
|
||||||
@IsString()
|
@IsString()
|
||||||
parentComposantId?: string;
|
parentComposantId?: string;
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export class CreateComposantDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
typeComposantId?: string;
|
typeComposantId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
typeMachineComponentRequirementId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
composantModelId?: string;
|
composantModelId?: string;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
import { IsString, IsOptional, IsNumber, ValidateIf } from 'class-validator';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class CreatePieceDto {
|
export class CreatePieceDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf((dto) => !dto.composantId)
|
||||||
@IsString()
|
@IsString()
|
||||||
machineId?: string;
|
machineId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf((dto) => !dto.machineId)
|
||||||
@IsString()
|
@IsString()
|
||||||
composantId?: string;
|
composantId?: string;
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export class CreatePieceDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
typePieceId?: string;
|
typePieceId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
typeMachinePieceRequirementId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
pieceModelId?: string;
|
pieceModelId?: string;
|
||||||
|
|||||||
@@ -9,10 +9,40 @@ describe('AppController (e2e)', () => {
|
|||||||
let app: INestApplication<App>;
|
let app: INestApplication<App>;
|
||||||
let prisma: PrismaService;
|
let prisma: PrismaService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
|
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({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideProvider(PrismaService)
|
||||||
|
.useValue(prisma)
|
||||||
|
.compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
@@ -30,6 +60,10 @@ describe('AppController (e2e)', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
it('/ (GET)', () => {
|
it('/ (GET)', () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get('/')
|
.get('/')
|
||||||
@@ -37,255 +71,108 @@ describe('AppController (e2e)', () => {
|
|||||||
.expect('Hello World!');
|
.expect('Hello World!');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Machines skeleton reconfiguration', () => {
|
describe('POST /composants', () => {
|
||||||
it('reconfigures machine skeleton according to updated requirements', async () => {
|
it('accepts creation when requirement matches the machine skeleton', async () => {
|
||||||
const site = await prisma.site.create({
|
prisma.machine.findUnique.mockResolvedValue({
|
||||||
data: {
|
id: 'machine-1',
|
||||||
name: 'Site principal',
|
typeMachine: {
|
||||||
contactName: 'Jane Doe',
|
componentRequirements: [
|
||||||
contactPhone: '0102030405',
|
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||||
contactAddress: '1 rue Principale',
|
],
|
||||||
contactPostalCode: '75000',
|
|
||||||
contactCity: 'Paris',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeComposant = await prisma.typeComposant.create({
|
const created = { id: 'component-1' };
|
||||||
data: {
|
prisma.composant.create.mockResolvedValue(created);
|
||||||
name: 'Module',
|
|
||||||
description: 'Module principal',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.customField.create({
|
const response = await request(app.getHttpServer())
|
||||||
data: {
|
.post('/composants')
|
||||||
name: 'Serial',
|
|
||||||
type: 'text',
|
|
||||||
required: false,
|
|
||||||
options: [],
|
|
||||||
typeComposantId: typeComposant.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const typePiece = await prisma.typePiece.create({
|
|
||||||
data: {
|
|
||||||
name: 'Pièce de rechange',
|
|
||||||
description: 'Pièce critique',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.customField.create({
|
|
||||||
data: {
|
|
||||||
name: 'Batch',
|
|
||||||
type: 'text',
|
|
||||||
required: false,
|
|
||||||
options: [],
|
|
||||||
typePieceId: typePiece.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const componentModelV1 = await prisma.composantModel.create({
|
|
||||||
data: {
|
|
||||||
name: 'Module V1',
|
|
||||||
typeComposantId: typeComposant.id,
|
|
||||||
structure: {
|
|
||||||
name: 'Module V1',
|
|
||||||
customFields: [{ name: 'Serial', defaultValue: 'SERIAL-V1' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const componentModelV2 = await prisma.composantModel.create({
|
|
||||||
data: {
|
|
||||||
name: 'Module V2',
|
|
||||||
typeComposantId: typeComposant.id,
|
|
||||||
structure: {
|
|
||||||
name: 'Module V2',
|
|
||||||
customFields: [{ name: 'Serial', defaultValue: 'SERIAL-V2' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pieceModelV1 = await prisma.pieceModel.create({
|
|
||||||
data: {
|
|
||||||
name: 'Pièce V1',
|
|
||||||
typePieceId: typePiece.id,
|
|
||||||
structure: {
|
|
||||||
name: 'Pièce V1',
|
|
||||||
customFields: [{ name: 'Batch', defaultValue: 'BATCH-V1' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pieceModelV2 = await prisma.pieceModel.create({
|
|
||||||
data: {
|
|
||||||
name: 'Pièce V2',
|
|
||||||
typePieceId: typePiece.id,
|
|
||||||
structure: {
|
|
||||||
name: 'Pièce V2',
|
|
||||||
customFields: [{ name: 'Batch', defaultValue: 'BATCH-V2' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeMachine = await prisma.typeMachine.create({
|
|
||||||
data: {
|
|
||||||
name: 'Type Machine A',
|
|
||||||
componentRequirements: {
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
label: 'Module initial',
|
|
||||||
minCount: 1,
|
|
||||||
maxCount: 1,
|
|
||||||
required: true,
|
|
||||||
allowNewModels: false,
|
|
||||||
typeComposantId: typeComposant.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
pieceRequirements: {
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
label: 'Pièce initiale',
|
|
||||||
minCount: 1,
|
|
||||||
maxCount: 1,
|
|
||||||
required: true,
|
|
||||||
allowNewModels: false,
|
|
||||||
typePieceId: typePiece.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
componentRequirements: true,
|
|
||||||
pieceRequirements: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialComponentRequirement = typeMachine.componentRequirements[0];
|
|
||||||
const initialPieceRequirement = typeMachine.pieceRequirements[0];
|
|
||||||
|
|
||||||
const createResponse = await request(app.getHttpServer())
|
|
||||||
.post('/machines')
|
|
||||||
.send({
|
.send({
|
||||||
name: 'Machine Alpha',
|
name: 'Comp A',
|
||||||
siteId: site.id,
|
machineId: 'machine-1',
|
||||||
typeMachineId: typeMachine.id,
|
typeComposantId: 'type-comp-1',
|
||||||
componentSelections: [
|
typeMachineComponentRequirementId: 'req-1',
|
||||||
{
|
|
||||||
requirementId: initialComponentRequirement.id,
|
|
||||||
componentModelId: componentModelV1.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pieceSelections: [
|
|
||||||
{
|
|
||||||
requirementId: initialPieceRequirement.id,
|
|
||||||
pieceModelId: pieceModelV1.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
const machineId = createResponse.body.id as string;
|
expect(response.body).toEqual(created);
|
||||||
|
expect(prisma.composant.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
const initialComponents = await prisma.composant.findMany({ where: { machineId } });
|
it('refuses creation when requirement is not part of the machine skeleton', async () => {
|
||||||
expect(initialComponents).toHaveLength(1);
|
prisma.machine.findUnique.mockResolvedValue({
|
||||||
const initialComponentId = initialComponents[0].id;
|
id: 'machine-1',
|
||||||
|
typeMachine: {
|
||||||
const initialPieces = await prisma.piece.findMany({ where: { machineId } });
|
componentRequirements: [
|
||||||
expect(initialPieces).toHaveLength(1);
|
{ id: 'req-1', typeComposantId: 'type-comp-1' },
|
||||||
const initialPieceId = initialPieces[0].id;
|
],
|
||||||
|
|
||||||
expect(
|
|
||||||
await prisma.customFieldValue.count({ where: { composantId: initialComponentId } }),
|
|
||||||
).toBe(1);
|
|
||||||
expect(await prisma.customFieldValue.count({ where: { pieceId: initialPieceId } })).toBe(1);
|
|
||||||
|
|
||||||
await prisma.typeMachineComponentRequirement.deleteMany({
|
|
||||||
where: { typeMachineId: typeMachine.id },
|
|
||||||
});
|
|
||||||
await prisma.typeMachinePieceRequirement.deleteMany({
|
|
||||||
where: { typeMachineId: typeMachine.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const newComponentRequirement = await prisma.typeMachineComponentRequirement.create({
|
|
||||||
data: {
|
|
||||||
label: 'Modules mis à jour',
|
|
||||||
minCount: 2,
|
|
||||||
maxCount: 2,
|
|
||||||
required: true,
|
|
||||||
allowNewModels: true,
|
|
||||||
typeMachineId: typeMachine.id,
|
|
||||||
typeComposantId: typeComposant.id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPieceRequirement = await prisma.typeMachinePieceRequirement.create({
|
await request(app.getHttpServer())
|
||||||
data: {
|
.post('/composants')
|
||||||
label: 'Pièce mise à jour',
|
|
||||||
minCount: 1,
|
|
||||||
maxCount: 1,
|
|
||||||
required: true,
|
|
||||||
allowNewModels: false,
|
|
||||||
typeMachineId: typeMachine.id,
|
|
||||||
typePieceId: typePiece.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const reconfigureResponse = await request(app.getHttpServer())
|
|
||||||
.patch(`/machines/${machineId}/skeleton`)
|
|
||||||
.send({
|
.send({
|
||||||
componentSelections: [
|
name: 'Comp A',
|
||||||
{
|
machineId: 'machine-1',
|
||||||
requirementId: newComponentRequirement.id,
|
typeComposantId: 'type-comp-1',
|
||||||
componentModelId: componentModelV2.id,
|
typeMachineComponentRequirementId: 'req-2',
|
||||||
},
|
|
||||||
{
|
|
||||||
requirementId: newComponentRequirement.id,
|
|
||||||
definition: {
|
|
||||||
name: 'Module personnalisé',
|
|
||||||
customFields: [{ name: 'Serial', defaultValue: 'SERIAL-CUSTOM' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pieceSelections: [
|
|
||||||
{
|
|
||||||
requirementId: newPieceRequirement.id,
|
|
||||||
pieceModelId: pieceModelV2.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(400);
|
||||||
|
|
||||||
expect(reconfigureResponse.body.composants).toHaveLength(2);
|
expect(prisma.composant.create).not.toHaveBeenCalled();
|
||||||
expect(reconfigureResponse.body.pieces).toHaveLength(1);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const componentsAfter = await prisma.composant.findMany({ where: { machineId } });
|
describe('POST /pieces', () => {
|
||||||
expect(componentsAfter).toHaveLength(2);
|
it('accepts creation when requirement matches the machine skeleton', async () => {
|
||||||
const componentIdsAfter = componentsAfter.map((component) => component.id);
|
prisma.machine.findUnique.mockResolvedValue({
|
||||||
expect(componentIdsAfter).not.toContain(initialComponentId);
|
id: 'machine-1',
|
||||||
componentsAfter.forEach((component) => {
|
typeMachine: {
|
||||||
expect(component.typeMachineComponentRequirementId).toBe(newComponentRequirement.id);
|
pieceRequirements: [
|
||||||
|
{ id: 'req-1', typePieceId: 'type-piece-1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const componentValueCount = await prisma.customFieldValue.count({
|
const created = { id: 'piece-1' };
|
||||||
where: { composantId: { in: componentIdsAfter } },
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(componentValueCount).toBe(2);
|
|
||||||
expect(
|
|
||||||
await prisma.customFieldValue.count({ where: { composantId: initialComponentId } }),
|
|
||||||
).toBe(0);
|
|
||||||
|
|
||||||
const piecesAfter = await prisma.piece.findMany({ where: { machineId } });
|
await request(app.getHttpServer())
|
||||||
expect(piecesAfter).toHaveLength(1);
|
.post('/pieces')
|
||||||
const [updatedPiece] = piecesAfter;
|
.send({
|
||||||
expect(updatedPiece.typeMachinePieceRequirementId).toBe(newPieceRequirement.id);
|
name: 'Piece A',
|
||||||
expect(updatedPiece.id).not.toBe(initialPieceId);
|
machineId: 'machine-1',
|
||||||
|
typePieceId: 'type-piece-1',
|
||||||
|
typeMachinePieceRequirementId: 'req-2',
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(prisma.piece.create).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(
|
|
||||||
await prisma.customFieldValue.count({ where: { pieceId: updatedPiece.id } }),
|
|
||||||
).toBe(1);
|
|
||||||
expect(await prisma.customFieldValue.count({ where: { pieceId: initialPieceId } })).toBe(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user