[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:
Adrian Flores Cortes 2026-02-03 08:23:15 -06:00
parent 728f8ae7fd
commit e2d446181c

View 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' });
});
});
});