690525:2327 ADR-023-229 dynamic prompt #01
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// File: backend/src/modules/ai/prompts/ai-prompts.service.spec.ts
|
||||
// Change Log
|
||||
// - 2026-05-25: Created unit tests for AiPromptsService (T028)
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AiPromptsService } from './ai-prompts.service';
|
||||
import { AiPrompt } from './ai-prompts.entity';
|
||||
import { AuditLog } from '../../../common/entities/audit-log.entity';
|
||||
import {
|
||||
BusinessException,
|
||||
ValidationException,
|
||||
NotFoundException,
|
||||
} from '../../../common/exceptions';
|
||||
|
||||
describe('AiPromptsService', () => {
|
||||
let service: AiPromptsService;
|
||||
const mockAiPromptRepo = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
const mockAuditLogRepo = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
const mockRedis = {
|
||||
get: jest.fn(),
|
||||
setex: jest.fn(),
|
||||
del: jest.fn(),
|
||||
};
|
||||
const mockQueryBuilder = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
setLock: jest.fn().mockReturnThis(),
|
||||
getRawOne: jest.fn(),
|
||||
};
|
||||
const mockQueryRunner = {
|
||||
connect: jest.fn(),
|
||||
startTransaction: jest.fn(),
|
||||
commitTransaction: jest.fn(),
|
||||
rollbackTransaction: jest.fn(),
|
||||
release: jest.fn(),
|
||||
manager: {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
};
|
||||
const mockDataSource = {
|
||||
createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner),
|
||||
};
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AiPromptsService,
|
||||
{
|
||||
provide: getRepositoryToken(AiPrompt),
|
||||
useValue: mockAiPromptRepo,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AuditLog),
|
||||
useValue: mockAuditLogRepo,
|
||||
},
|
||||
{
|
||||
provide: 'default_IORedisModuleConnectionToken',
|
||||
useValue: mockRedis,
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<AiPromptsService>(AiPromptsService);
|
||||
});
|
||||
describe('create', () => {
|
||||
it('ควรปฏิเสธ template ที่ไม่มี {{ocr_text}} placeholder', async () => {
|
||||
await expect(
|
||||
service.create(
|
||||
'ocr_extraction',
|
||||
{ template: 'Invalid prompt structure' },
|
||||
1
|
||||
)
|
||||
).rejects.toThrow(ValidationException);
|
||||
});
|
||||
it('ควรปฏิเสธ template ที่ตัวอักษรเกิน 4,000 ตัว', async () => {
|
||||
const longTemplate = 'a'.repeat(4005) + '{{ocr_text}}';
|
||||
await expect(
|
||||
service.create('ocr_extraction', { template: longTemplate }, 1)
|
||||
).rejects.toThrow(ValidationException);
|
||||
});
|
||||
it('ควรบันทึกสำเร็จและรัน version number ต่อเนื่อง', async () => {
|
||||
mockQueryBuilder.getRawOne.mockResolvedValue({ max: 5 });
|
||||
mockAiPromptRepo.create.mockReturnValue({
|
||||
id: 12,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 6,
|
||||
template: 'Test {{ocr_text}}',
|
||||
isActive: false,
|
||||
});
|
||||
mockQueryRunner.manager.save.mockResolvedValue({
|
||||
id: 12,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 6,
|
||||
template: 'Test {{ocr_text}}',
|
||||
isActive: false,
|
||||
});
|
||||
const result = await service.create(
|
||||
'ocr_extraction',
|
||||
{ template: 'Test {{ocr_text}}' },
|
||||
1
|
||||
);
|
||||
expect(result.versionNumber).toBe(6);
|
||||
expect(mockQueryRunner.manager.save).toHaveBeenCalled();
|
||||
expect(mockAuditLogRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('activate', () => {
|
||||
it('ควร activate สำเร็จ ยกเลิกตัวอื่น และลบ cache', async () => {
|
||||
const activePrompt = {
|
||||
id: 1,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 1,
|
||||
isActive: true,
|
||||
};
|
||||
const targetPrompt = {
|
||||
id: 2,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 2,
|
||||
isActive: false,
|
||||
};
|
||||
mockQueryRunner.manager.findOne.mockResolvedValue(targetPrompt);
|
||||
mockQueryRunner.manager.find.mockResolvedValue([activePrompt]);
|
||||
mockQueryRunner.manager.save.mockResolvedValue({
|
||||
...targetPrompt,
|
||||
isActive: true,
|
||||
});
|
||||
const result = await service.activate('ocr_extraction', 2, 1);
|
||||
expect(result.isActive).toBe(true);
|
||||
expect(mockQueryRunner.manager.update).toHaveBeenCalledWith(
|
||||
AiPrompt,
|
||||
{ promptType: 'ocr_extraction', isActive: true },
|
||||
{ isActive: false }
|
||||
);
|
||||
expect(mockRedis.del).toHaveBeenCalledWith(
|
||||
'ai:prompt:active:ocr_extraction'
|
||||
);
|
||||
expect(mockAuditLogRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
it('ควร throw error เมื่อไม่พบ prompt version ที่ต้องการ activate', async () => {
|
||||
mockQueryRunner.manager.findOne.mockResolvedValue(null);
|
||||
await expect(service.activate('ocr_extraction', 99, 1)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('delete', () => {
|
||||
it('ควร throw error เมื่อลบ active version', async () => {
|
||||
mockAiPromptRepo.findOne.mockResolvedValue({
|
||||
id: 1,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 1,
|
||||
isActive: true,
|
||||
});
|
||||
await expect(service.delete('ocr_extraction', 1, 1)).rejects.toThrow(
|
||||
BusinessException
|
||||
);
|
||||
});
|
||||
it('ควรลบ inactive version สำเร็จและบันทึก audit log', async () => {
|
||||
const inactivePrompt = {
|
||||
id: 2,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 2,
|
||||
isActive: false,
|
||||
};
|
||||
mockAiPromptRepo.findOne.mockResolvedValue(inactivePrompt);
|
||||
await service.delete('ocr_extraction', 2, 1);
|
||||
expect(mockAiPromptRepo.remove).toHaveBeenCalledWith(inactivePrompt);
|
||||
expect(mockAuditLogRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('getActive', () => {
|
||||
it('ควรดึงจาก Redis cache เมื่อมี cache hit', async () => {
|
||||
const cachedPrompt = {
|
||||
id: 1,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 1,
|
||||
isActive: true,
|
||||
};
|
||||
mockRedis.get.mockResolvedValue(JSON.stringify(cachedPrompt));
|
||||
const result = await service.getActive('ocr_extraction');
|
||||
expect(result).toEqual(cachedPrompt);
|
||||
expect(mockAiPromptRepo.findOne).not.toHaveBeenCalled();
|
||||
});
|
||||
it('ควร fallback ไปหา DB เมื่อ Redis มีปัญหา', async () => {
|
||||
const dbPrompt = {
|
||||
id: 1,
|
||||
promptType: 'ocr_extraction',
|
||||
versionNumber: 1,
|
||||
isActive: true,
|
||||
};
|
||||
mockRedis.get.mockRejectedValue(new Error('Redis connection lost'));
|
||||
mockAiPromptRepo.findOne.mockResolvedValue(dbPrompt);
|
||||
const result = await service.getActive('ocr_extraction');
|
||||
expect(result).toEqual(dbPrompt);
|
||||
expect(mockAiPromptRepo.findOne).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user