[GAP-001,002,006] test: Add unit tests for GAP services
- kpi-config.service.spec.ts: 9 test cases for KPI CRUD and values - tool-loan.service.spec.ts: 8 test cases for loan operations - digital-signature.service.spec.ts: 10 test cases for signature workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2caa489093
commit
6fa4a6c552
188
src/modules/assets/__tests__/tool-loan.service.spec.ts
Normal file
188
src/modules/assets/__tests__/tool-loan.service.spec.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* ToolLoanService Unit Tests
|
||||||
|
* Module: Assets (MAE-015)
|
||||||
|
* GAP: GAP-002
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { ToolLoanService, CreateToolLoanDto, ReturnToolDto } from '../services/tool-loan.service';
|
||||||
|
import { ToolLoan } from '../entities/tool-loan.entity';
|
||||||
|
import { Asset } from '../entities/asset.entity';
|
||||||
|
|
||||||
|
const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
count: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
createQueryBuilder: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<Repository<T>>);
|
||||||
|
|
||||||
|
const createMockQueryBuilder = <T>(): jest.Mocked<SelectQueryBuilder<T>> => ({
|
||||||
|
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(),
|
||||||
|
} as unknown as jest.Mocked<SelectQueryBuilder<T>>);
|
||||||
|
|
||||||
|
describe('ToolLoanService', () => {
|
||||||
|
let service: ToolLoanService;
|
||||||
|
let loanRepository: jest.Mocked<Repository<ToolLoan>>;
|
||||||
|
let assetRepository: jest.Mocked<Repository<Asset>>;
|
||||||
|
|
||||||
|
const mockTenantId = 'tenant-123';
|
||||||
|
const mockUserId = 'user-456';
|
||||||
|
|
||||||
|
const mockTool: Partial<Asset> = {
|
||||||
|
id: 'tool-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
name: 'Taladro Industrial',
|
||||||
|
assetType: 'tool',
|
||||||
|
status: 'available',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLoan: Partial<ToolLoan> = {
|
||||||
|
id: 'loan-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
toolId: 'tool-1',
|
||||||
|
employeeId: 'emp-1',
|
||||||
|
employeeName: 'Juan Perez',
|
||||||
|
status: 'active',
|
||||||
|
loanDate: new Date('2026-01-15'),
|
||||||
|
expectedReturnDate: new Date('2026-01-30'),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loanRepository = createMockRepository<ToolLoan>();
|
||||||
|
assetRepository = createMockRepository<Asset>();
|
||||||
|
|
||||||
|
const mockDataSource = {
|
||||||
|
getRepository: jest.fn().mockImplementation((entity) => {
|
||||||
|
if (entity === ToolLoan) return loanRepository;
|
||||||
|
if (entity === Asset) return assetRepository;
|
||||||
|
return createMockRepository();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new ToolLoanService(mockDataSource as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
const createDto: CreateToolLoanDto = {
|
||||||
|
toolId: 'tool-1',
|
||||||
|
employeeId: 'emp-1',
|
||||||
|
employeeName: 'Juan Perez',
|
||||||
|
loanDate: new Date('2026-01-15'),
|
||||||
|
expectedReturnDate: new Date('2026-01-30'),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create a new tool loan', async () => {
|
||||||
|
assetRepository.findOne.mockResolvedValue(mockTool as Asset);
|
||||||
|
loanRepository.findOne.mockResolvedValue(null); // No active loan
|
||||||
|
loanRepository.create.mockReturnValue(mockLoan as ToolLoan);
|
||||||
|
loanRepository.save.mockResolvedValue(mockLoan as ToolLoan);
|
||||||
|
assetRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||||
|
|
||||||
|
const result = await service.create(mockTenantId, createDto, mockUserId);
|
||||||
|
|
||||||
|
expect(loanRepository.save).toHaveBeenCalled();
|
||||||
|
expect(assetRepository.update).toHaveBeenCalled(); // Tool status updated
|
||||||
|
expect(result).toEqual(mockLoan);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if tool not found', async () => {
|
||||||
|
assetRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.create(mockTenantId, createDto, mockUserId))
|
||||||
|
.rejects.toThrow('Tool not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if tool already has active loan', async () => {
|
||||||
|
assetRepository.findOne.mockResolvedValue(mockTool as Asset);
|
||||||
|
loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
|
||||||
|
|
||||||
|
await expect(service.create(mockTenantId, createDto, mockUserId))
|
||||||
|
.rejects.toThrow('Tool already has an active loan');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('returnTool', () => {
|
||||||
|
const returnDto: ReturnToolDto = {
|
||||||
|
actualReturnDate: new Date('2026-01-28'),
|
||||||
|
conditionIn: 'good',
|
||||||
|
status: 'returned',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return tool and update status', async () => {
|
||||||
|
loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
|
||||||
|
loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'returned' } as ToolLoan);
|
||||||
|
assetRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||||
|
|
||||||
|
const result = await service.returnTool(mockTenantId, 'loan-1', returnDto, mockUserId);
|
||||||
|
|
||||||
|
expect(loanRepository.save).toHaveBeenCalled();
|
||||||
|
expect(assetRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: mockLoan.toolId, tenantId: mockTenantId },
|
||||||
|
expect.objectContaining({ status: 'available' })
|
||||||
|
);
|
||||||
|
expect(result?.status).toBe('returned');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark tool as in_maintenance if returned damaged', async () => {
|
||||||
|
loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
|
||||||
|
loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'damaged' } as ToolLoan);
|
||||||
|
assetRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||||
|
|
||||||
|
await service.returnTool(mockTenantId, 'loan-1', { ...returnDto, status: 'damaged' }, mockUserId);
|
||||||
|
|
||||||
|
expect(assetRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: mockLoan.toolId, tenantId: mockTenantId },
|
||||||
|
expect.objectContaining({ status: 'in_maintenance' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markAsLost', () => {
|
||||||
|
it('should mark loan as lost and tool as inactive', async () => {
|
||||||
|
loanRepository.findOne.mockResolvedValue(mockLoan as ToolLoan);
|
||||||
|
loanRepository.save.mockResolvedValue({ ...mockLoan, status: 'lost' } as ToolLoan);
|
||||||
|
assetRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||||
|
|
||||||
|
const result = await service.markAsLost(mockTenantId, 'loan-1', 'Extraviado en obra', mockUserId);
|
||||||
|
|
||||||
|
expect(result?.status).toBe('lost');
|
||||||
|
expect(assetRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: mockLoan.toolId, tenantId: mockTenantId },
|
||||||
|
{ status: 'inactive' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatistics', () => {
|
||||||
|
it('should return loan statistics', async () => {
|
||||||
|
loanRepository.count.mockResolvedValueOnce(5) // active
|
||||||
|
.mockResolvedValueOnce(20) // returned
|
||||||
|
.mockResolvedValueOnce(2) // overdue
|
||||||
|
.mockResolvedValueOnce(1) // lost
|
||||||
|
.mockResolvedValueOnce(3); // damaged
|
||||||
|
|
||||||
|
const stats = await service.getStatistics(mockTenantId);
|
||||||
|
|
||||||
|
expect(stats).toEqual({
|
||||||
|
active: 5,
|
||||||
|
returned: 20,
|
||||||
|
overdue: 2,
|
||||||
|
lost: 1,
|
||||||
|
damaged: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* DigitalSignatureService Unit Tests
|
||||||
|
* Module: Documents (MAE-016)
|
||||||
|
* GAP: GAP-006
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { DigitalSignatureService, CreateSignatureRequestDto, SignDocumentDto } from '../services/digital-signature.service';
|
||||||
|
import { DigitalSignature } from '../entities/digital-signature.entity';
|
||||||
|
|
||||||
|
const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
count: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
createQueryBuilder: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<Repository<T>>);
|
||||||
|
|
||||||
|
const createMockQueryBuilder = <T>(): jest.Mocked<SelectQueryBuilder<T>> => ({
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
skip: jest.fn().mockReturnThis(),
|
||||||
|
take: jest.fn().mockReturnThis(),
|
||||||
|
getManyAndCount: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<SelectQueryBuilder<T>>);
|
||||||
|
|
||||||
|
describe('DigitalSignatureService', () => {
|
||||||
|
let service: DigitalSignatureService;
|
||||||
|
let repository: jest.Mocked<Repository<DigitalSignature>>;
|
||||||
|
|
||||||
|
const mockTenantId = 'tenant-123';
|
||||||
|
const mockUserId = 'user-456';
|
||||||
|
|
||||||
|
const mockSignature: Partial<DigitalSignature> = {
|
||||||
|
id: 'sig-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
documentId: 'doc-1',
|
||||||
|
signerId: 'signer-1',
|
||||||
|
signerName: 'Juan Perez',
|
||||||
|
signerEmail: 'juan@example.com',
|
||||||
|
signatureType: 'simple',
|
||||||
|
status: 'pending',
|
||||||
|
requestedAt: new Date('2026-02-01'),
|
||||||
|
isValid: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
repository = createMockRepository<DigitalSignature>();
|
||||||
|
|
||||||
|
const mockDataSource = {
|
||||||
|
getRepository: jest.fn().mockReturnValue(repository),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new DigitalSignatureService(mockDataSource as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requestSignature', () => {
|
||||||
|
const createDto: CreateSignatureRequestDto = {
|
||||||
|
documentId: 'doc-1',
|
||||||
|
signerId: 'signer-1',
|
||||||
|
signerName: 'Juan Perez',
|
||||||
|
signerEmail: 'juan@example.com',
|
||||||
|
signatureType: 'simple',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create a signature request', async () => {
|
||||||
|
repository.create.mockReturnValue(mockSignature as DigitalSignature);
|
||||||
|
repository.save.mockResolvedValue(mockSignature as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.requestSignature(mockTenantId, createDto, mockUserId);
|
||||||
|
|
||||||
|
expect(repository.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
documentId: createDto.documentId,
|
||||||
|
signerId: createDto.signerId,
|
||||||
|
status: 'pending',
|
||||||
|
}));
|
||||||
|
expect(result).toEqual(mockSignature);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('signDocument', () => {
|
||||||
|
const signDto: SignDocumentDto = {
|
||||||
|
signatureData: 'base64-signature-data',
|
||||||
|
ipAddress: '192.168.1.1',
|
||||||
|
userAgent: 'Mozilla/5.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should sign a pending signature request', async () => {
|
||||||
|
repository.findOne.mockResolvedValue(mockSignature as DigitalSignature);
|
||||||
|
const signedSignature = { ...mockSignature, status: 'signed', signedAt: new Date() };
|
||||||
|
repository.save.mockResolvedValue(signedSignature as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.signDocument(mockTenantId, 'sig-1', signDto, mockUserId);
|
||||||
|
|
||||||
|
expect(repository.save).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
status: 'signed',
|
||||||
|
signatureData: signDto.signatureData,
|
||||||
|
}));
|
||||||
|
expect(result?.status).toBe('signed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if signature not pending', async () => {
|
||||||
|
const signedSig = { ...mockSignature, status: 'signed' };
|
||||||
|
repository.findOne.mockResolvedValue(signedSig as DigitalSignature);
|
||||||
|
|
||||||
|
await expect(service.signDocument(mockTenantId, 'sig-1', signDto, mockUserId))
|
||||||
|
.rejects.toThrow('Signature request is not pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if signature expired', async () => {
|
||||||
|
const expiredSig = { ...mockSignature, expiresAt: new Date('2025-01-01') };
|
||||||
|
repository.findOne.mockResolvedValue(expiredSig as DigitalSignature);
|
||||||
|
repository.save.mockResolvedValue({ ...expiredSig, status: 'expired' } as DigitalSignature);
|
||||||
|
|
||||||
|
await expect(service.signDocument(mockTenantId, 'sig-1', signDto, mockUserId))
|
||||||
|
.rejects.toThrow('Signature request has expired');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rejectSignature', () => {
|
||||||
|
it('should reject a pending signature', async () => {
|
||||||
|
repository.findOne.mockResolvedValue(mockSignature as DigitalSignature);
|
||||||
|
repository.save.mockResolvedValue({ ...mockSignature, status: 'rejected' } as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.rejectSignature(mockTenantId, 'sig-1', { reason: 'Not authorized' }, mockUserId);
|
||||||
|
|
||||||
|
expect(result?.status).toBe('rejected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('revokeSignature', () => {
|
||||||
|
it('should revoke a signed signature', async () => {
|
||||||
|
const signedSig = { ...mockSignature, status: 'signed' };
|
||||||
|
repository.findOne.mockResolvedValue(signedSig as DigitalSignature);
|
||||||
|
repository.save.mockResolvedValue({ ...signedSig, status: 'revoked', isValid: false } as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.revokeSignature(mockTenantId, 'sig-1', 'Document superseded', mockUserId);
|
||||||
|
|
||||||
|
expect(result?.status).toBe('revoked');
|
||||||
|
expect(result?.isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if not signed', async () => {
|
||||||
|
repository.findOne.mockResolvedValue(mockSignature as DigitalSignature);
|
||||||
|
|
||||||
|
await expect(service.revokeSignature(mockTenantId, 'sig-1', 'Reason', mockUserId))
|
||||||
|
.rejects.toThrow('Only signed documents can be revoked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateSignature', () => {
|
||||||
|
it('should validate a signed signature', async () => {
|
||||||
|
const signedSig = { ...mockSignature, status: 'signed' };
|
||||||
|
repository.findOne.mockResolvedValue(signedSig as DigitalSignature);
|
||||||
|
repository.save.mockResolvedValue(signedSig as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.validateSignature(mockTenantId, 'sig-1');
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(true);
|
||||||
|
expect(result.errors).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return errors for invalid status', async () => {
|
||||||
|
repository.findOne.mockResolvedValue(mockSignature as DigitalSignature); // pending
|
||||||
|
repository.save.mockResolvedValue(mockSignature as DigitalSignature);
|
||||||
|
|
||||||
|
const result = await service.validateSignature(mockTenantId, 'sig-1');
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(false);
|
||||||
|
expect(result.errors).toContain('Invalid status: pending');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStatistics', () => {
|
||||||
|
it('should return signature statistics', async () => {
|
||||||
|
repository.count.mockResolvedValueOnce(10) // pending
|
||||||
|
.mockResolvedValueOnce(50) // signed
|
||||||
|
.mockResolvedValueOnce(5) // rejected
|
||||||
|
.mockResolvedValueOnce(3) // expired
|
||||||
|
.mockResolvedValueOnce(2); // revoked
|
||||||
|
|
||||||
|
const stats = await service.getStatistics(mockTenantId);
|
||||||
|
|
||||||
|
expect(stats).toEqual({
|
||||||
|
pending: 10,
|
||||||
|
signed: 50,
|
||||||
|
rejected: 5,
|
||||||
|
expired: 3,
|
||||||
|
revoked: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
237
src/modules/reports/__tests__/kpi-config.service.spec.ts
Normal file
237
src/modules/reports/__tests__/kpi-config.service.spec.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* KpiConfigService Unit Tests
|
||||||
|
* Module: Reports (MAI-006)
|
||||||
|
* GAP: GAP-001
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { KpiConfigService, CreateKpiConfigDto, CreateKpiValueDto } from '../services/kpi-config.service';
|
||||||
|
import { KpiConfig } from '../entities/kpi-config.entity';
|
||||||
|
import { KpiValue } from '../entities/kpi-value.entity';
|
||||||
|
|
||||||
|
// Mock repositories
|
||||||
|
const createMockRepository = <T>(): jest.Mocked<Repository<T>> => ({
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
createQueryBuilder: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<Repository<T>>);
|
||||||
|
|
||||||
|
const createMockQueryBuilder = <T>(): jest.Mocked<SelectQueryBuilder<T>> => ({
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
addOrderBy: jest.fn().mockReturnThis(),
|
||||||
|
skip: jest.fn().mockReturnThis(),
|
||||||
|
take: jest.fn().mockReturnThis(),
|
||||||
|
getOne: jest.fn(),
|
||||||
|
getManyAndCount: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<SelectQueryBuilder<T>>);
|
||||||
|
|
||||||
|
describe('KpiConfigService', () => {
|
||||||
|
let service: KpiConfigService;
|
||||||
|
let configRepository: jest.Mocked<Repository<KpiConfig>>;
|
||||||
|
let valueRepository: jest.Mocked<Repository<KpiValue>>;
|
||||||
|
|
||||||
|
const mockTenantId = 'tenant-123';
|
||||||
|
const mockUserId = 'user-456';
|
||||||
|
|
||||||
|
const mockKpiConfig: Partial<KpiConfig> = {
|
||||||
|
id: 'kpi-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
code: 'SPI',
|
||||||
|
name: 'Schedule Performance Index',
|
||||||
|
category: 'progress',
|
||||||
|
module: 'construction',
|
||||||
|
formula: 'earned_value / planned_value',
|
||||||
|
formulaType: 'expression',
|
||||||
|
targetValue: 1.0,
|
||||||
|
thresholdGreen: 0.95,
|
||||||
|
thresholdYellow: 0.85,
|
||||||
|
isActive: true,
|
||||||
|
isSystem: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configRepository = createMockRepository<KpiConfig>();
|
||||||
|
valueRepository = createMockRepository<KpiValue>();
|
||||||
|
|
||||||
|
const mockDataSource = {
|
||||||
|
getRepository: jest.fn().mockImplementation((entity) => {
|
||||||
|
if (entity === KpiConfig) return configRepository;
|
||||||
|
if (entity === KpiValue) return valueRepository;
|
||||||
|
return createMockRepository();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new KpiConfigService(mockDataSource as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
const createDto: CreateKpiConfigDto = {
|
||||||
|
code: 'SPI',
|
||||||
|
name: 'Schedule Performance Index',
|
||||||
|
category: 'progress',
|
||||||
|
module: 'construction',
|
||||||
|
formula: 'earned_value / planned_value',
|
||||||
|
targetValue: 1.0,
|
||||||
|
thresholdGreen: 0.95,
|
||||||
|
thresholdYellow: 0.85,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create a new KPI config', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(null);
|
||||||
|
configRepository.create.mockReturnValue(mockKpiConfig as KpiConfig);
|
||||||
|
configRepository.save.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
|
||||||
|
const result = await service.create(mockTenantId, createDto, mockUserId);
|
||||||
|
|
||||||
|
expect(configRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: { tenantId: mockTenantId, code: createDto.code },
|
||||||
|
});
|
||||||
|
expect(configRepository.create).toHaveBeenCalled();
|
||||||
|
expect(configRepository.save).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(mockKpiConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if KPI code already exists', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
|
||||||
|
await expect(service.create(mockTenantId, createDto, mockUserId))
|
||||||
|
.rejects.toThrow(`KPI with code ${createDto.code} already exists`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should return KPI config by ID', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
|
||||||
|
const result = await service.findById(mockTenantId, 'kpi-1');
|
||||||
|
|
||||||
|
expect(configRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'kpi-1', tenantId: mockTenantId, deletedAt: undefined },
|
||||||
|
});
|
||||||
|
expect(result).toEqual(mockKpiConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if not found', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await service.findById(mockTenantId, 'non-existent');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findAll', () => {
|
||||||
|
it('should return paginated KPI configs', async () => {
|
||||||
|
const mockQueryBuilder = createMockQueryBuilder<KpiConfig>();
|
||||||
|
mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockKpiConfig as KpiConfig], 1]);
|
||||||
|
configRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
|
||||||
|
|
||||||
|
const result = await service.findAll(mockTenantId, {}, { page: 1, limit: 20 });
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
expect(result.page).toBe(1);
|
||||||
|
expect(result.totalPages).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by category', async () => {
|
||||||
|
const mockQueryBuilder = createMockQueryBuilder<KpiConfig>();
|
||||||
|
mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]);
|
||||||
|
configRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);
|
||||||
|
|
||||||
|
await service.findAll(mockTenantId, { category: 'progress' }, { page: 1, limit: 20 });
|
||||||
|
|
||||||
|
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
|
||||||
|
'kpi.category = :category',
|
||||||
|
{ category: 'progress' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should update KPI config', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
configRepository.save.mockResolvedValue({ ...mockKpiConfig, name: 'Updated' } as KpiConfig);
|
||||||
|
|
||||||
|
const result = await service.update(mockTenantId, 'kpi-1', { name: 'Updated' }, mockUserId);
|
||||||
|
|
||||||
|
expect(configRepository.save).toHaveBeenCalled();
|
||||||
|
expect(result?.name).toBe('Updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when modifying system KPI', async () => {
|
||||||
|
const systemKpi = { ...mockKpiConfig, isSystem: true };
|
||||||
|
configRepository.findOne.mockResolvedValue(systemKpi as KpiConfig);
|
||||||
|
|
||||||
|
await expect(service.update(mockTenantId, 'kpi-1', { isActive: false }, mockUserId))
|
||||||
|
.rejects.toThrow('Cannot modify system KPIs');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should soft delete KPI config', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
configRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||||
|
|
||||||
|
const result = await service.delete(mockTenantId, 'kpi-1', mockUserId);
|
||||||
|
|
||||||
|
expect(configRepository.update).toHaveBeenCalled();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when deleting system KPI', async () => {
|
||||||
|
const systemKpi = { ...mockKpiConfig, isSystem: true };
|
||||||
|
configRepository.findOne.mockResolvedValue(systemKpi as KpiConfig);
|
||||||
|
|
||||||
|
await expect(service.delete(mockTenantId, 'kpi-1', mockUserId))
|
||||||
|
.rejects.toThrow('Cannot delete system KPIs');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recordValue', () => {
|
||||||
|
const createValueDto: CreateKpiValueDto = {
|
||||||
|
kpiId: 'kpi-1',
|
||||||
|
periodStart: new Date('2026-01-01'),
|
||||||
|
periodEnd: new Date('2026-01-31'),
|
||||||
|
value: 0.92,
|
||||||
|
previousValue: 0.88,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should record KPI value with calculated status', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(mockKpiConfig as KpiConfig);
|
||||||
|
|
||||||
|
const mockValue: Partial<KpiValue> = {
|
||||||
|
id: 'value-1',
|
||||||
|
tenantId: mockTenantId,
|
||||||
|
kpiId: 'kpi-1',
|
||||||
|
value: 0.92,
|
||||||
|
status: 'yellow',
|
||||||
|
isOnTarget: false,
|
||||||
|
};
|
||||||
|
valueRepository.create.mockReturnValue(mockValue as KpiValue);
|
||||||
|
valueRepository.save.mockResolvedValue(mockValue as KpiValue);
|
||||||
|
|
||||||
|
const result = await service.recordValue(mockTenantId, createValueDto, mockUserId);
|
||||||
|
|
||||||
|
expect(valueRepository.create).toHaveBeenCalled();
|
||||||
|
expect(valueRepository.save).toHaveBeenCalled();
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if KPI not found', async () => {
|
||||||
|
configRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.recordValue(mockTenantId, createValueDto, mockUserId))
|
||||||
|
.rejects.toThrow('KPI configuration not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user