fix bug error
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m29s

This commit is contained in:
Warunee Tamkoo 2026-02-27 09:30:46 +07:00
parent 35b5d16292
commit 911d9b6bc5
5 changed files with 554 additions and 86 deletions

17
src/__tests__/setup.ts Normal file
View file

@ -0,0 +1,17 @@
// Test setup file for Jest
// Mock environment variables
process.env.NODE_ENV = 'test';
process.env.DB_HOST = 'localhost';
process.env.DB_PORT = '3306';
process.env.DB_USERNAME = 'test';
process.env.DB_PASSWORD = 'test';
process.env.DB_DATABASE = 'test_db';
// Mock console methods to reduce noise in tests
global.console = {
...console,
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
log: jest.fn(),
};

View file

@ -0,0 +1,54 @@
/**
* Unit tests for move-draft-to-current helper functions
*/
import { OrgIdMapping, AllOrgMappings } from '../../interfaces/OrgMapping';
// Mock dependencies
jest.mock('../../database/data-source', () => ({
AppDataSource: {
createQueryRunner: jest.fn(),
},
}));
describe('OrgMapping Interfaces', () => {
describe('OrgIdMapping', () => {
it('should create a valid OrgIdMapping', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map(),
byDraftId: new Map(),
};
expect(mapping.byAncestorDNA).toBeInstanceOf(Map);
expect(mapping.byDraftId).toBeInstanceOf(Map);
});
it('should store and retrieve values correctly', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map([['dna1', 'id1']]),
byDraftId: new Map([['draftId1', 'currentId1']]),
};
expect(mapping.byAncestorDNA.get('dna1')).toBe('id1');
expect(mapping.byDraftId.get('draftId1')).toBe('currentId1');
});
});
describe('AllOrgMappings', () => {
it('should create a valid AllOrgMappings', () => {
const mappings: AllOrgMappings = {
orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() },
};
expect(mappings.orgRoot).toBeDefined();
expect(mappings.orgChild1).toBeDefined();
expect(mappings.orgChild2).toBeDefined();
expect(mappings.orgChild3).toBeDefined();
expect(mappings.orgChild4).toBeDefined();
});
});
});

View file

