[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