feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
This commit is contained in:
@@ -1,60 +1,42 @@
|
||||
// File: tests/unit/response-code/response-code.service.spec.ts
|
||||
// Unit tests สำหรับ ResponseCodeService (T074)
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ResponseCodeService } from '../../../src/modules/response-code/response-code.service';
|
||||
import { ResponseCode } from '../../../src/modules/response-code/entities/response-code.entity';
|
||||
import { ResponseCodeRule } from '../../../src/modules/response-code/entities/response-code-rule.entity';
|
||||
import { ResponseCodeCategory } from '../../../src/modules/common/enums/review.enums';
|
||||
import { BadRequestException, ConflictException } from '@nestjs/common';
|
||||
|
||||
const mockCode: Partial<ResponseCode> = {
|
||||
id: 1,
|
||||
publicId: 'test-uuid-1',
|
||||
code: '1A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
descriptionTh: 'ผ่าน — ไม่มีเงื่อนไข',
|
||||
descriptionEn: 'Approved — No Comments',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
};
|
||||
|
||||
const mockCodeRepo = {
|
||||
find: jest.fn().mockResolvedValue([mockCode]),
|
||||
findOne: jest.fn().mockResolvedValue(mockCode),
|
||||
create: jest.fn(
|
||||
(payload: Partial<ResponseCode>): Partial<ResponseCode> => payload
|
||||
),
|
||||
save: jest.fn(
|
||||
(payload: Partial<ResponseCode>): Promise<Partial<ResponseCode>> =>
|
||||
Promise.resolve(payload)
|
||||
),
|
||||
};
|
||||
|
||||
const mockRuleRepo = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
import {
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { CreateResponseCodeDto } from '../../../src/modules/response-code/dto/create-response-code.dto';
|
||||
|
||||
describe('ResponseCodeService', () => {
|
||||
let service: ResponseCodeService;
|
||||
let repo: Repository<ResponseCode>;
|
||||
let _ruleRepo: Repository<ResponseCodeRule>;
|
||||
|
||||
const mockRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRuleRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
mockCodeRepo.find.mockResolvedValue([mockCode]);
|
||||
mockCodeRepo.findOne.mockResolvedValue(mockCode);
|
||||
mockCodeRepo.create.mockImplementation(
|
||||
(payload: Partial<ResponseCode>): Partial<ResponseCode> => payload
|
||||
);
|
||||
mockCodeRepo.save.mockImplementation(
|
||||
(payload: Partial<ResponseCode>): Promise<Partial<ResponseCode>> =>
|
||||
Promise.resolve(payload)
|
||||
);
|
||||
mockRuleRepo.find.mockResolvedValue([]);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ResponseCodeService,
|
||||
{ provide: getRepositoryToken(ResponseCode), useValue: mockCodeRepo },
|
||||
{
|
||||
provide: getRepositoryToken(ResponseCode),
|
||||
useValue: mockRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ResponseCodeRule),
|
||||
useValue: mockRuleRepo,
|
||||
@@ -63,100 +45,209 @@ describe('ResponseCodeService', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<ResponseCodeService>(ResponseCodeService);
|
||||
repo = module.get<Repository<ResponseCode>>(
|
||||
getRepositoryToken(ResponseCode)
|
||||
);
|
||||
_ruleRepo = module.get<Repository<ResponseCodeRule>>(
|
||||
getRepositoryToken(ResponseCodeRule)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all active codes', async () => {
|
||||
const mockCodes = [{ code: '1A', isActive: true }];
|
||||
mockRepo.find.mockResolvedValue(mockCodes);
|
||||
const result = await service.findAll();
|
||||
expect(result).toEqual(mockCodes);
|
||||
expect(repo.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { isActive: true } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByCategory', () => {
|
||||
it('should return codes filtered by category', async () => {
|
||||
it('should filter by category', async () => {
|
||||
const mockCodes = [
|
||||
{ code: '1A', category: ResponseCodeCategory.ENGINEERING },
|
||||
];
|
||||
mockRepo.find.mockResolvedValue(mockCodes);
|
||||
const result = await service.findByCategory(
|
||||
ResponseCodeCategory.ENGINEERING
|
||||
);
|
||||
expect(mockCodeRepo.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(result).toEqual([mockCode]);
|
||||
expect(result).toEqual(mockCodes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByDocumentType', () => {
|
||||
it('should return enabled codes for document type', async () => {
|
||||
const result = await service.findByDocumentType(1, 1);
|
||||
expect(result).toBeDefined();
|
||||
it('should handle global and project rules with overrides and sorting', async () => {
|
||||
const globalRule1 = {
|
||||
responseCodeId: 2,
|
||||
projectId: null,
|
||||
responseCode: { id: 2, code: '2', isActive: true },
|
||||
};
|
||||
const globalRule2 = {
|
||||
responseCodeId: 1,
|
||||
projectId: null,
|
||||
responseCode: { id: 1, code: '1A', isActive: true },
|
||||
};
|
||||
const projectRule = {
|
||||
responseCodeId: 1,
|
||||
projectId: 10,
|
||||
responseCode: { id: 1, code: '1A_OVERRIDE', isActive: true },
|
||||
};
|
||||
|
||||
mockRuleRepo.find.mockResolvedValue([
|
||||
globalRule1,
|
||||
globalRule2,
|
||||
projectRule,
|
||||
]);
|
||||
|
||||
const result = await service.findByDocumentType(1, 10);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].code).toBe('1A_OVERRIDE');
|
||||
expect(result[1].code).toBe('2');
|
||||
});
|
||||
|
||||
it('should ignore inactive codes from rules', async () => {
|
||||
const rule = {
|
||||
responseCodeId: 1,
|
||||
responseCode: { id: 1, code: '1A', isActive: false },
|
||||
};
|
||||
mockRuleRepo.find.mockResolvedValue([rule]);
|
||||
const result = await service.findByDocumentType(1);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByPublicId', () => {
|
||||
it('should throw NotFoundException if not found', async () => {
|
||||
mockRepo.findOne.mockResolvedValue(null);
|
||||
await expect(service.findByPublicId('none')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should return code if found', async () => {
|
||||
const mockCode = { publicId: 'uuid' };
|
||||
mockRepo.findOne.mockResolvedValue(mockCode);
|
||||
const result = await service.findByPublicId('uuid');
|
||||
expect(result).toEqual(mockCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a non-system response code when code/category is unique', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await service.create({
|
||||
code: '9A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
descriptionTh: 'ทดสอบ',
|
||||
descriptionEn: 'Test',
|
||||
});
|
||||
|
||||
expect(mockCodeRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: '9A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
code: '9A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
isSystem: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject duplicate code/category pairs', async () => {
|
||||
it('should throw ConflictException if already exists', async () => {
|
||||
mockRepo.findOne.mockResolvedValue({ id: 1 });
|
||||
await expect(
|
||||
service.create({
|
||||
code: '1A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
descriptionTh: 'ซ้ำ',
|
||||
descriptionEn: 'Duplicate',
|
||||
})
|
||||
).rejects.toBeInstanceOf(ConflictException);
|
||||
} as unknown as CreateResponseCodeDto)
|
||||
).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it('should create and save new code', async () => {
|
||||
mockRepo.findOne.mockResolvedValue(null);
|
||||
mockRepo.create.mockReturnValue({ code: '1A' });
|
||||
mockRepo.save.mockResolvedValue({ id: 1, code: '1A' });
|
||||
|
||||
const result = await service.create({
|
||||
code: '1A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
isActive: true,
|
||||
} as unknown as CreateResponseCodeDto);
|
||||
expect(result.code).toBe('1A');
|
||||
expect(repo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an existing response code by publicId', async () => {
|
||||
const result = await service.update('test-uuid-1', {
|
||||
descriptionEn: 'Updated Description',
|
||||
});
|
||||
it('should throw ConflictException if update creates a duplicate', async () => {
|
||||
const existing = {
|
||||
id: 1,
|
||||
publicId: 'uuid1',
|
||||
code: '1A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
};
|
||||
const duplicate = {
|
||||
id: 2,
|
||||
publicId: 'uuid2',
|
||||
code: '1B',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
};
|
||||
|
||||
expect(mockCodeRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
publicId: 'test-uuid-1',
|
||||
descriptionEn: 'Updated Description',
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
descriptionEn: 'Updated Description',
|
||||
})
|
||||
mockRepo.findOne.mockResolvedValueOnce(existing); // findByPublicId
|
||||
mockRepo.findOne.mockResolvedValueOnce(duplicate); // check existing duplicate
|
||||
|
||||
await expect(service.update('uuid1', { code: '1B' })).rejects.toThrow(
|
||||
ConflictException
|
||||
);
|
||||
});
|
||||
|
||||
it('should update and save when no duplicate exists', async () => {
|
||||
const existing = { id: 1, publicId: 'uuid1', code: '1A' };
|
||||
mockRepo.findOne.mockResolvedValueOnce(existing);
|
||||
mockRepo.findOne.mockResolvedValueOnce(null); // No duplicate
|
||||
mockRepo.save.mockImplementation((d) => Promise.resolve(d));
|
||||
|
||||
const result = await service.update('uuid1', { descriptionEn: 'New' });
|
||||
expect(result.descriptionEn).toBe('New');
|
||||
});
|
||||
|
||||
it('should handle update with same code and category (self-match)', async () => {
|
||||
const existing = {
|
||||
id: 1,
|
||||
publicId: 'uuid1',
|
||||
code: '1A',
|
||||
category: ResponseCodeCategory.ENGINEERING,
|
||||
};
|
||||
mockRepo.findOne.mockResolvedValueOnce(existing); // findByPublicId
|
||||
mockRepo.findOne.mockResolvedValueOnce(existing); // self match in check existing
|
||||
mockRepo.save.mockImplementation((d) => Promise.resolve(d));
|
||||
|
||||
const result = await service.update('uuid1', { descriptionEn: 'Same' });
|
||||
expect(result.descriptionEn).toBe('Same');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivate', () => {
|
||||
it('should reject deactivation for system response codes', async () => {
|
||||
await expect(service.deactivate('test-uuid-1')).rejects.toBeInstanceOf(
|
||||
it('should throw BadRequestException for system codes', async () => {
|
||||
mockRepo.findOne.mockResolvedValue({ publicId: 'uuid', isSystem: true });
|
||||
await expect(service.deactivate('uuid')).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
});
|
||||
|
||||
it('should set isActive to false and save', async () => {
|
||||
const entity = { isSystem: false, isActive: true, publicId: 'uuid' };
|
||||
mockRepo.findOne.mockResolvedValue(entity);
|
||||
await service.deactivate('uuid');
|
||||
expect(entity.isActive).toBe(false);
|
||||
expect(repo.save).toHaveBeenCalledWith(entity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotifyRoles', () => {
|
||||
it('should return notifyRoles or empty array', async () => {
|
||||
mockRepo.findOne.mockResolvedValueOnce({
|
||||
publicId: 'uuid',
|
||||
notifyRoles: ['PM'],
|
||||
});
|
||||
expect(await service.getNotifyRoles('uuid')).toEqual(['PM']);
|
||||
|
||||
mockRepo.findOne.mockResolvedValueOnce({
|
||||
publicId: 'uuid',
|
||||
notifyRoles: null,
|
||||
});
|
||||
expect(await service.getNotifyRoles('uuid')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// File: tests/unit/review-team/aggregate-status.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AggregateStatusService } from '../../../src/modules/review-team/services/aggregate-status.service';
|
||||
import { ReviewTask } from '../../../src/modules/review-team/entities/review-task.entity';
|
||||
import {
|
||||
ReviewTaskStatus,
|
||||
ConsensusDecision,
|
||||
} from '../../../src/modules/common/enums/review.enums';
|
||||
|
||||
describe('AggregateStatusService', () => {
|
||||
let service: AggregateStatusService;
|
||||
let _taskRepo: Repository<ReviewTask>;
|
||||
|
||||
const mockTaskRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AggregateStatusService,
|
||||
{
|
||||
provide: getRepositoryToken(ReviewTask),
|
||||
useValue: mockTaskRepo,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AggregateStatusService>(AggregateStatusService);
|
||||
_taskRepo = module.get<Repository<ReviewTask>>(
|
||||
getRepositoryToken(ReviewTask)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getForRevision', () => {
|
||||
it('should return 0 status if no tasks exist', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
const result = await service.getForRevision(1);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.completionPct).toBe(0);
|
||||
expect(result.isAllComplete).toBe(false);
|
||||
});
|
||||
|
||||
it('should calculate counts correctly', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.PENDING },
|
||||
{ status: ReviewTaskStatus.IN_PROGRESS },
|
||||
{ status: ReviewTaskStatus.DELEGATED },
|
||||
{ status: ReviewTaskStatus.EXPIRED },
|
||||
]);
|
||||
|
||||
const result = await service.getForRevision(1);
|
||||
|
||||
expect(result.total).toBe(6);
|
||||
expect(result.completed).toBe(2);
|
||||
expect(result.pending).toBe(1);
|
||||
expect(result.inProgress).toBe(1);
|
||||
expect(result.delegated).toBe(1);
|
||||
expect(result.expired).toBe(1);
|
||||
expect(result.completionPct).toBe(33);
|
||||
expect(result.isAllComplete).toBe(false);
|
||||
expect(result.hasExpired).toBe(true);
|
||||
});
|
||||
|
||||
it('should return isAllComplete true if all tasks are COMPLETED', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
]);
|
||||
|
||||
const result = await service.getForRevision(1);
|
||||
expect(result.isAllComplete).toBe(true);
|
||||
expect(result.completionPct).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReadyForConsensus', () => {
|
||||
it('should return true if all complete', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.COMPLETED },
|
||||
]);
|
||||
expect(await service.isReadyForConsensus(1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if not all complete', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ status: ReviewTaskStatus.PENDING },
|
||||
]);
|
||||
expect(await service.isReadyForConsensus(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateConsensus', () => {
|
||||
it('should return PENDING if no completed tasks', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.PENDING
|
||||
);
|
||||
});
|
||||
|
||||
it('should return REJECTED if any Code 3 exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '3' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.REJECTED
|
||||
);
|
||||
});
|
||||
|
||||
it('should return APPROVED if all are 1A or 1B', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.APPROVED
|
||||
);
|
||||
});
|
||||
|
||||
it('should return APPROVED_WITH_COMMENTS if any Code 2 exists and no Code 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
]);
|
||||
expect(await service.evaluateConsensus(1)).toBe(
|
||||
ConsensusDecision.APPROVED_WITH_COMMENTS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMostRestrictiveResponseCode', () => {
|
||||
it('should return 1A if no completed tasks', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1A');
|
||||
});
|
||||
|
||||
it('should return 3 if any Code 3 exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
{ responseCode: { code: '3' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('3');
|
||||
});
|
||||
|
||||
it('should return 2 if Code 2 exists and no Code 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
{ responseCode: { code: '2' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('2');
|
||||
});
|
||||
|
||||
it('should return 1B if Code 1B exists and no Code 2 or 3', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ responseCode: { code: '1A' } },
|
||||
{ responseCode: { code: '1B' } },
|
||||
]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1B');
|
||||
});
|
||||
|
||||
it('should return 1A if only Code 1A exists', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([{ responseCode: { code: '1A' } }]);
|
||||
expect(await service.getMostRestrictiveResponseCode(1)).toBe('1A');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,7 @@ describe('TaskCreationService delegation resolution', () => {
|
||||
|
||||
const tasks = await service.createParallelTasks(
|
||||
100,
|
||||
'rfa-public-id',
|
||||
team.publicId,
|
||||
new Date('2026-05-20T00:00:00.000Z'),
|
||||
manager as unknown as EntityManager
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// File: tests/unit/review-team/veto-override.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import {
|
||||
VetoOverrideService,
|
||||
VetoOverrideDto,
|
||||
} from '../../../src/modules/review-team/services/veto-override.service';
|
||||
import { ReviewTask } from '../../../src/modules/review-team/entities/review-task.entity';
|
||||
import { ApprovalListenerService } from '../../../src/modules/distribution/services/approval-listener.service';
|
||||
import { ConsensusDecision } from '../../../src/modules/common/enums/review.enums';
|
||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
describe('VetoOverrideService', () => {
|
||||
let service: VetoOverrideService;
|
||||
let _taskRepo: Repository<ReviewTask>;
|
||||
let approvalListenerService: ApprovalListenerService;
|
||||
|
||||
const mockTaskRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockApprovalListenerService = {
|
||||
onConsensusReached: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDataSource = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
VetoOverrideService,
|
||||
{
|
||||
provide: getRepositoryToken(ReviewTask),
|
||||
useValue: mockTaskRepo,
|
||||
},
|
||||
{
|
||||
provide: ApprovalListenerService,
|
||||
useValue: mockApprovalListenerService,
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<VetoOverrideService>(VetoOverrideService);
|
||||
_taskRepo = module.get<Repository<ReviewTask>>(
|
||||
getRepositoryToken(ReviewTask)
|
||||
);
|
||||
approvalListenerService = module.get<ApprovalListenerService>(
|
||||
ApprovalListenerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('executeOverride', () => {
|
||||
const validDto: VetoOverrideDto = {
|
||||
rfaRevisionId: 1,
|
||||
rfaPublicId: 'rfa-uuid',
|
||||
rfaRevisionPublicId: 'rev-uuid',
|
||||
projectId: 10,
|
||||
documentTypeCode: 'SD',
|
||||
overrideReason: 'This is a valid justification for override.',
|
||||
overriddenByUserId: 1,
|
||||
};
|
||||
|
||||
it('should throw NotFoundException if no tasks found', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([]);
|
||||
await expect(service.executeOverride(validDto)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if no Code 3 veto found', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '1A' } },
|
||||
{ id: 2, responseCode: { code: '2' } },
|
||||
]);
|
||||
await expect(service.executeOverride(validDto)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if reason is too short', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '3' } },
|
||||
]);
|
||||
const shortDto = { ...validDto, overrideReason: 'Too short' };
|
||||
await expect(service.executeOverride(shortDto)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully execute override and call approval listener', async () => {
|
||||
mockTaskRepo.find.mockResolvedValue([
|
||||
{ id: 1, responseCode: { code: '3' } },
|
||||
]);
|
||||
|
||||
const result = await service.executeOverride(validDto);
|
||||
|
||||
expect(result.decision).toBe(ConsensusDecision.OVERRIDDEN);
|
||||
expect(approvalListenerService.onConsensusReached).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rfaPublicId: validDto.rfaPublicId,
|
||||
decision: ConsensusDecision.OVERRIDDEN,
|
||||
responseCode: '1A',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user