690522:0554 227 #01
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// File: src/modules/response-code/services/audit.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ ResponseCodeAuditService
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ResponseCodeAuditService } from './audit.service';
|
||||
import { AuditLog } from '../../../common/entities/audit-log.entity';
|
||||
|
||||
describe('ResponseCodeAuditService', () => {
|
||||
let service: ResponseCodeAuditService;
|
||||
const mockAuditLog: Partial<AuditLog> = {
|
||||
userId: 1,
|
||||
action: 'response_code.change',
|
||||
severity: 'INFO',
|
||||
entityType: 'review_task',
|
||||
entityId: 'task-uuid-001',
|
||||
detailsJson: {},
|
||||
};
|
||||
const mockAuditLogRepo = {
|
||||
create: jest.fn().mockReturnValue(mockAuditLog),
|
||||
save: jest.fn().mockResolvedValue(mockAuditLog),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ResponseCodeAuditService,
|
||||
{
|
||||
provide: getRepositoryToken(AuditLog),
|
||||
useValue: mockAuditLogRepo,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<ResponseCodeAuditService>(ResponseCodeAuditService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('logReviewTaskResponseCodeChange', () => {
|
||||
it('ควรบันทึก audit log พร้อมข้อมูลครบถ้วน (Happy Path)', async () => {
|
||||
await service.logReviewTaskResponseCodeChange({
|
||||
reviewTaskPublicId: 'task-uuid-001',
|
||||
responseCodePublicId: 'rc-uuid-001',
|
||||
previousResponseCodeId: 1,
|
||||
currentResponseCodeId: 2,
|
||||
comments: 'Changed from 1A to 2',
|
||||
userId: 10,
|
||||
});
|
||||
expect(mockAuditLogRepo.create).toHaveBeenCalledWith({
|
||||
userId: 10,
|
||||
action: 'response_code.change',
|
||||
severity: 'INFO',
|
||||
entityType: 'review_task',
|
||||
entityId: 'task-uuid-001',
|
||||
detailsJson: {
|
||||
previousResponseCodeId: 1,
|
||||
currentResponseCodeId: 2,
|
||||
responseCodePublicId: 'rc-uuid-001',
|
||||
comments: 'Changed from 1A to 2',
|
||||
},
|
||||
});
|
||||
expect(mockAuditLogRepo.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('ควร default userId เป็น null เมื่อไม่ระบุ', async () => {
|
||||
await service.logReviewTaskResponseCodeChange({
|
||||
reviewTaskPublicId: 'task-uuid-002',
|
||||
responseCodePublicId: 'rc-uuid-002',
|
||||
currentResponseCodeId: 3,
|
||||
});
|
||||
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userId: null })
|
||||
);
|
||||
});
|
||||
|
||||
it('ควร default previousResponseCodeId เป็น null เมื่อไม่ระบุ', async () => {
|
||||
await service.logReviewTaskResponseCodeChange({
|
||||
reviewTaskPublicId: 'task-uuid-003',
|
||||
responseCodePublicId: 'rc-uuid-003',
|
||||
currentResponseCodeId: 1,
|
||||
});
|
||||
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
detailsJson: expect.objectContaining({
|
||||
previousResponseCodeId: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('ควร default comments เป็น null เมื่อไม่ระบุ', async () => {
|
||||
await service.logReviewTaskResponseCodeChange({
|
||||
reviewTaskPublicId: 'task-uuid-004',
|
||||
responseCodePublicId: 'rc-uuid-004',
|
||||
currentResponseCodeId: 2,
|
||||
});
|
||||
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
detailsJson: expect.objectContaining({ comments: null }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('ควร throw เมื่อ repo.save ล้มเหลว', async () => {
|
||||
mockAuditLogRepo.save.mockRejectedValueOnce(new Error('DB Error'));
|
||||
await expect(
|
||||
service.logReviewTaskResponseCodeChange({
|
||||
reviewTaskPublicId: 'task-uuid-005',
|
||||
responseCodePublicId: 'rc-uuid-005',
|
||||
currentResponseCodeId: 1,
|
||||
})
|
||||
).rejects.toThrow('DB Error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
// File: src/modules/response-code/services/implications.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ ImplicationsService (FR-007)
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ImplicationsService } from './implications.service';
|
||||
import { ResponseCode } from '../entities/response-code.entity';
|
||||
|
||||
// Helper สร้าง mock ResponseCode
|
||||
const makeCode = (
|
||||
code: string,
|
||||
overrides: Partial<ResponseCode> = {}
|
||||
): ResponseCode =>
|
||||
({
|
||||
id: 1,
|
||||
code,
|
||||
descriptionTh: 'ทดสอบ',
|
||||
descriptionEn: 'Test',
|
||||
category: 'ENGINEERING',
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
implications: {},
|
||||
notifyRoles: [],
|
||||
...overrides,
|
||||
}) as unknown as ResponseCode;
|
||||
|
||||
describe('ImplicationsService', () => {
|
||||
let service: ImplicationsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ImplicationsService],
|
||||
}).compile();
|
||||
service = module.get<ImplicationsService>(ImplicationsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('evaluate — severity', () => {
|
||||
it('ควรคืน CRITICAL เมื่อ code=3 (Rejected)', () => {
|
||||
const result = service.evaluate(makeCode('3'));
|
||||
expect(result.severity).toBe('CRITICAL');
|
||||
});
|
||||
|
||||
it('ควรคืน HIGH เมื่อ code=1C', () => {
|
||||
const result = service.evaluate(makeCode('1C'));
|
||||
expect(result.severity).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('ควรคืน HIGH เมื่อ code=1D', () => {
|
||||
const result = service.evaluate(makeCode('1D'));
|
||||
expect(result.severity).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('ควรคืน HIGH เมื่อ affectsSchedule=true และ affectsCost=true', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('2', {
|
||||
implications: { affectsSchedule: true, affectsCost: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.severity).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('ควรคืน MEDIUM เมื่อ requiresContractReview=true', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { requiresContractReview: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.severity).toBe('MEDIUM');
|
||||
});
|
||||
|
||||
it('ควรคืน MEDIUM เมื่อ affectsSchedule=true', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { affectsSchedule: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.severity).toBe('MEDIUM');
|
||||
});
|
||||
|
||||
it('ควรคืน MEDIUM เมื่อ affectsCost=true', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { affectsCost: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.severity).toBe('MEDIUM');
|
||||
});
|
||||
|
||||
it('ควรคืน LOW เมื่อไม่มีผลกระทบใดๆ', () => {
|
||||
const result = service.evaluate(makeCode('1A'));
|
||||
expect(result.severity).toBe('LOW');
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluate — actionRequired', () => {
|
||||
it('ควรเพิ่ม action สำหรับ code=3', () => {
|
||||
const result = service.evaluate(makeCode('3'));
|
||||
expect(result.actionRequired).toContain(
|
||||
'Document rejected — originator must revise and resubmit'
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรเพิ่ม action สำหรับ requiresContractReview', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1C', {
|
||||
implications: { requiresContractReview: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.actionRequired).toContain(
|
||||
'Contract review required — notify Contract Manager'
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรเพิ่ม action สำหรับ affectsCost', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { affectsCost: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.actionRequired).toContain(
|
||||
'Cost impact assessment required — notify QS Manager'
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรเพิ่ม action สำหรับ requiresEiaAmendment', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { requiresEiaAmendment: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.actionRequired).toContain(
|
||||
'EIA amendment may be required — notify EIA Officer'
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรเพิ่ม action สำหรับ code=2', () => {
|
||||
const result = service.evaluate(makeCode('2'));
|
||||
expect(result.actionRequired).toContain(
|
||||
'Minor comments — originator to revise and resubmit'
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรคืน actionRequired ว่างเมื่อ code=1A ไม่มี implications', () => {
|
||||
const result = service.evaluate(makeCode('1A'));
|
||||
expect(result.actionRequired).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluate — flags', () => {
|
||||
it('ควรคืน affectsSchedule=true จาก implications', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('1B', {
|
||||
implications: { affectsSchedule: true },
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.affectsSchedule).toBe(true);
|
||||
});
|
||||
|
||||
it('ควร default ทุก flag เป็น false เมื่อ implications ว่าง', () => {
|
||||
const result = service.evaluate(makeCode('1A'));
|
||||
expect(result.affectsSchedule).toBe(false);
|
||||
expect(result.affectsCost).toBe(false);
|
||||
expect(result.requiresContractReview).toBe(false);
|
||||
expect(result.requiresEiaAmendment).toBe(false);
|
||||
});
|
||||
|
||||
it('ควรคืน notifyRoles จาก responseCode', () => {
|
||||
const result = service.evaluate(
|
||||
makeCode('3', {
|
||||
notifyRoles: ['CONTRACT_MANAGER', 'QS_MANAGER'],
|
||||
} as Partial<ResponseCode>)
|
||||
);
|
||||
expect(result.notifyRoles).toEqual(['CONTRACT_MANAGER', 'QS_MANAGER']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// File: src/modules/response-code/services/inheritance.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ InheritanceService (T062, FR-021)
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { InheritanceService } from './inheritance.service';
|
||||
import { ResponseCodeRule } from '../entities/response-code-rule.entity';
|
||||
|
||||
// Helper สร้าง mock rule
|
||||
const makeRule = (
|
||||
id: number,
|
||||
responseCodeId: number,
|
||||
publicId: string,
|
||||
projectId?: number,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Partial<ResponseCodeRule> => ({
|
||||
id,
|
||||
responseCodeId,
|
||||
documentTypeId: 1,
|
||||
projectId,
|
||||
isEnabled: true,
|
||||
requiresComments: false,
|
||||
triggersNotification: false,
|
||||
responseCode: { publicId } as unknown as ResponseCodeRule['responseCode'],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('InheritanceService', () => {
|
||||
let service: InheritanceService;
|
||||
const mockRuleRepo = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
InheritanceService,
|
||||
{
|
||||
provide: getRepositoryToken(ResponseCodeRule),
|
||||
useValue: mockRuleRepo,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<InheritanceService>(InheritanceService);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('resolveMatrix — global only', () => {
|
||||
it('ควรคืน global rules เมื่อไม่ระบุ projectId', async () => {
|
||||
const globalRules = [makeRule(1, 10, 'rc-1A'), makeRule(2, 20, 'rc-2')];
|
||||
mockRuleRepo.find.mockResolvedValueOnce(globalRules);
|
||||
const result = await service.resolveMatrix(1);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].isOverridden).toBe(false);
|
||||
expect(result[0].responseCodePublicId).toBe('rc-1A');
|
||||
});
|
||||
|
||||
it('ควรคืน array ว่างเมื่อไม่มี global rules', async () => {
|
||||
mockRuleRepo.find.mockResolvedValueOnce([]);
|
||||
const result = await service.resolveMatrix(99);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMatrix — with project overrides', () => {
|
||||
it('ควร merge: project rule ชนะ global rule ของ responseCode เดียวกัน', async () => {
|
||||
const globalRules = [makeRule(1, 10, 'rc-1A')];
|
||||
const projectRules = [
|
||||
makeRule(2, 10, 'rc-1A-override', 5, {
|
||||
isEnabled: false,
|
||||
requiresComments: true,
|
||||
}),
|
||||
];
|
||||
// เรียก find สองครั้ง: global, project
|
||||
mockRuleRepo.find
|
||||
.mockResolvedValueOnce(globalRules)
|
||||
.mockResolvedValueOnce(projectRules);
|
||||
const result = await service.resolveMatrix(1, 5);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].isOverridden).toBe(true);
|
||||
expect(result[0].isEnabled).toBe(false);
|
||||
expect(result[0].requiresComments).toBe(true);
|
||||
expect(result[0].parentRuleId).toBe(1); // global rule id
|
||||
});
|
||||
|
||||
it('ควรใช้ global rule เมื่อ project ไม่ override', async () => {
|
||||
const globalRules = [makeRule(1, 10, 'rc-1A'), makeRule(2, 20, 'rc-2')];
|
||||
const projectRules: Partial<ResponseCodeRule>[] = []; // ไม่มี override
|
||||
mockRuleRepo.find
|
||||
.mockResolvedValueOnce(globalRules)
|
||||
.mockResolvedValueOnce(projectRules);
|
||||
const result = await service.resolveMatrix(1, 5);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].isOverridden).toBe(false);
|
||||
expect(result[0].parentRuleId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ควรเพิ่ม project-only rule ที่ไม่มี global parent', async () => {
|
||||
const globalRules = [makeRule(1, 10, 'rc-1A')];
|
||||
const projectRules = [
|
||||
makeRule(1, 10, 'rc-1A'), // overlap กับ global
|
||||
makeRule(3, 30, 'rc-extra', 5), // project-only (responseCodeId=30 ไม่มีใน global)
|
||||
];
|
||||
mockRuleRepo.find
|
||||
.mockResolvedValueOnce(globalRules)
|
||||
.mockResolvedValueOnce(projectRules);
|
||||
const result = await service.resolveMatrix(1, 5);
|
||||
// 1 merged + 1 project-only = 2
|
||||
expect(result).toHaveLength(2);
|
||||
const extra = result.find((r) => r.responseCodeId === 30);
|
||||
expect(extra).toBeDefined();
|
||||
expect(extra?.isOverridden).toBe(true);
|
||||
expect(extra?.parentRuleId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
// File: src/modules/response-code/services/matrix-management.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ MatrixManagementService (T061, FR-022)
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { MatrixManagementService } from './matrix-management.service';
|
||||
import { ResponseCodeRule } from '../entities/response-code-rule.entity';
|
||||
import { ResponseCode } from '../entities/response-code.entity';
|
||||
|
||||
const mockCode = {
|
||||
id: 1,
|
||||
publicId: 'rc-uuid-1A',
|
||||
code: '1A',
|
||||
isSystem: false,
|
||||
};
|
||||
const mockSystemCode = { id: 2, publicId: 'rc-sys', code: '0', isSystem: true };
|
||||
const mockExistingRule = {
|
||||
id: 10,
|
||||
publicId: 'rule-uuid-001',
|
||||
documentTypeId: 1,
|
||||
responseCodeId: 1,
|
||||
projectId: undefined,
|
||||
isEnabled: true,
|
||||
requiresComments: false,
|
||||
triggersNotification: false,
|
||||
};
|
||||
|
||||
describe('MatrixManagementService', () => {
|
||||
let service: MatrixManagementService;
|
||||
const mockRuleRepo = {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
const mockCodeRepo = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MatrixManagementService,
|
||||
{
|
||||
provide: getRepositoryToken(ResponseCodeRule),
|
||||
useValue: mockRuleRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ResponseCode),
|
||||
useValue: mockCodeRepo,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<MatrixManagementService>(MatrixManagementService);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('upsertRule', () => {
|
||||
it('ควร throw NotFoundException เมื่อ ResponseCode ไม่พบ', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
service.upsertRule({
|
||||
documentTypeId: 1,
|
||||
responseCodePublicId: 'not-found',
|
||||
isEnabled: true,
|
||||
})
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('ควร throw BadRequestException เมื่อพยายาม disable system code', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(mockSystemCode);
|
||||
await expect(
|
||||
service.upsertRule({
|
||||
documentTypeId: 1,
|
||||
responseCodePublicId: 'rc-sys',
|
||||
isEnabled: false,
|
||||
})
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('ควรอัปเดต existing rule (isEnabled, requiresComments)', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce({ ...mockExistingRule });
|
||||
mockRuleRepo.save.mockResolvedValueOnce({
|
||||
...mockExistingRule,
|
||||
isEnabled: false,
|
||||
});
|
||||
const result = await service.upsertRule({
|
||||
documentTypeId: 1,
|
||||
responseCodePublicId: 'rc-uuid-1A',
|
||||
isEnabled: false,
|
||||
requiresComments: true,
|
||||
});
|
||||
expect(mockRuleRepo.save).toHaveBeenCalledTimes(1);
|
||||
expect(result.isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('ควรสร้าง rule ใหม่เมื่อยังไม่มี', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce(null); // ไม่มี existing
|
||||
const createdRule = {
|
||||
documentTypeId: 1,
|
||||
responseCodeId: 1,
|
||||
isEnabled: true,
|
||||
requiresComments: false,
|
||||
triggersNotification: false,
|
||||
};
|
||||
mockRuleRepo.create.mockReturnValueOnce(createdRule);
|
||||
mockRuleRepo.save.mockResolvedValueOnce(createdRule);
|
||||
const result = await service.upsertRule({
|
||||
documentTypeId: 1,
|
||||
responseCodePublicId: 'rc-uuid-1A',
|
||||
isEnabled: true,
|
||||
});
|
||||
expect(mockRuleRepo.create).toHaveBeenCalledTimes(1);
|
||||
expect(result.isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('ควร default requiresComments=false และ triggersNotification=false เมื่อสร้างใหม่', async () => {
|
||||
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce(null);
|
||||
mockRuleRepo.create.mockImplementation(
|
||||
(v: Partial<ResponseCodeRule>) => v
|
||||
);
|
||||
mockRuleRepo.save.mockImplementation((v: Partial<ResponseCodeRule>) =>
|
||||
Promise.resolve(v)
|
||||
);
|
||||
const result = await service.upsertRule({
|
||||
documentTypeId: 1,
|
||||
responseCodePublicId: 'rc-uuid-1A',
|
||||
isEnabled: true,
|
||||
});
|
||||
expect(result.requiresComments).toBe(false);
|
||||
expect(result.triggersNotification).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRulesByDocType', () => {
|
||||
it('ควรดึง rules ของ documentType + projectId ที่ระบุ', async () => {
|
||||
mockRuleRepo.find.mockResolvedValueOnce([mockExistingRule]);
|
||||
const result = await service.getRulesByDocType(1, 5);
|
||||
expect(mockRuleRepo.find).toHaveBeenCalledWith({
|
||||
where: { documentTypeId: 1, projectId: 5 },
|
||||
relations: ['responseCode'],
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ควรดึง global rules เมื่อไม่ระบุ projectId', async () => {
|
||||
mockRuleRepo.find.mockResolvedValueOnce([mockExistingRule]);
|
||||
await service.getRulesByDocType(1);
|
||||
expect(mockRuleRepo.find).toHaveBeenCalledWith({
|
||||
where: { documentTypeId: 1, projectId: undefined },
|
||||
relations: ['responseCode'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProjectOverride', () => {
|
||||
it('ควร throw NotFoundException เมื่อ rule ไม่พบ', async () => {
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
service.deleteProjectOverride('nonexistent-rule')
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('ควร throw BadRequestException เมื่อพยายามลบ global rule', async () => {
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce({
|
||||
...mockExistingRule,
|
||||
projectId: undefined,
|
||||
});
|
||||
await expect(
|
||||
service.deleteProjectOverride('rule-uuid-001')
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('ควรลบ project override สำเร็จ', async () => {
|
||||
const projectRule = { ...mockExistingRule, projectId: 5 };
|
||||
mockRuleRepo.findOne.mockResolvedValueOnce(projectRule);
|
||||
mockRuleRepo.remove.mockResolvedValueOnce(undefined);
|
||||
await service.deleteProjectOverride('rule-uuid-001');
|
||||
expect(mockRuleRepo.remove).toHaveBeenCalledWith(projectRule);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
// File: src/modules/response-code/services/notification-trigger.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ NotificationTriggerService (FR-007)
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { NotificationTriggerService } from './notification-trigger.service';
|
||||
import { ResponseCode } from '../entities/response-code.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { NotificationService } from '../../notification/notification.service';
|
||||
import { ImplicationsService } from './implications.service';
|
||||
|
||||
const mockResponseCode = {
|
||||
id: 1,
|
||||
publicId: 'rc-3',
|
||||
code: '3',
|
||||
descriptionEn: 'Rejected',
|
||||
notifyRoles: ['CONTRACT_MANAGER'],
|
||||
implications: {},
|
||||
};
|
||||
|
||||
describe('NotificationTriggerService', () => {
|
||||
let service: NotificationTriggerService;
|
||||
const mockRcRepo = { findOne: jest.fn() };
|
||||
const mockUserRepo = {
|
||||
createQueryBuilder: jest.fn(),
|
||||
};
|
||||
const mockNotificationService = {
|
||||
send: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const mockImplicationsService = { evaluate: jest.fn() };
|
||||
|
||||
// Helper สำหรับ query builder chain
|
||||
const makeQB = (users: Partial<User>[]) => ({
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue(users),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NotificationTriggerService,
|
||||
{ provide: getRepositoryToken(ResponseCode), useValue: mockRcRepo },
|
||||
{ provide: getRepositoryToken(User), useValue: mockUserRepo },
|
||||
{ provide: NotificationService, useValue: mockNotificationService },
|
||||
{ provide: ImplicationsService, useValue: mockImplicationsService },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<NotificationTriggerService>(
|
||||
NotificationTriggerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('triggerIfRequired', () => {
|
||||
it('ควร return ทันทีเมื่อ ResponseCode ไม่พบ (warn, no throw)', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
service.triggerIfRequired('not-found', 'rfa-1', 'DOC-001', 1)
|
||||
).resolves.not.toThrow();
|
||||
expect(mockNotificationService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร return ทันทีเมื่อ severity=LOW', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
|
||||
mockImplicationsService.evaluate.mockReturnValueOnce({
|
||||
severity: 'LOW',
|
||||
notifyRoles: [],
|
||||
actionRequired: [],
|
||||
});
|
||||
await service.triggerIfRequired('rc-3', 'rfa-1', 'DOC-001', 1);
|
||||
expect(mockNotificationService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร return ทันทีเมื่อ notifyRoles ว่าง (severity != LOW)', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
|
||||
mockImplicationsService.evaluate.mockReturnValueOnce({
|
||||
severity: 'CRITICAL',
|
||||
notifyRoles: [],
|
||||
actionRequired: [],
|
||||
});
|
||||
await service.triggerIfRequired('rc-3', 'rfa-1', 'DOC-001', 1);
|
||||
expect(mockNotificationService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควรส่งแจ้งเตือนถึง user ที่มี role ที่เกี่ยวข้อง', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
|
||||
mockImplicationsService.evaluate.mockReturnValueOnce({
|
||||
severity: 'CRITICAL',
|
||||
notifyRoles: ['CONTRACT_MANAGER'],
|
||||
actionRequired: ['Contract review required'],
|
||||
});
|
||||
const targetUser = { user_id: 99 } as User;
|
||||
const qb = makeQB([targetUser]);
|
||||
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
|
||||
await service.triggerIfRequired('rc-3', 'rfa-001', 'DOC-001', 1);
|
||||
expect(mockNotificationService.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockNotificationService.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 99,
|
||||
type: 'SYSTEM',
|
||||
entityType: 'rfa',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('ควรส่งแจ้งเตือนแบบ parallel ถึงหลาย users', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
|
||||
mockImplicationsService.evaluate.mockReturnValueOnce({
|
||||
severity: 'HIGH',
|
||||
notifyRoles: ['CONTRACT_MANAGER'],
|
||||
actionRequired: [],
|
||||
});
|
||||
const users = [{ user_id: 1 }, { user_id: 2 }, { user_id: 3 }] as User[];
|
||||
const qb = makeQB(users);
|
||||
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
|
||||
await service.triggerIfRequired('rc-1C', 'rfa-002', 'DOC-002', 5);
|
||||
expect(mockNotificationService.send).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('ควร return ทันทีเมื่อไม่พบ users ที่ match roles', async () => {
|
||||
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
|
||||
mockImplicationsService.evaluate.mockReturnValueOnce({
|
||||
severity: 'HIGH',
|
||||
notifyRoles: ['CONTRACT_MANAGER'],
|
||||
actionRequired: [],
|
||||
});
|
||||
const qb = makeQB([]); // ไม่มี users
|
||||
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
|
||||
await service.triggerIfRequired('rc-1C', 'rfa-003', 'DOC-003', 5);
|
||||
expect(mockNotificationService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user