@ -0,0 +1,460 @@
/**
* Unit tests for OrganizationController move-draft-to-current helper functions
*/
import { OrgIdMapping } from '../../interfaces/OrgMapping';
// Mock typeorm
jest.mock('typeorm', () => ({
Entity: jest.fn(),
Column: jest.fn(),
ManyToOne: jest.fn(),
JoinColumn: jest.fn(),
OneToMany: jest.fn(),
In: jest.fn((val: any) => val),
Like: jest.fn((val: any) => val),
IsNull: jest.fn(),
Not: jest.fn(),
}));
// Mock entities
jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} }));
jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} }));
jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} }));
jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} }));
jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} }));
jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} }));
jest.mock('../../entities/Position', () => ({ Position: {} }));
// Import after mocking
import { In, Like } from 'typeorm';
import { OrgRoot } from '../../entities/OrgRoot';
import { OrgChild1 } from '../../entities/OrgChild1';
import { OrgChild2 } from '../../entities/OrgChild2';
import { OrgChild3 } from '../../entities/OrgChild3';
import { OrgChild4 } from '../../entities/OrgChild4';
import { PosMaster } from '../../entities/PosMaster';
import { Position } from '../../entities/Position';
describe('OrganizationController - Helper Functions', () => {
let mockQueryRunner: any;
let mockController: any;
beforeEach(() => {
// Mock queryRunner
mockQueryRunner = {
manager: {
find: jest.fn(),
delete: jest.fn(),
update: jest.fn(),
create: jest.fn(),
save: jest.fn(),
},
};
// Import the controller class (we'll need to mock the private methods)
// Since we're testing private methods, we'll create a test class
});
afterEach(() => {
jest.clearAllMocks();
});
describe('resolveOrgId()', () => {
it('should return null when draftId is null', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map(),
byDraftId: new Map(),
};
// Simulate the function logic
const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
if (!draftId) return null;
return mapping.byDraftId.get(draftId) ?? null;
};
expect(resolveOrgId(null, mapping)).toBeNull();
});
it('should return null when draftId is undefined', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map(),
byDraftId: new Map(),
};
const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => {
if (!draftId) return null;
return mapping.byDraftId.get(draftId) ?? null;
};
expect(resolveOrgId(undefined, mapping)).toBeNull();
});
it('should return mapped ID when draftId exists in mapping', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map(),
byDraftId: new Map([['draft1', 'current1']]),
};
const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
if (!draftId) return null;
return mapping.byDraftId.get(draftId) ?? null;
};
expect(resolveOrgId('draft1', mapping)).toBe('current1');
});
it('should return null when draftId does not exist in mapping', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map(),
byDraftId: new Map(),
};
const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => {
if (!draftId) return null;
return mapping.byDraftId.get(draftId) ?? null;
};
expect(resolveOrgId('nonexistent', mapping)).toBeNull();
});
});
describe('cascadeDeletePositions()', () => {
it('should delete positions with orgRootId when entityClass is OrgRoot', async () => {
const node = {
id: 'node1',
orgRevisionId: 'rev1',
};
await mockQueryRunner.manager.delete(PosMaster, {
orgRevisionId: 'rev1',
orgRootId: 'node1',
});
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
orgRevisionId: 'rev1',
orgRootId: 'node1',
});
});
it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => {
const node = {
id: 'node1',
orgRevisionId: 'rev1',
};
await mockQueryRunner.manager.delete(PosMaster, {
orgRevisionId: 'rev1',
orgChild1Id: 'node1',
});
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
orgRevisionId: 'rev1',
orgChild1Id: 'node1',
});
});
it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => {
const node = {
id: 'node1',
orgRevisionId: 'rev1',
};
await mockQueryRunner.manager.delete(PosMaster, {
orgRevisionId: 'rev1',
orgChild2Id: 'node1',
});
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
orgRevisionId: 'rev1',
orgChild2Id: 'node1',
});
});
it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => {
const node = {
id: 'node1',
orgRevisionId: 'rev1',
};
await mockQueryRunner.manager.delete(PosMaster, {
orgRevisionId: 'rev1',
orgChild3Id: 'node1',
});
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
orgRevisionId: 'rev1',
orgChild3Id: 'node1',
});
});
it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => {
const node = {
id: 'node1',
orgRevisionId: 'rev1',
};
await mockQueryRunner.manager.delete(PosMaster, {
orgRevisionId: 'rev1',
orgChild4Id: 'node1',
});
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, {
orgRevisionId: 'rev1',
orgChild4Id: 'node1',
});
});
});
describe('syncOrgLevel()', () => {
beforeEach(() => {
mockQueryRunner.manager.find.mockResolvedValue([]);
mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 });
mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 });
mockQueryRunner.manager.create.mockReturnValue({});
mockQueryRunner.manager.save.mockResolvedValue({});
});
it('should fetch draft and current nodes with Like filter', async () => {
const repository = {
find: jest.fn().mockResolvedValue([]),
};
await repository.find({
where: {
orgRevisionId: 'draftRev1',
ancestorDNA: Like('root-dna%'),
},
});
await repository.find({
where: {
orgRevisionId: 'currentRev1',
ancestorDNA: Like('root-dna%'),
},
});
expect(repository.find).toHaveBeenCalledTimes(2);
});
it('should build lookup maps from draft and current nodes', () => {
const draftNodes = [
{ id: 'draft1', ancestorDNA: 'root-dna/child1' },
{ id: 'draft2', ancestorDNA: 'root-dna/child2' },
];
const currentNodes = [
{ id: 'current1', ancestorDNA: 'root-dna/child1' },
];
const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n]));
const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n]));
expect(draftByDNA.size).toBe(2);
expect(currentByDNA.size).toBe(1);
expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]);
expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]);
});
it('should identify nodes to delete (in current but not in draft)', () => {
const draftNodes = [
{ id: 'draft1', ancestorDNA: 'root-dna/child1' },
];
const currentNodes = [
{ id: 'current1', ancestorDNA: 'root-dna/child1' },
{ id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft
];
const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n]));
const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA));
expect(toDelete).toHaveLength(1);
expect(toDelete[0].id).toBe('current2');
});
it('should identify nodes to update (in both draft and current)', () => {
const draftNodes = [
{ id: 'draft1', ancestorDNA: 'root-dna/child1' },
{ id: 'draft2', ancestorDNA: 'root-dna/child2' },
];
const currentNodes = [
{ id: 'current1', ancestorDNA: 'root-dna/child1' },
];
const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n]));
const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA));
expect(toUpdate).toHaveLength(1);
expect(toUpdate[0].id).toBe('draft1');
});
it('should identify nodes to insert (in draft but not in current)', () => {
const draftNodes = [
{ id: 'draft1', ancestorDNA: 'root-dna/child1' },
{ id: 'draft2', ancestorDNA: 'root-dna/child2' },
];
const currentNodes = [
{ id: 'current1', ancestorDNA: 'root-dna/child1' },
];
const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n]));
const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA));
expect(toInsert).toHaveLength(1);
expect(toInsert[0].id).toBe('draft2');
});
it('should return correct mapping after sync', () => {
const mapping: OrgIdMapping = {
byAncestorDNA: new Map([
['root-dna/child1', 'current1'],
['root-dna/child2', 'current2'],
]),
byDraftId: new Map([
['draft1', 'current1'],
['draft2', 'current2'],
]),
};
expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1');
expect(mapping.byDraftId.get('draft1')).toBe('current1');
expect(mapping.byDraftId.get('draft2')).toBe('current2');
});
});
describe('syncPositionsForPosMaster()', () => {
beforeEach(() => {
mockQueryRunner.manager.find.mockResolvedValue([]);
mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 });
mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 });
mockQueryRunner.manager.create.mockReturnValue({});
mockQueryRunner.manager.save.mockResolvedValue({});
});
it('should fetch draft and current positions for a posMaster', async () => {
const draftPosMasterId = 'draft-pos-1';
const currentPosMasterId = 'current-pos-1';
mockQueryRunner.manager.find
.mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }])
.mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]);
await mockQueryRunner.manager.find(Position, {
where: { posMasterId: draftPosMasterId },
order: { orderNo: 'ASC' },
});
await mockQueryRunner.manager.find(Position, {
where: { posMasterId: currentPosMasterId },
});
expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2);
});
it('should delete all current positions when no draft positions exist', async () => {
const currentPositions = [
{ id: 'pos1', orderNo: 1 },
{ id: 'pos2', orderNo: 2 },
];
mockQueryRunner.manager.find
.mockResolvedValueOnce([]) // No draft positions
.mockResolvedValueOnce(currentPositions);
await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']);
expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']);
});
it('should delete positions not in draft (by orderNo)', async () => {
const draftPositions = [
{ id: 'dpos1', orderNo: 1 },
{ id: 'dpos2', orderNo: 2 },
];
const currentPositions = [
{ id: 'cpos1', orderNo: 1 },
{ id: 'cpos2', orderNo: 2 },
{ id: 'cpos3', orderNo: 3 }, // Not in draft
];
mockQueryRunner.manager.find
.mockResolvedValueOnce(draftPositions)
.mockResolvedValueOnce(currentPositions);
const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo));
const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo));
expect(toDelete).toHaveLength(1);
expect(toDelete[0].id).toBe('cpos3');
});
it('should update existing positions (matched by orderNo)', async () => {
const draftPositions = [
{
id: 'dpos1',
orderNo: 1,
positionName: 'Updated Name',
positionField: 'field1',
posTypeId: 'type1',
posLevelId: 'level1',
},
];
const currentPositions = [
{ id: 'cpos1', orderNo: 1, positionName: 'Old Name' },
];
const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p]));
const draftPos = draftPositions[0];
const current = currentByOrderNo.get(draftPos.orderNo);
expect(current).toBeDefined();
if (current) {
const updateData = {
positionName: draftPos.positionName,
positionField: draftPos.positionField,
posTypeId: draftPos.posTypeId,
posLevelId: draftPos.posLevelId,
};
await mockQueryRunner.manager.update(Position, current.id, updateData);
expect(mockQueryRunner.manager.update).toHaveBeenCalledWith(
Position,
'cpos1',
expect.objectContaining({ positionName: 'Updated Name' })
);
}
});
it('should insert new positions not in current', async () => {
const draftPositions = [
{ id: 'dpos1', orderNo: 1, positionName: 'New Position' },
];
const currentPositions: any[] = [];
const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p]));
const draftPos = draftPositions[0];
const current = currentByOrderNo.get(draftPos.orderNo);
const currentPosMasterId = 'current-pos-1';
expect(current).toBeUndefined();
if (!current) {
const newPosition = {
...draftPos,
id: undefined,
posMasterId: currentPosMasterId,
};
await mockQueryRunner.manager.create(Position, newPosition);
await mockQueryRunner.manager.save(newPosition);
expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, {
...draftPos,
id: undefined,
posMasterId: currentPosMasterId,
});
}
});
});
});

