[S3-T3] test: Add InspectionService unit tests
- Created inspection.service.spec.ts (300+ lines) - Tests for CRUD, state transitions, result recording - Tests for approval/rejection workflow - Tests for non-conformity auto-creation - Mocks for TypeORM repositories Sprint 3 Task: Basic tests for priority projects Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
728f8ae7fd
commit
e2d446181c
450
src/modules/quality/__tests__/inspection.service.spec.ts
Normal file
450
src/modules/quality/__tests__/inspection.service.spec.ts
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
/**
|
||||||
|
* InspectionService Unit Tests
|
||||||
|
* Module: Quality
|
||||||
|
* Project: erp-construccion
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { InspectionService, CreateInspectionDto, RecordResultDto } from '../services/inspection.service';
|
||||||
|
import { Inspection, InspectionStatus } from '../entities/inspection.entity';
|
||||||
|
import { InspectionResult } from '../entities/inspection-result.entity';
|
||||||
|
import { NonConformity } from '../entities/non-conformity.entity';
|
||||||
|
import { Checklist } from '../entities/checklist.entity';
|
||||||
|
|
||||||
|
// Mock repositories
|
||||||
|
const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
createQueryBuilder: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<Repository<T>>);
|
||||||
|
|
||||||
|
describe('InspectionService', () => {
|
||||||
|
let service: InspectionService;
|
||||||
|
let inspectionRepository: jest.Mocked<Repository<Inspection>>;
|
||||||
|
let resultRepository: jest.Mocked<Repository<InspectionResult>>;
|
||||||
|
let nonConformityRepository: jest.Mocked<Repository<NonConformity>>;
|
||||||
|
let checklistRepository: jest.Mocked<Repository<Checklist>>;
|
||||||
|
|
||||||
|
const mockTenantId = 'tenant-123';
|
||||||
|
const mockUserId = 'user-456';
|
||||||
|
const mockCtx = { tenantId: mockTenantId, userId: mockUserId };
|
||||||
|
|
||||||
|
const mockChecklist: Partial<Checklist> = {
|
||||||
|
id: 'checklist-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
name: 'Quality Checklist',
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
items: [
|
||||||
|
{ id: 'item-1', isActive: true } as any,
|
||||||
|
{ id: 'item-2', isActive: true } as any,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockInspection: Partial<Inspection> = {
|
||||||
|
id: 'inspection-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
checklistId: 'checklist-1',
|
||||||
|
loteId: 'lote-1',
|
||||||
|
inspectionNumber: 'INS-2601-ABC123',
|
||||||
|
inspectionDate: new Date('2026-02-03'),
|
||||||
|
inspectorId: 'inspector-1',
|
||||||
|
status: 'pending' as InspectionStatus,
|
||||||
|
totalItems: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
inspectionRepository = createMockRepository<Inspection>();
|
||||||
|
resultRepository = createMockRepository<InspectionResult>();
|
||||||
|
nonConformityRepository = createMockRepository<NonConformity>();
|
||||||
|
checklistRepository = createMockRepository<Checklist>();
|
||||||
|
|
||||||
|
service = new InspectionService(
|
||||||
|
inspectionRepository,
|
||||||
|
resultRepository,
|
||||||
|
nonConformityRepository,
|
||||||
|
checklistRepository
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
const createDto: CreateInspectionDto = {
|
||||||
|
checklistId: 'checklist-1',
|
||||||
|
loteId: 'lote-1',
|
||||||
|
inspectionDate: new Date('2026-02-03'),
|
||||||
|
inspectorId: 'inspector-1',
|
||||||
|
notes: 'Test inspection',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create inspection successfully', async () => {
|
||||||
|
checklistRepository.findOne.mockResolvedValue(mockChecklist as Checklist);
|
||||||
|
inspectionRepository.create.mockReturnValue(mockInspection as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(mockInspection as Inspection);
|
||||||
|
|
||||||
|
const result = await service.create(mockCtx, createDto);
|
||||||
|
|
||||||
|
expect(checklistRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
id: createDto.checklistId,
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
}),
|
||||||
|
relations: ['items'],
|
||||||
|
});
|
||||||
|
expect(inspectionRepository.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
checklistId: createDto.checklistId,
|
||||||
|
loteId: createDto.loteId,
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockInspection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when checklist not found', async () => {
|
||||||
|
checklistRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.create(mockCtx, createDto)).rejects.toThrow(
|
||||||
|
'Checklist not found or inactive'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate total items from active checklist items', async () => {
|
||||||
|
checklistRepository.findOne.mockResolvedValue(mockChecklist as Checklist);
|
||||||
|
inspectionRepository.create.mockReturnValue(mockInspection as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(mockInspection as Inspection);
|
||||||
|
|
||||||
|
await service.create(mockCtx, createDto);
|
||||||
|
|
||||||
|
expect(inspectionRepository.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
totalItems: 2,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should return inspection when found', async () => {
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(mockInspection as Inspection);
|
||||||
|
|
||||||
|
const result = await service.findById(mockCtx, 'inspection-1');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockInspection);
|
||||||
|
expect(inspectionRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'inspection-1', tenantId: mockTenantId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when inspection not found', async () => {
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await service.findById(mockCtx, 'non-existent');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startInspection', () => {
|
||||||
|
it('should start pending inspection', async () => {
|
||||||
|
const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
|
||||||
|
const startedInspection = { ...pendingInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(startedInspection as Inspection);
|
||||||
|
|
||||||
|
const result = await service.startInspection(mockCtx, 'inspection-1');
|
||||||
|
|
||||||
|
expect(result?.status).toBe('in_progress');
|
||||||
|
expect(inspectionRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection is not pending', async () => {
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
|
||||||
|
await expect(service.startInspection(mockCtx, 'inspection-1')).rejects.toThrow(
|
||||||
|
'Can only start pending inspections'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when inspection not found', async () => {
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await service.startInspection(mockCtx, 'non-existent');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordResult', () => {
|
||||||
|
const resultDto: RecordResultDto = {
|
||||||
|
checklistItemId: 'item-1',
|
||||||
|
result: 'passed',
|
||||||
|
observations: 'Looks good',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should record passed result', async () => {
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
const mockResult: Partial<InspectionResult> = {
|
||||||
|
id: 'result-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
inspectionId: 'inspection-1',
|
||||||
|
checklistItemId: 'item-1',
|
||||||
|
result: 'passed',
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
resultRepository.findOne.mockResolvedValue(null);
|
||||||
|
resultRepository.create.mockReturnValue(mockResult as InspectionResult);
|
||||||
|
resultRepository.save.mockResolvedValue(mockResult as InspectionResult);
|
||||||
|
|
||||||
|
const result = await service.recordResult(mockCtx, 'inspection-1', resultDto);
|
||||||
|
|
||||||
|
expect(result.result).toBe('passed');
|
||||||
|
expect(nonConformityRepository.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create non-conformity when result is failed', async () => {
|
||||||
|
const failedDto: RecordResultDto = {
|
||||||
|
checklistItemId: 'item-1',
|
||||||
|
result: 'failed',
|
||||||
|
observations: 'Defect found',
|
||||||
|
};
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
const mockResult: Partial<InspectionResult> = {
|
||||||
|
id: 'result-1',
|
||||||
|
result: 'failed',
|
||||||
|
};
|
||||||
|
const mockNC: Partial<NonConformity> = {
|
||||||
|
id: 'nc-1',
|
||||||
|
ncNumber: 'NC-2601-ABCD',
|
||||||
|
status: 'open',
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
resultRepository.findOne.mockResolvedValue(null);
|
||||||
|
resultRepository.create.mockReturnValue(mockResult as InspectionResult);
|
||||||
|
resultRepository.save.mockResolvedValue(mockResult as InspectionResult);
|
||||||
|
nonConformityRepository.create.mockReturnValue(mockNC as NonConformity);
|
||||||
|
nonConformityRepository.save.mockResolvedValue(mockNC as NonConformity);
|
||||||
|
|
||||||
|
const result = await service.recordResult(mockCtx, 'inspection-1', failedDto);
|
||||||
|
|
||||||
|
expect(result.result).toBe('failed');
|
||||||
|
expect(nonConformityRepository.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
inspectionId: 'inspection-1',
|
||||||
|
severity: 'minor',
|
||||||
|
status: 'open',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing result instead of creating new', async () => {
|
||||||
|
const existingResult: Partial<InspectionResult> = {
|
||||||
|
id: 'result-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
inspectionId: 'inspection-1',
|
||||||
|
checklistItemId: 'item-1',
|
||||||
|
result: 'failed',
|
||||||
|
};
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
resultRepository.findOne.mockResolvedValue(existingResult as InspectionResult);
|
||||||
|
resultRepository.save.mockResolvedValue({ ...existingResult, result: 'passed' } as InspectionResult);
|
||||||
|
|
||||||
|
await service.recordResult(mockCtx, 'inspection-1', resultDto);
|
||||||
|
|
||||||
|
expect(resultRepository.create).not.toHaveBeenCalled();
|
||||||
|
expect(resultRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection not found', async () => {
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.recordResult(mockCtx, 'non-existent', resultDto)).rejects.toThrow(
|
||||||
|
'Inspection not found'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection is not in progress', async () => {
|
||||||
|
const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);
|
||||||
|
|
||||||
|
await expect(service.recordResult(mockCtx, 'inspection-1', resultDto)).rejects.toThrow(
|
||||||
|
'Can only record results for in-progress inspections'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('completeInspection', () => {
|
||||||
|
it('should complete inspection and calculate pass rate', async () => {
|
||||||
|
const inProgressInspection = {
|
||||||
|
...mockInspection,
|
||||||
|
status: 'in_progress' as InspectionStatus,
|
||||||
|
results: [
|
||||||
|
{ result: 'passed' },
|
||||||
|
{ result: 'passed' },
|
||||||
|
{ result: 'failed' },
|
||||||
|
{ result: 'not_applicable' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const completedInspection = {
|
||||||
|
...inProgressInspection,
|
||||||
|
status: 'completed' as InspectionStatus,
|
||||||
|
passedItems: 2,
|
||||||
|
failedItems: 1,
|
||||||
|
passRate: 66.67,
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(completedInspection as unknown as Inspection);
|
||||||
|
|
||||||
|
const result = await service.completeInspection(mockCtx, 'inspection-1');
|
||||||
|
|
||||||
|
expect(result?.status).toBe('completed');
|
||||||
|
expect(result?.passedItems).toBe(2);
|
||||||
|
expect(result?.failedItems).toBe(1);
|
||||||
|
expect(inspectionRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection is not in progress', async () => {
|
||||||
|
const pendingInspection = { ...mockInspection, status: 'pending' as InspectionStatus };
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(pendingInspection as Inspection);
|
||||||
|
|
||||||
|
await expect(service.completeInspection(mockCtx, 'inspection-1')).rejects.toThrow(
|
||||||
|
'Can only complete in-progress inspections'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('approveInspection', () => {
|
||||||
|
it('should approve completed inspection without critical NCs', async () => {
|
||||||
|
const completedInspection = {
|
||||||
|
...mockInspection,
|
||||||
|
status: 'completed' as InspectionStatus,
|
||||||
|
nonConformities: [
|
||||||
|
{ severity: 'minor', status: 'verified' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const approvedInspection = {
|
||||||
|
...completedInspection,
|
||||||
|
status: 'approved' as InspectionStatus,
|
||||||
|
approvedById: mockUserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(completedInspection as unknown as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(approvedInspection as unknown as Inspection);
|
||||||
|
|
||||||
|
const result = await service.approveInspection(mockCtx, 'inspection-1');
|
||||||
|
|
||||||
|
expect(result?.status).toBe('approved');
|
||||||
|
expect(result?.approvedById).toBe(mockUserId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when critical NCs are open', async () => {
|
||||||
|
const completedInspection = {
|
||||||
|
...mockInspection,
|
||||||
|
status: 'completed' as InspectionStatus,
|
||||||
|
nonConformities: [
|
||||||
|
{ severity: 'critical', status: 'open' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(completedInspection as unknown as Inspection);
|
||||||
|
|
||||||
|
await expect(service.approveInspection(mockCtx, 'inspection-1')).rejects.toThrow(
|
||||||
|
'Cannot approve inspection with open critical non-conformities'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection is not completed', async () => {
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
|
||||||
|
await expect(service.approveInspection(mockCtx, 'inspection-1')).rejects.toThrow(
|
||||||
|
'Can only approve completed inspections'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rejectInspection', () => {
|
||||||
|
it('should reject completed inspection with reason', async () => {
|
||||||
|
const completedInspection = {
|
||||||
|
...mockInspection,
|
||||||
|
status: 'completed' as InspectionStatus,
|
||||||
|
};
|
||||||
|
const rejectedInspection = {
|
||||||
|
...completedInspection,
|
||||||
|
status: 'rejected' as InspectionStatus,
|
||||||
|
rejectionReason: 'Does not meet standards',
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(completedInspection as Inspection);
|
||||||
|
inspectionRepository.save.mockResolvedValue(rejectedInspection as unknown as Inspection);
|
||||||
|
|
||||||
|
const result = await service.rejectInspection(mockCtx, 'inspection-1', 'Does not meet standards');
|
||||||
|
|
||||||
|
expect(result?.status).toBe('rejected');
|
||||||
|
expect(result?.rejectionReason).toBe('Does not meet standards');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inspection is not completed', async () => {
|
||||||
|
const inProgressInspection = { ...mockInspection, status: 'in_progress' as InspectionStatus };
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(inProgressInspection as Inspection);
|
||||||
|
|
||||||
|
await expect(service.rejectInspection(mockCtx, 'inspection-1', 'reason')).rejects.toThrow(
|
||||||
|
'Can only reject completed inspections'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when inspection not found', async () => {
|
||||||
|
inspectionRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await service.rejectInspection(mockCtx, 'non-existent', 'reason');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findWithFilters', () => {
|
||||||
|
it('should return paginated results with filters', async () => {
|
||||||
|
const mockQueryBuilder = {
|
||||||
|
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
skip: jest.fn().mockReturnThis(),
|
||||||
|
take: jest.fn().mockReturnThis(),
|
||||||
|
getManyAndCount: jest.fn().mockResolvedValue([[mockInspection], 1]),
|
||||||
|
};
|
||||||
|
|
||||||
|
inspectionRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
|
||||||
|
|
||||||
|
const result = await service.findWithFilters(
|
||||||
|
mockCtx,
|
||||||
|
{ status: 'pending' as InspectionStatus },
|
||||||
|
1,
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
expect(result.page).toBe(1);
|
||||||
|
expect(result.limit).toBe(20);
|
||||||
|
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('insp.status = :status', { status: 'pending' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user