[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