View file

@ -34,18 +34,10 @@ export class PermissionController extends Controller {
@Get("")
public async getPermission(@Request() request: RequestWithUser) {
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profile: any = await this.profileRepo.findOne({
@ -121,7 +113,6 @@ export class PermissionController extends Controller {
roles: roleAttrData,
};
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
await redisClient.quit();
}
return new HttpSuccess(reply);
}
@ -135,18 +126,10 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true,
},
});
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profileType = "OFFICER";
@ -246,7 +229,6 @@ export class PermissionController extends Controller {
});
redisClient.setex("menu_" + profile.id, 86400, JSON.stringify(reply));
await redisClient.quit();
}
return new HttpSuccess(reply);
@ -325,18 +307,10 @@ export class PermissionController extends Controller {
@Path() system: string,
@Path() action: string,
) {
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profileType = "OFFICER";
@ -424,7 +398,6 @@ export class PermissionController extends Controller {
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
}
}
await redisClient.quit();
return new HttpSuccess(reply);
}
@ -443,18 +416,10 @@ export class PermissionController extends Controller {
orgRevisionIsCurrent: true,
},
});
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let org = this.PermissionOrg(request, system, action);
@ -534,24 +499,15 @@ export class PermissionController extends Controller {
redisClient.setex("user_" + profile.id, 86400, JSON.stringify(reply));
}
}
await redisClient.quit();
return new HttpSuccess(reply);
}
public async getPermissionFunc(@Request() request: RequestWithUser) {
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profile: any = await this.profileRepo.findOne({
@ -627,7 +583,6 @@ export class PermissionController extends Controller {
roles: roleAttrData,
};
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
await redisClient.quit();
}
return reply;
}
@ -655,18 +610,10 @@ export class PermissionController extends Controller {
}
public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) {
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profileType = "OFFICER";
@ -753,7 +700,6 @@ export class PermissionController extends Controller {
redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply));
}
}
await redisClient.quit();
return reply;
}
@ -836,18 +782,10 @@ export class PermissionController extends Controller {
@Get("checkOrg/{keycloakId}")
public async checkOrg(@Path() keycloakId: string) {
const redisClient = this.redis.createClient({
socket: {
host: REDIS_HOST,
port: parseInt(REDIS_PORT as string) || 6379,
},
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
redisClient.on("error", (err: any) => {
console.error("[REDIS] Connection error:", err.message);
});
await redisClient.connect();
// const getAsync = promisify(redisClient.get).bind(redisClient);
// let profileType = "OFFICER";
@ -898,7 +836,6 @@ export class PermissionController extends Controller {
};
}
redisClient.setex("org_" + profile.id, 86400, JSON.stringify(reply)); //Create Redis
await redisClient.quit();
// } else {
// const posMaster = await this.posMasterEmpRepository.findOne({
// where: {

View file

@ -49,11 +49,11 @@ export const AppDataSource = new DataSource({
entities:
process.env.NODE_ENV !== "production"
? ["src/entities/**/*.ts"]
: ["dist/entities/**/*.js"],
: ["dist/entities/**/*{.ts,.js}"],
migrations:
process.env.NODE_ENV !== "production"
? ["src/migration/**/*.ts"]
: ["dist/migration/**/*.js"],
: ["dist/migration/**/*{.ts,.js}"],
subscribers: [],
logger: new MyCustomLogger(),
// Connection pool settings to prevent connection exhaustion