690522:0554 227 #01
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
// File: src/modules/json-schema/services/schema-migration.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ SchemaMigrationService
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SchemaMigrationService } from './schema-migration.service';
|
||||
import { JsonSchemaService } from '../json-schema.service';
|
||||
|
||||
// Helper สร้าง mock QueryRunner
|
||||
const makeQueryRunner = () => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
startTransaction: jest.fn().mockResolvedValue(undefined),
|
||||
commitTransaction: jest.fn().mockResolvedValue(undefined),
|
||||
rollbackTransaction: jest.fn().mockResolvedValue(undefined),
|
||||
release: jest.fn().mockResolvedValue(undefined),
|
||||
manager: {
|
||||
query: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
describe('SchemaMigrationService', () => {
|
||||
let service: SchemaMigrationService;
|
||||
let mockQR: ReturnType<typeof makeQueryRunner>;
|
||||
const mockJsonSchemaService = {
|
||||
findOneByCodeAndVersion: jest.fn(),
|
||||
findLatestByCode: jest.fn(),
|
||||
validateData: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockQR = makeQueryRunner();
|
||||
const mockDataSource = {
|
||||
createQueryRunner: jest.fn().mockReturnValue(mockQR),
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SchemaMigrationService,
|
||||
{ provide: DataSource, useValue: mockDataSource },
|
||||
{ provide: JsonSchemaService, useValue: mockJsonSchemaService },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<SchemaMigrationService>(SchemaMigrationService);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('migrateData', () => {
|
||||
const targetSchema = {
|
||||
version: 2,
|
||||
schemaCode: 'RFA_FORM',
|
||||
migrationScript: null,
|
||||
};
|
||||
|
||||
it('ควรคืน success=true ทันทีเมื่อ currentVersion >= targetVersion', async () => {
|
||||
mockJsonSchemaService.findLatestByCode.mockResolvedValueOnce({
|
||||
...targetSchema,
|
||||
version: 1,
|
||||
});
|
||||
mockQR.manager.query.mockResolvedValueOnce([
|
||||
{ details: { title: 'test' }, schema_version: 1 },
|
||||
]);
|
||||
const result = await service.migrateData('rfa_revisions', 1, 'RFA_FORM');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.migratedFields).toHaveLength(0);
|
||||
expect(result.fromVersion).toBe(1);
|
||||
expect(result.toVersion).toBe(1);
|
||||
});
|
||||
|
||||
it('ควร throw NotFoundException เมื่อ entity ไม่พบ', async () => {
|
||||
mockJsonSchemaService.findLatestByCode.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce([]); // ไม่มี record
|
||||
await expect(
|
||||
service.migrateData('rfa_revisions', 999, 'RFA_FORM')
|
||||
).rejects.toThrow();
|
||||
expect(mockQR.rollbackTransaction).toHaveBeenCalled();
|
||||
expect(mockQR.release).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร migrate ข้ามไปยัง targetVersion พร้อม commit', async () => {
|
||||
// Target = v2, current entity = v1
|
||||
mockJsonSchemaService.findLatestByCode.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce([
|
||||
{ details: { title: 'old' }, schema_version: 1 },
|
||||
]);
|
||||
// v2 migration script
|
||||
mockJsonSchemaService.findOneByCodeAndVersion.mockResolvedValueOnce({
|
||||
version: 2,
|
||||
schemaCode: 'RFA_FORM',
|
||||
migrationScript: {
|
||||
steps: [
|
||||
{
|
||||
type: 'FIELD_RENAME',
|
||||
config: { old_field: 'title', new_field: 'subject' },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
mockJsonSchemaService.validateData.mockResolvedValueOnce({
|
||||
isValid: true,
|
||||
sanitizedData: { subject: 'old' },
|
||||
});
|
||||
mockQR.manager.query.mockResolvedValueOnce(undefined); // UPDATE
|
||||
const result = await service.migrateData('rfa_revisions', 1, 'RFA_FORM');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.fromVersion).toBe(1);
|
||||
expect(result.toVersion).toBe(2);
|
||||
expect(result.migratedFields).toContain('subject');
|
||||
expect(mockQR.commitTransaction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร rollback และ throw เมื่อ validation ล้มเหลว', async () => {
|
||||
mockJsonSchemaService.findLatestByCode.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce([
|
||||
{ details: { title: 'old' }, schema_version: 1 },
|
||||
]);
|
||||
mockJsonSchemaService.findOneByCodeAndVersion.mockResolvedValueOnce({
|
||||
version: 2,
|
||||
migrationScript: { steps: [] },
|
||||
});
|
||||
mockJsonSchemaService.validateData.mockResolvedValueOnce({
|
||||
isValid: false,
|
||||
sanitizedData: null,
|
||||
});
|
||||
let error: any;
|
||||
try {
|
||||
await service.migrateData('rfa_revisions', 1, 'RFA_FORM');
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.code).toBe('SCHEMA_MIGRATION_VALIDATION_FAILED');
|
||||
expect(mockQR.rollbackTransaction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควรดึง schema ด้วย version ที่ระบุ เมื่อส่ง targetVersion', async () => {
|
||||
mockJsonSchemaService.findOneByCodeAndVersion.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce([
|
||||
{ details: {}, schema_version: 2 },
|
||||
]); // already up-to-date
|
||||
await service.migrateData('rfa_revisions', 1, 'RFA_FORM', 2);
|
||||
expect(
|
||||
mockJsonSchemaService.findOneByCodeAndVersion
|
||||
).toHaveBeenCalledWith('RFA_FORM', 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyMigrationStep (private — tested via migrateData)', () => {
|
||||
const runStep = async (
|
||||
step: Record<string, unknown>,
|
||||
data: Record<string, unknown>
|
||||
) => {
|
||||
const targetSchema = {
|
||||
version: 2,
|
||||
schemaCode: 'TEST',
|
||||
migrationScript: { steps: [step] },
|
||||
};
|
||||
mockJsonSchemaService.findLatestByCode.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce([
|
||||
{ details: data, schema_version: 1 },
|
||||
]);
|
||||
mockJsonSchemaService.findOneByCodeAndVersion.mockResolvedValueOnce(
|
||||
targetSchema
|
||||
);
|
||||
let capturedData: Record<string, unknown> = {};
|
||||
mockJsonSchemaService.validateData.mockImplementationOnce(
|
||||
(_code: string, d: Record<string, unknown>) => {
|
||||
capturedData = d;
|
||||
return Promise.resolve({ isValid: true, sanitizedData: d });
|
||||
}
|
||||
);
|
||||
mockQR.manager.query.mockResolvedValueOnce(undefined);
|
||||
await service.migrateData('test_table', 1, 'TEST');
|
||||
return capturedData;
|
||||
};
|
||||
|
||||
it('FIELD_RENAME: ควรเปลี่ยนชื่อ field', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_RENAME',
|
||||
config: { old_field: 'title', new_field: 'subject' },
|
||||
},
|
||||
{ title: 'Hello' }
|
||||
);
|
||||
expect(result['subject']).toBe('Hello');
|
||||
expect(result['title']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('FIELD_ADD: ควรเพิ่ม field ด้วย default value', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_ADD',
|
||||
config: { field: 'newField', default_value: 'N/A' },
|
||||
},
|
||||
{ existing: 'data' }
|
||||
);
|
||||
expect(result['newField']).toBe('N/A');
|
||||
});
|
||||
|
||||
it('FIELD_ADD: ควรไม่ overwrite field ที่มีอยู่แล้ว', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_ADD',
|
||||
config: { field: 'existing', default_value: 'N/A' },
|
||||
},
|
||||
{ existing: 'original' }
|
||||
);
|
||||
expect(result['existing']).toBe('original');
|
||||
});
|
||||
|
||||
it('FIELD_REMOVE: ควรลบ field', async () => {
|
||||
const result = await runStep(
|
||||
{ type: 'FIELD_REMOVE', config: { field: 'toRemove' } },
|
||||
{ toRemove: 'bye', keep: 'yes' }
|
||||
);
|
||||
expect(result['toRemove']).toBeUndefined();
|
||||
expect(result['keep']).toBe('yes');
|
||||
});
|
||||
|
||||
it('FIELD_TRANSFORM MAP_VALUES: ควร map ค่าตาม mapping', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_TRANSFORM',
|
||||
config: {
|
||||
field: 'status',
|
||||
transform: 'MAP_VALUES',
|
||||
mapping: { DRAFT: 'IN_DRAFT' },
|
||||
},
|
||||
},
|
||||
{ status: 'DRAFT' }
|
||||
);
|
||||
expect(result['status']).toBe('IN_DRAFT');
|
||||
});
|
||||
|
||||
it('FIELD_TRANSFORM TO_NUMBER: ควรแปลง string เป็น number', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_TRANSFORM',
|
||||
config: { field: 'amount', transform: 'TO_NUMBER' },
|
||||
},
|
||||
{ amount: '42' }
|
||||
);
|
||||
expect(result['amount']).toBe(42);
|
||||
});
|
||||
|
||||
it('FIELD_TRANSFORM TO_STRING: ควรแปลง number เป็น string', async () => {
|
||||
const result = await runStep(
|
||||
{
|
||||
type: 'FIELD_TRANSFORM',
|
||||
config: { field: 'code', transform: 'TO_STRING' },
|
||||
},
|
||||
{ code: 123 }
|
||||
);
|
||||
expect(result['code']).toBe('123');
|
||||
});
|
||||
|
||||
it('STRUCTURE_CHANGE (unknown step type): ควร warn และไม่ crash', async () => {
|
||||
const result = await runStep(
|
||||
{ type: 'STRUCTURE_CHANGE', config: {} },
|
||||
{ key: 'value' }
|
||||
);
|
||||
expect(result['key']).toBe('value'); // ไม่ถูกแตะ
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,200 @@
|
||||
// File: src/modules/json-schema/services/ui-schema.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ UiSchemaService
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UiSchemaService } from './ui-schema.service';
|
||||
import { UiSchema } from '../interfaces/ui-schema.interface';
|
||||
|
||||
// Helper สร้าง UiSchema ที่ valid
|
||||
const makeValidUiSchema = (): UiSchema => ({
|
||||
layout: {
|
||||
type: 'stack',
|
||||
groups: [
|
||||
{
|
||||
id: 'g1',
|
||||
title: 'General',
|
||||
type: 'section',
|
||||
fields: ['title', 'status'],
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
title: { type: 'string', title: 'Title', widget: 'text' },
|
||||
status: {
|
||||
type: 'string',
|
||||
title: 'Status',
|
||||
widget: 'select',
|
||||
enum: ['DRAFT', 'SUBMITTED'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('UiSchemaService', () => {
|
||||
let service: UiSchemaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UiSchemaService],
|
||||
}).compile();
|
||||
service = module.get<UiSchemaService>(UiSchemaService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('validateUiSchema', () => {
|
||||
it('ควรคืน true เมื่อ uiSchema เป็น null/undefined (Optional)', () => {
|
||||
expect(service.validateUiSchema(null as any, {})).toBe(true);
|
||||
});
|
||||
|
||||
it('ควรคืน true เมื่อ UI Schema ถูกต้อง', () => {
|
||||
const result = service.validateUiSchema(makeValidUiSchema(), {
|
||||
properties: { title: { type: 'string' }, status: { type: 'string' } },
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('ควร throw ValidationException เมื่อขาด layout', () => {
|
||||
const badSchema = {
|
||||
fields: { title: { type: 'string', title: 'Title' } },
|
||||
} as unknown as UiSchema;
|
||||
// ValidationException expose เฉพาะ class — เช็ค class instance
|
||||
expect(() => service.validateUiSchema(badSchema, {})).toThrow(
|
||||
/Validation/
|
||||
);
|
||||
});
|
||||
|
||||
it('ควร throw ValidationException เมื่อขาด fields', () => {
|
||||
const badSchema = {
|
||||
layout: { type: 'stack', groups: [] },
|
||||
} as unknown as UiSchema;
|
||||
expect(() => service.validateUiSchema(badSchema, {})).toThrow(
|
||||
/Validation/
|
||||
);
|
||||
});
|
||||
|
||||
it('ควร throw ValidationException เมื่อ field ใน layout ไม่มีนิยามใน fields', () => {
|
||||
const schema: UiSchema = {
|
||||
layout: {
|
||||
type: 'stack',
|
||||
groups: [
|
||||
{
|
||||
id: 'g1',
|
||||
title: 'G',
|
||||
type: 'section',
|
||||
fields: ['missing_field'],
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {},
|
||||
};
|
||||
// ValidationException expose class message — เช็ค class instance
|
||||
expect(() => service.validateUiSchema(schema, {})).toThrow(/Validation/);
|
||||
});
|
||||
|
||||
it('ควรไม่ throw แม้ dataSchema มี field ที่ไม่มีใน UI Schema (warn เฉยๆ)', () => {
|
||||
const result = service.validateUiSchema(makeValidUiSchema(), {
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
extra_field_not_in_ui: { type: 'string' }, // ไม่ throw
|
||||
},
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('ควรคืน true เมื่อ dataSchema ไม่มี properties', () => {
|
||||
const result = service.validateUiSchema(makeValidUiSchema(), {});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDefaultUiSchema', () => {
|
||||
it('ควรสร้าง UI Schema พื้นฐานจาก dataSchema', () => {
|
||||
const dataSchema = {
|
||||
properties: {
|
||||
title: { type: 'string', title: 'Document Title' },
|
||||
dueDate: { type: 'string', format: 'date' },
|
||||
isPublic: { type: 'boolean' },
|
||||
priority: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'] },
|
||||
},
|
||||
required: ['title'],
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.layout.groups).toHaveLength(1);
|
||||
expect(result.layout.groups[0].title).toBe('General Information');
|
||||
expect(Object.keys(result.fields)).toHaveLength(4);
|
||||
// Widget guessing
|
||||
expect(result.fields['dueDate'].widget).toBe('date');
|
||||
expect(result.fields['isPublic'].widget).toBe('checkbox');
|
||||
expect(result.fields['priority'].widget).toBe('select');
|
||||
expect(result.fields['title'].widget).toBe('text');
|
||||
});
|
||||
|
||||
it('ควรกำหนด required=true สำหรับ field ที่อยู่ใน required array', () => {
|
||||
const dataSchema = {
|
||||
properties: { title: { type: 'string' }, note: { type: 'string' } },
|
||||
required: ['title'],
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.fields['title'].required).toBe(true);
|
||||
expect(result.fields['note'].required).toBe(false);
|
||||
});
|
||||
|
||||
it('ควรคืน empty schema เมื่อ dataSchema ไม่มี properties', () => {
|
||||
const result = service.generateDefaultUiSchema({});
|
||||
expect(result.layout.groups).toHaveLength(0);
|
||||
expect(result.fields).toEqual({});
|
||||
});
|
||||
|
||||
it('ควรคืน empty schema เมื่อ dataSchema เป็น null/undefined', () => {
|
||||
const result = service.generateDefaultUiSchema(null as any);
|
||||
expect(result.fields).toEqual({});
|
||||
});
|
||||
|
||||
it('ควร guess widget=datetime สำหรับ format=date-time', () => {
|
||||
const dataSchema = {
|
||||
properties: { createdAt: { type: 'string', format: 'date-time' } },
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.fields['createdAt'].widget).toBe('datetime');
|
||||
});
|
||||
|
||||
it('ควร guess widget=file-upload สำหรับ format=binary', () => {
|
||||
const dataSchema = {
|
||||
properties: { attachment: { type: 'string', format: 'binary' } },
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.fields['attachment'].widget).toBe('file-upload');
|
||||
});
|
||||
|
||||
it('ควร humanize field name สำหรับ camelCase', () => {
|
||||
const dataSchema = {
|
||||
properties: { documentTitle: { type: 'string' } },
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
// humanize: "documentTitle" → "Document Title"
|
||||
expect(result.fields['documentTitle'].title).toBe('Document Title');
|
||||
});
|
||||
|
||||
it('ควรใช้ title จาก property ถ้ามี', () => {
|
||||
const dataSchema = {
|
||||
properties: { myField: { type: 'string', title: 'My Custom Title' } },
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.fields['myField'].title).toBe('My Custom Title');
|
||||
});
|
||||
|
||||
it('ควรกำหนด colSpan=12 เป็น default', () => {
|
||||
const dataSchema = {
|
||||
properties: { note: { type: 'string' } },
|
||||
};
|
||||
const result = service.generateDefaultUiSchema(dataSchema);
|
||||
expect(result.fields['note'].colSpan).toBe(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
// File: src/modules/json-schema/services/virtual-column.service.spec.ts
|
||||
// Change Log:
|
||||
// - 2026-05-21: เพิ่ม unit tests สำหรับ VirtualColumnService
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { VirtualColumnService } from './virtual-column.service';
|
||||
import { VirtualColumnConfig } from '../entities/json-schema.entity';
|
||||
|
||||
// Helper สร้าง mock QueryRunner
|
||||
const makeQueryRunner = (tableExists = true, hasColumn = false) => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
release: jest.fn().mockResolvedValue(undefined),
|
||||
hasTable: jest.fn().mockResolvedValue(tableExists),
|
||||
hasColumn: jest.fn().mockResolvedValue(hasColumn),
|
||||
query: jest.fn().mockResolvedValue([{ count: 0 }]),
|
||||
});
|
||||
|
||||
const makeDataSource = (qr: ReturnType<typeof makeQueryRunner>) =>
|
||||
({
|
||||
createQueryRunner: jest.fn().mockReturnValue(qr),
|
||||
}) as unknown as DataSource;
|
||||
|
||||
const baseConfig: VirtualColumnConfig = {
|
||||
columnName: 'vc_discipline_code',
|
||||
jsonPath: '$.disciplineCode',
|
||||
dataType: 'VARCHAR',
|
||||
indexType: undefined,
|
||||
};
|
||||
|
||||
describe('VirtualColumnService', () => {
|
||||
let service: VirtualColumnService;
|
||||
|
||||
const buildService = async (ds: DataSource) => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [VirtualColumnService, { provide: DataSource, useValue: ds }],
|
||||
}).compile();
|
||||
return module.get<VirtualColumnService>(VirtualColumnService);
|
||||
};
|
||||
|
||||
describe('setupVirtualColumns', () => {
|
||||
it('ควร return ทันทีเมื่อ configs ว่าง', async () => {
|
||||
const qr = makeQueryRunner();
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', []);
|
||||
expect(qr.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร return ทันทีเมื่อ configs เป็น null/undefined', async () => {
|
||||
const qr = makeQueryRunner();
|
||||
service = await buildService(makeDataSource(qr));
|
||||
|
||||
await service.setupVirtualColumns('rfa_revisions', null as any);
|
||||
expect(qr.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควร skip เมื่อ table ไม่มีอยู่ใน DB', async () => {
|
||||
const qr = makeQueryRunner(false); // tableExists=false
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('nonexistent_table', [baseConfig]);
|
||||
expect(qr.hasTable).toHaveBeenCalledWith('nonexistent_table');
|
||||
expect(qr.hasColumn).not.toHaveBeenCalled();
|
||||
expect(qr.release).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควรสร้าง virtual column เมื่อ column ยังไม่มี', async () => {
|
||||
const qr = makeQueryRunner(true, false); // column ยังไม่มี
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', [baseConfig]);
|
||||
// ควรเรียก query สร้าง column
|
||||
expect(qr.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('ADD COLUMN vc_discipline_code')
|
||||
);
|
||||
expect(qr.release).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควรไม่สร้าง column ซ้ำเมื่อ column มีอยู่แล้ว', async () => {
|
||||
const qr = makeQueryRunner(true, true); // column มีอยู่แล้ว
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', [baseConfig]);
|
||||
// query ถูกเรียก 0 ครั้ง (ไม่สร้างซ้ำ)
|
||||
expect(qr.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ควรสร้าง index เมื่อ config มี indexType และ index ยังไม่มี', async () => {
|
||||
const qr = makeQueryRunner(true, false);
|
||||
qr.query
|
||||
.mockResolvedValueOnce(undefined) // ADD COLUMN
|
||||
.mockResolvedValueOnce([{ count: 0 }]) // check index — ยังไม่มี
|
||||
.mockResolvedValueOnce(undefined); // CREATE INDEX
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', [
|
||||
{ ...baseConfig, indexType: 'BTREE' },
|
||||
]);
|
||||
// ควรเรียก query 3 ครั้ง: ADD COLUMN, check index, CREATE INDEX
|
||||
expect(qr.query).toHaveBeenCalledTimes(3);
|
||||
const lastCall = qr.query.mock.calls[2][0] as string;
|
||||
expect(lastCall).toContain('CREATE');
|
||||
expect(lastCall).toContain('INDEX');
|
||||
});
|
||||
|
||||
it('ควรสร้าง UNIQUE index เมื่อ indexType=UNIQUE', async () => {
|
||||
const qr = makeQueryRunner(true, false);
|
||||
qr.query
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce([{ count: 0 }])
|
||||
.mockResolvedValueOnce(undefined);
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', [
|
||||
{ ...baseConfig, indexType: 'UNIQUE' },
|
||||
]);
|
||||
const indexCall = qr.query.mock.calls[2][0] as string;
|
||||
expect(indexCall).toContain('UNIQUE');
|
||||
});
|
||||
|
||||
it('ควรไม่สร้าง index ซ้ำเมื่อ index มีอยู่แล้ว', async () => {
|
||||
const qr = makeQueryRunner(true, false);
|
||||
qr.query
|
||||
.mockResolvedValueOnce(undefined) // ADD COLUMN
|
||||
.mockResolvedValueOnce([{ count: 1 }]); // index มีอยู่แล้ว
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('rfa_revisions', [
|
||||
{ ...baseConfig, indexType: 'BTREE' },
|
||||
]);
|
||||
// ไม่ควรเรียก CREATE INDEX
|
||||
expect(qr.query).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('ควร release queryRunner แม้จะ throw error', async () => {
|
||||
const qr = makeQueryRunner(true, false);
|
||||
qr.query.mockRejectedValueOnce(new Error('DB Error'));
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await expect(
|
||||
service.setupVirtualColumns('rfa_revisions', [baseConfig])
|
||||
).rejects.toThrow('DB Error');
|
||||
expect(qr.release).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SQL generation — data type mapping', () => {
|
||||
const dataTypes: Array<[string, string]> = [
|
||||
['INT', 'INT'],
|
||||
['VARCHAR', 'VARCHAR(255)'],
|
||||
['BOOLEAN', 'TINYINT(1)'],
|
||||
['DATE', 'DATE'],
|
||||
['DATETIME', 'DATETIME'],
|
||||
['DECIMAL', 'DECIMAL(10,2)'],
|
||||
['UNKNOWN_TYPE', 'VARCHAR(255)'], // default fallback
|
||||
];
|
||||
|
||||
for (const [input, expected] of dataTypes) {
|
||||
it(`ควร map dataType=${input} เป็น SQL type ${expected}`, async () => {
|
||||
const qr = makeQueryRunner(true, false);
|
||||
service = await buildService(makeDataSource(qr));
|
||||
await service.setupVirtualColumns('t', [
|
||||
{
|
||||
columnName: 'col',
|
||||
jsonPath: '$.x',
|
||||
dataType: input,
|
||||
indexType: undefined,
|
||||
},
|
||||
]);
|
||||
const addColSql = qr.query.mock.calls[0][0] as string;
|
||||
expect(addColSql).toContain(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user