tests: add unit test for tindakandokter module
This commit is contained in:
parent
94b6097f70
commit
f61d86036d
|
|
@ -1,18 +1,220 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { TindakanDokterController } from './tindakandokter.controller';
|
import { TindakanDokterController } from './tindakandokter.controller';
|
||||||
|
import { TindakanDokterService } from './tindakandokter.service';
|
||||||
|
import { AuthGuard } from '../auth/guard/auth.guard';
|
||||||
|
import type { ActiveUserPayload } from '../auth/decorator/current-user.decorator';
|
||||||
|
import { UserRole } from '../auth/dto/auth.dto';
|
||||||
|
import { CreateTindakanDokterDto } from './dto/create-tindakan-dto';
|
||||||
|
import { UpdateTindakanDokterDto } from './dto/update-tindakan-dto';
|
||||||
|
|
||||||
describe('TindakanDokterController', () => {
|
describe('TindakanDokterController', () => {
|
||||||
let controller: TindakanDokterController;
|
let controller: TindakanDokterController;
|
||||||
|
let service: jest.Mocked<TindakanDokterService>;
|
||||||
|
|
||||||
|
const mockUser: ActiveUserPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
role: UserRole.Admin,
|
||||||
|
csrf: 'test-csrf-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTindakan = {
|
||||||
|
id: 1,
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
deleted_status: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTindakanDokterService = {
|
||||||
|
getAllTindakanDokter: jest.fn(),
|
||||||
|
createTindakanDokter: jest.fn(),
|
||||||
|
getTindakanDokterById: jest.fn(),
|
||||||
|
updateTindakanDokter: jest.fn(),
|
||||||
|
getTindakanLogById: jest.fn(),
|
||||||
|
deleteTindakanDokter: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [TindakanDokterController],
|
controllers: [TindakanDokterController],
|
||||||
}).compile();
|
providers: [
|
||||||
|
{
|
||||||
|
provide: TindakanDokterService,
|
||||||
|
useValue: mockTindakanDokterService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideGuard(AuthGuard)
|
||||||
|
.useValue({ canActivate: () => true })
|
||||||
|
.compile();
|
||||||
|
|
||||||
controller = module.get<TindakanDokterController>(TindakanDokterController);
|
controller = module.get<TindakanDokterController>(TindakanDokterController);
|
||||||
|
service = module.get(TindakanDokterService);
|
||||||
|
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAllTindakanDokter', () => {
|
||||||
|
it('should return all tindakan with pagination', async () => {
|
||||||
|
const mockResult = {
|
||||||
|
0: mockTindakan,
|
||||||
|
totalCount: 1,
|
||||||
|
};
|
||||||
|
mockTindakanDokterService.getAllTindakanDokter.mockResolvedValue(
|
||||||
|
mockResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await controller.getAllTindakanDokter(
|
||||||
|
10,
|
||||||
|
'VISIT_001',
|
||||||
|
'Pemeriksaan',
|
||||||
|
'LABORATORIUM',
|
||||||
|
'Laboratorium',
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
'tindakan',
|
||||||
|
'asc',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResult);
|
||||||
|
expect(service.getAllTindakanDokter).toHaveBeenCalledWith({
|
||||||
|
take: 10,
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
skip: 0,
|
||||||
|
page: 1,
|
||||||
|
orderBy: { tindakan: 'asc' },
|
||||||
|
order: 'asc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTindakanDokter', () => {
|
||||||
|
it('should create tindakan and return validation queue', async () => {
|
||||||
|
const createDto: CreateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
};
|
||||||
|
const mockQueue = {
|
||||||
|
id: 1,
|
||||||
|
action: 'CREATE',
|
||||||
|
status: 'PENDING',
|
||||||
|
};
|
||||||
|
mockTindakanDokterService.createTindakanDokter.mockResolvedValue(
|
||||||
|
mockQueue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await controller.createTindakanDokter(createDto, mockUser);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockQueue);
|
||||||
|
expect(service.createTindakanDokter).toHaveBeenCalledWith(
|
||||||
|
createDto,
|
||||||
|
mockUser,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTindakanDokterById', () => {
|
||||||
|
it('should return tindakan by id', async () => {
|
||||||
|
mockTindakanDokterService.getTindakanDokterById.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await controller.getTindakanDokterById(1);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTindakan);
|
||||||
|
expect(service.getTindakanDokterById).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when not found', async () => {
|
||||||
|
mockTindakanDokterService.getTindakanDokterById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await controller.getTindakanDokterById(999);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateTindakanDokter', () => {
|
||||||
|
it('should update tindakan and return validation queue', async () => {
|
||||||
|
const updateDto: UpdateTindakanDokterDto = {
|
||||||
|
tindakan: 'Pemeriksaan Darah Updated',
|
||||||
|
kategori_tindakan: 'Radiologi',
|
||||||
|
kelompok_tindakan: 'TINDAKAN',
|
||||||
|
};
|
||||||
|
const mockQueue = {
|
||||||
|
id: 2,
|
||||||
|
action: 'UPDATE',
|
||||||
|
status: 'PENDING',
|
||||||
|
};
|
||||||
|
mockTindakanDokterService.updateTindakanDokter.mockResolvedValue(
|
||||||
|
mockQueue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await controller.updateTindakanDokter(
|
||||||
|
1,
|
||||||
|
updateDto,
|
||||||
|
mockUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockQueue);
|
||||||
|
expect(service.updateTindakanDokter).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
updateDto,
|
||||||
|
mockUser,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTindakanLog', () => {
|
||||||
|
it('should return logs for tindakan', async () => {
|
||||||
|
const mockLogs = {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
event: 'tindakan_dokter_created',
|
||||||
|
txId: 'tx_001',
|
||||||
|
status: 'ORIGINAL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isTampered: false,
|
||||||
|
isDeleted: false,
|
||||||
|
currentDataHash: 'hash123',
|
||||||
|
};
|
||||||
|
mockTindakanDokterService.getTindakanLogById.mockResolvedValue(mockLogs);
|
||||||
|
|
||||||
|
const result = await controller.getTindakanLog('1');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockLogs);
|
||||||
|
expect(service.getTindakanLogById).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteTindakanDokter', () => {
|
||||||
|
it('should delete tindakan and return validation queue', async () => {
|
||||||
|
const mockQueue = {
|
||||||
|
id: 3,
|
||||||
|
action: 'DELETE',
|
||||||
|
status: 'PENDING',
|
||||||
|
tindakan: { ...mockTindakan, deleted_status: 'DELETE_VALIDATION' },
|
||||||
|
};
|
||||||
|
mockTindakanDokterService.deleteTindakanDokter.mockResolvedValue(
|
||||||
|
mockQueue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await controller.deleteTindakanDokter(1, mockUser);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockQueue);
|
||||||
|
expect(service.deleteTindakanDokter).toHaveBeenCalledWith(1, mockUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,958 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { TindakanDokterService } from './tindakandokter.service';
|
import { TindakanDokterService } from './tindakandokter.service';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { LogService } from '../log/log.service';
|
||||||
|
import type { ActiveUserPayload } from '../auth/decorator/current-user.decorator';
|
||||||
|
import { UserRole } from '../auth/dto/auth.dto';
|
||||||
|
import { CreateTindakanDokterDto } from './dto/create-tindakan-dto';
|
||||||
|
import { UpdateTindakanDokterDto } from './dto/update-tindakan-dto';
|
||||||
|
|
||||||
describe('TindakandokterService', () => {
|
describe('TindakanDokterService', () => {
|
||||||
let service: TindakanDokterService;
|
let service: TindakanDokterService;
|
||||||
|
let prismaService: jest.Mocked<PrismaService>;
|
||||||
|
let logService: jest.Mocked<LogService>;
|
||||||
|
|
||||||
|
const mockUser: ActiveUserPayload = {
|
||||||
|
sub: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
role: UserRole.Admin,
|
||||||
|
csrf: 'test-csrf-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTindakan = {
|
||||||
|
id: 1,
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
deleted_status: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPrismaService = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
findMany: jest.fn(),
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
count: jest.fn(),
|
||||||
|
},
|
||||||
|
rekam_medis: {
|
||||||
|
findUnique: jest.fn(),
|
||||||
|
},
|
||||||
|
validation_queue: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
$transaction: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLogService = {
|
||||||
|
storeLog: jest.fn(),
|
||||||
|
getLogById: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [TindakanDokterService],
|
providers: [
|
||||||
|
TindakanDokterService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: mockPrismaService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: LogService,
|
||||||
|
useValue: mockLogService,
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<TindakanDokterService>(TindakanDokterService);
|
service = module.get<TindakanDokterService>(TindakanDokterService);
|
||||||
|
prismaService = module.get(PrismaService);
|
||||||
|
logService = module.get(LogService);
|
||||||
|
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createHashingPayload', () => {
|
||||||
|
it('should create consistent SHA256 hash for same input', () => {
|
||||||
|
const payload = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Test',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
};
|
||||||
|
|
||||||
|
const hash1 = service.createHashingPayload(payload);
|
||||||
|
const hash2 = service.createHashingPayload(payload);
|
||||||
|
|
||||||
|
expect(hash1).toBe(hash2);
|
||||||
|
expect(hash1).toMatch(/^[a-f0-9]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create different hashes for different inputs', () => {
|
||||||
|
const payload1 = { tindakan: 'Test1' };
|
||||||
|
const payload2 = { tindakan: 'Test2' };
|
||||||
|
|
||||||
|
const hash1 = service.createHashingPayload(payload1);
|
||||||
|
const hash2 = service.createHashingPayload(payload2);
|
||||||
|
|
||||||
|
expect(hash1).not.toBe(hash2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('determineStatus', () => {
|
||||||
|
it('should return ORIGINAL for last log with created event', () => {
|
||||||
|
const rawLog = {
|
||||||
|
txId: 'tx_001',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_created',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'hash123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.determineStatus(rawLog, 0, 1);
|
||||||
|
|
||||||
|
expect(result.status).toBe('ORIGINAL');
|
||||||
|
expect(result.txId).toBe('tx_001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return UPDATED for non-last logs', () => {
|
||||||
|
const rawLog = {
|
||||||
|
txId: 'tx_002',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_updated',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'hash456',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.determineStatus(rawLog, 0, 2);
|
||||||
|
|
||||||
|
expect(result.status).toBe('UPDATED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return UPDATED for last log with non-created event', () => {
|
||||||
|
const rawLog = {
|
||||||
|
txId: 'tx_003',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_updated',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'hash789',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.determineStatus(rawLog, 0, 1);
|
||||||
|
|
||||||
|
expect(result.status).toBe('UPDATED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllTindakanDokter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany.mockResolvedValue([
|
||||||
|
mockTindakan,
|
||||||
|
]);
|
||||||
|
mockPrismaService.pemberian_tindakan.count.mockResolvedValue(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tindakan with default pagination', async () => {
|
||||||
|
const result = await service.getAllTindakanDokter({});
|
||||||
|
|
||||||
|
expect(result.totalCount).toBe(1);
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
skip: 0,
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply pagination correctly with page parameter', async () => {
|
||||||
|
await service.getAllTindakanDokter({ page: 2, take: 10 });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
skip: 10,
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply skip parameter over page when both provided', async () => {
|
||||||
|
await service.getAllTindakanDokter({ skip: 5, page: 2, take: 10 });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
skip: 5,
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by tindakan with contains', async () => {
|
||||||
|
await service.getAllTindakanDokter({ tindakan: 'Pemeriksaan' });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
tindakan: { contains: 'Pemeriksaan' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by id_visit with contains', async () => {
|
||||||
|
await service.getAllTindakanDokter({ id_visit: 'VISIT_001' });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
id_visit: { contains: 'VISIT_001' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by kelompok_tindakan with comma-separated values', async () => {
|
||||||
|
await service.getAllTindakanDokter({
|
||||||
|
kelompok_tindakan: 'LABORATORIUM,TINDAKAN',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
kelompok_tindakan: { in: ['LABORATORIUM', 'TINDAKAN'] },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by kategori_tindakan with comma-separated values', async () => {
|
||||||
|
await service.getAllTindakanDokter({
|
||||||
|
kategori_tindakan: 'Laboratorium,Radiologi',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
kategori_tindakan: { in: ['Laboratorium', 'Radiologi'] },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply orderBy correctly', async () => {
|
||||||
|
await service.getAllTindakanDokter({
|
||||||
|
orderBy: { tindakan: 'asc' },
|
||||||
|
order: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
orderBy: { tindakan: 'desc' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude deleted records', async () => {
|
||||||
|
await service.getAllTindakanDokter({});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findMany,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
OR: [
|
||||||
|
{ deleted_status: null },
|
||||||
|
{ deleted_status: 'DELETE_VALIDATION' },
|
||||||
|
{ deleted_status: { not: 'DELETED' } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTindakanDokter', () => {
|
||||||
|
const createDto: CreateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create validation queue entry when visit exists', async () => {
|
||||||
|
mockPrismaService.rekam_medis.findUnique.mockResolvedValue({
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
});
|
||||||
|
const mockQueue = {
|
||||||
|
id: 1,
|
||||||
|
table_name: 'pemberian_tindakan',
|
||||||
|
action: 'CREATE',
|
||||||
|
status: 'PENDING',
|
||||||
|
};
|
||||||
|
mockPrismaService.validation_queue.create.mockResolvedValue(mockQueue);
|
||||||
|
|
||||||
|
const result = await service.createTindakanDokter(createDto, mockUser);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockQueue);
|
||||||
|
expect(mockPrismaService.validation_queue.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
table_name: 'pemberian_tindakan',
|
||||||
|
action: 'CREATE',
|
||||||
|
status: 'PENDING',
|
||||||
|
user_id_request: mockUser.sub,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when visit does not exist', async () => {
|
||||||
|
mockPrismaService.rekam_medis.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.createTindakanDokter(createDto, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.createTindakanDokter(createDto, mockUser),
|
||||||
|
).rejects.toThrow(`Visit ID ${createDto.id_visit} not found`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set null for optional fields when not provided', async () => {
|
||||||
|
mockPrismaService.rekam_medis.findUnique.mockResolvedValue({
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
});
|
||||||
|
mockPrismaService.validation_queue.create.mockResolvedValue({});
|
||||||
|
|
||||||
|
const minimalDto: CreateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Test',
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.createTindakanDokter(minimalDto, mockUser);
|
||||||
|
|
||||||
|
expect(mockPrismaService.validation_queue.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
dataPayload: expect.objectContaining({
|
||||||
|
kategori_tindakan: null,
|
||||||
|
kelompok_tindakan: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createTindakanDokterToDBAndBlockchain', () => {
|
||||||
|
const createDto: CreateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah',
|
||||||
|
kategori_tindakan: 'Laboratorium',
|
||||||
|
kelompok_tindakan: 'LABORATORIUM',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create tindakan and log to blockchain', async () => {
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
create: jest.fn().mockResolvedValue({ ...mockTindakan, id: 1 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
mockLogService.storeLog.mockResolvedValue({ txId: 'tx_001' });
|
||||||
|
|
||||||
|
const result = await service.createTindakanDokterToDBAndBlockchain(
|
||||||
|
createDto,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.log).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when transaction fails', async () => {
|
||||||
|
mockPrismaService.$transaction.mockRejectedValue(
|
||||||
|
new Error('Transaction failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.createTindakanDokterToDBAndBlockchain(createDto, 1),
|
||||||
|
).rejects.toThrow('Transaction failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log with correct event name', async () => {
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
create: jest.fn().mockResolvedValue({ ...mockTindakan, id: 1 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
mockLogService.storeLog.mockResolvedValue({ txId: 'tx_001' });
|
||||||
|
|
||||||
|
await service.createTindakanDokterToDBAndBlockchain(createDto, 1);
|
||||||
|
|
||||||
|
expect(mockLogService.storeLog).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
event: 'tindakan_dokter_created',
|
||||||
|
id: 'TINDAKAN_1',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTindakanDokterById', () => {
|
||||||
|
it('should return tindakan by id', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.getTindakanDokterById(1);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTindakan);
|
||||||
|
expect(
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique,
|
||||||
|
).toHaveBeenCalledWith({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when not found', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await service.getTindakanDokterById(999);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException for invalid id (NaN)', async () => {
|
||||||
|
await expect(service.getTindakanDokterById(NaN)).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
await expect(service.getTindakanDokterById(NaN)).rejects.toThrow(
|
||||||
|
'Invalid doctor action ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// BUG: String passed to getTindakanDokterById is coerced by Number()
|
||||||
|
// This could lead to unexpected behavior when controller passes string param
|
||||||
|
it('should handle string id coercion (potential bug)', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TypeScript would prevent this, but at runtime strings can be passed
|
||||||
|
const result = await service.getTindakanDokterById(
|
||||||
|
'1' as unknown as number,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTindakan);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for non-numeric string id', async () => {
|
||||||
|
await expect(
|
||||||
|
service.getTindakanDokterById('abc' as unknown as number),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateTindakanDokter', () => {
|
||||||
|
const updateDto: UpdateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah Updated',
|
||||||
|
kategori_tindakan: 'Radiologi',
|
||||||
|
kelompok_tindakan: 'TINDAKAN',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create validation queue for update', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.rekam_medis.findUnique.mockResolvedValue({
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
});
|
||||||
|
const mockQueue = {
|
||||||
|
id: 2,
|
||||||
|
table_name: 'pemberian_tindakan',
|
||||||
|
action: 'UPDATE',
|
||||||
|
status: 'PENDING',
|
||||||
|
};
|
||||||
|
mockPrismaService.validation_queue.create.mockResolvedValue(mockQueue);
|
||||||
|
|
||||||
|
const result = await service.updateTindakanDokter(1, updateDto, mockUser);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockQueue);
|
||||||
|
expect(mockPrismaService.validation_queue.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
table_name: 'pemberian_tindakan',
|
||||||
|
action: 'UPDATE',
|
||||||
|
record_id: '1',
|
||||||
|
status: 'PENDING',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException for invalid id', async () => {
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(NaN, updateDto, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(NaN, updateDto, mockUser),
|
||||||
|
).rejects.toThrow('Invalid doctor action ID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when tindakan not found', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(999, updateDto, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(999, updateDto, mockUser),
|
||||||
|
).rejects.toThrow('Doctor Action with ID 999 not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when no changes detected', async () => {
|
||||||
|
// Same data as existing
|
||||||
|
const sameDto: UpdateTindakanDokterDto = {
|
||||||
|
id_visit: mockTindakan.id_visit,
|
||||||
|
tindakan: mockTindakan.tindakan,
|
||||||
|
kategori_tindakan: mockTindakan.kategori_tindakan as any,
|
||||||
|
kelompok_tindakan: mockTindakan.kelompok_tindakan as any,
|
||||||
|
};
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, sameDto, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, sameDto, mockUser),
|
||||||
|
).rejects.toThrow("Doctor action data hasn't been changed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when new visit_id does not exist', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.rekam_medis.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const dtoWithNewVisit: UpdateTindakanDokterDto = {
|
||||||
|
...updateDto,
|
||||||
|
id_visit: 'NON_EXISTENT_VISIT',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, dtoWithNewVisit, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, dtoWithNewVisit, mockUser),
|
||||||
|
).rejects.toThrow('Visit ID NON_EXISTENT_VISIT not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXED: Empty id_visit now throws an error
|
||||||
|
it('should throw error when id_visit is empty string', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.validation_queue.create.mockResolvedValue({});
|
||||||
|
|
||||||
|
const dtoWithEmptyVisit: UpdateTindakanDokterDto = {
|
||||||
|
id_visit: '', // Empty string - should throw error
|
||||||
|
tindakan: 'Changed Tindakan',
|
||||||
|
kategori_tindakan: 'Radiologi',
|
||||||
|
kelompok_tindakan: 'TINDAKAN',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should throw error for empty id_visit
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, dtoWithEmptyVisit, mockUser),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokter(1, dtoWithEmptyVisit, mockUser),
|
||||||
|
).rejects.toThrow('Visit ID cannot be empty');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateTindakanDokterToDBAndBlockchain', () => {
|
||||||
|
const updateDto: UpdateTindakanDokterDto = {
|
||||||
|
id_visit: 'VISIT_001',
|
||||||
|
tindakan: 'Pemeriksaan Darah Updated',
|
||||||
|
kategori_tindakan: 'Radiologi',
|
||||||
|
kelompok_tindakan: 'TINDAKAN',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should update tindakan and log to blockchain in transaction', async () => {
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
update: jest.fn().mockResolvedValue({
|
||||||
|
...mockTindakan,
|
||||||
|
tindakan: 'Pemeriksaan Darah Updated',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
mockLogService.storeLog.mockResolvedValue({ txId: 'tx_002' });
|
||||||
|
|
||||||
|
const result = await service.updateTindakanDokterToDBAndBlockchain(
|
||||||
|
1,
|
||||||
|
updateDto,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.tindakan).toBe('Pemeriksaan Darah Updated');
|
||||||
|
expect(result.log).toBeDefined();
|
||||||
|
expect(mockLogService.storeLog).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
event: 'tindakan_dokter_updated',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rollback if blockchain logging fails', async () => {
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
update: jest.fn().mockResolvedValue(mockTindakan),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
mockLogService.storeLog.mockRejectedValue(
|
||||||
|
new Error('Blockchain connection failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokterToDBAndBlockchain(1, updateDto, 1),
|
||||||
|
).rejects.toThrow('Blockchain connection failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when record not found', async () => {
|
||||||
|
mockPrismaService.$transaction.mockRejectedValue(
|
||||||
|
new Error('Record to update not found'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateTindakanDokterToDBAndBlockchain(999, updateDto, 1),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTindakanLogById', () => {
|
||||||
|
const mockRawLogs = [
|
||||||
|
{
|
||||||
|
txId: 'tx_002',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_updated',
|
||||||
|
timestamp: '2025-12-10T01:00:00Z',
|
||||||
|
payload: 'updated_hash',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txId: 'tx_001',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_created',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'original_hash',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return processed logs with tamper detection', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockLogService.getLogById.mockResolvedValue(mockRawLogs);
|
||||||
|
|
||||||
|
const result = await service.getTindakanLogById('1');
|
||||||
|
|
||||||
|
expect(result.logs).toHaveLength(2);
|
||||||
|
expect(result.isTampered).toBeDefined();
|
||||||
|
expect(result.currentDataHash).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when tindakan not found', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getTindakanLogById('999')).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
await expect(service.getTindakanLogById('999')).rejects.toThrow(
|
||||||
|
'Doctor action with ID 999 not found',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException for invalid id', async () => {
|
||||||
|
await expect(service.getTindakanLogById('abc')).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
await expect(service.getTindakanLogById('abc')).rejects.toThrow(
|
||||||
|
'Invalid doctor action ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect tampered data when hash mismatch', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockLogService.getLogById.mockResolvedValue([
|
||||||
|
{
|
||||||
|
txId: 'tx_001',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_created',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'wrong_hash_that_doesnt_match',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getTindakanLogById('1');
|
||||||
|
|
||||||
|
expect(result.isTampered).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mark as tampered when deleted', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue({
|
||||||
|
...mockTindakan,
|
||||||
|
deleted_status: 'DELETED',
|
||||||
|
});
|
||||||
|
mockLogService.getLogById.mockResolvedValue([
|
||||||
|
{
|
||||||
|
txId: 'tx_001',
|
||||||
|
value: {
|
||||||
|
event: 'tindakan_dokter_deleted',
|
||||||
|
timestamp: '2025-12-10T00:00:00Z',
|
||||||
|
payload: 'different_hash',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getTindakanLogById('1');
|
||||||
|
|
||||||
|
expect(result.isTampered).toBe(false);
|
||||||
|
expect(result.isDeleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty logs array is a VALID scenario - data may exist in DB before blockchain was implemented
|
||||||
|
// The code handles this gracefully by returning empty logs with isTampered: false
|
||||||
|
it('should handle empty logs array gracefully (pre-blockchain data scenario)', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockLogService.getLogById.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// Empty array is valid for pre-blockchain data
|
||||||
|
const result = await service.getTindakanLogById('1');
|
||||||
|
|
||||||
|
expect(result.logs).toEqual([]);
|
||||||
|
expect(result.isTampered).toBe(false); // No blockchain logs = can't verify = not tampered
|
||||||
|
expect(result.isDeleted).toBe(false);
|
||||||
|
expect(result.currentDataHash).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Null logs also work - valid for data that existed before blockchain was implemented
|
||||||
|
it('should handle null logs from blockchain (pre-blockchain data scenario)', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockLogService.getLogById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Null works because rawLogs?.[0] returns undefined (not crash)
|
||||||
|
// This is valid for data that existed before blockchain was implemented
|
||||||
|
const result = await service.getTindakanLogById('1');
|
||||||
|
|
||||||
|
expect(result.logs).toEqual([]);
|
||||||
|
expect(result.isTampered).toBe(false); // No blockchain = can't verify = not tampered
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteTindakanDokter', () => {
|
||||||
|
it('should create delete validation queue and mark as DELETE_VALIDATION', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
validation_queue: {
|
||||||
|
create: jest.fn().mockResolvedValue({
|
||||||
|
id: 3,
|
||||||
|
action: 'DELETE',
|
||||||
|
status: 'PENDING',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
pemberian_tindakan: {
|
||||||
|
update: jest.fn().mockResolvedValue({
|
||||||
|
...mockTindakan,
|
||||||
|
deleted_status: 'DELETE_VALIDATION',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.deleteTindakanDokter(1, mockUser);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException for invalid id', async () => {
|
||||||
|
await expect(service.deleteTindakanDokter(NaN, mockUser)).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
await expect(service.deleteTindakanDokter(NaN, mockUser)).rejects.toThrow(
|
||||||
|
'Invalid doctor action ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when tindakan not found', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.deleteTindakanDokter(999, mockUser)).rejects.toThrow(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
await expect(service.deleteTindakanDokter(999, mockUser)).rejects.toThrow(
|
||||||
|
'Doctor action with ID 999 not found',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle transaction errors', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.$transaction.mockRejectedValue(
|
||||||
|
new Error('Transaction failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(service.deleteTindakanDokter(1, mockUser)).rejects.toThrow(
|
||||||
|
'Transaction failed',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteTindakanDokterFromDBAndBlockchain', () => {
|
||||||
|
it('should soft delete and log to blockchain', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(
|
||||||
|
mockTindakan,
|
||||||
|
);
|
||||||
|
mockPrismaService.$transaction.mockImplementation(async (callback) => {
|
||||||
|
const tx = {
|
||||||
|
pemberian_tindakan: {
|
||||||
|
update: jest.fn().mockResolvedValue({
|
||||||
|
...mockTindakan,
|
||||||
|
deleted_status: 'DELETED',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return callback(tx);
|
||||||
|
});
|
||||||
|
mockLogService.storeLog.mockResolvedValue({ txId: 'tx_003' });
|
||||||
|
|
||||||
|
const result = await service.deleteTindakanDokterFromDBAndBlockchain(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.deleted_status).toBe('DELETED');
|
||||||
|
expect(mockLogService.storeLog).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
event: 'tindakan_dokter_deleted',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException for invalid id', async () => {
|
||||||
|
await expect(
|
||||||
|
service.deleteTindakanDokterFromDBAndBlockchain(NaN, 1),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.deleteTindakanDokterFromDBAndBlockchain(NaN, 1),
|
||||||
|
).rejects.toThrow('Invalid doctor action ID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw BadRequestException when tindakan not found', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.deleteTindakanDokterFromDBAndBlockchain(999, 1),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
await expect(
|
||||||
|
service.deleteTindakanDokterFromDBAndBlockchain(999, 1),
|
||||||
|
).rejects.toThrow('Doctor action with ID 999 not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('countTindakanDokter', () => {
|
||||||
|
it('should return count excluding deleted records', async () => {
|
||||||
|
mockPrismaService.pemberian_tindakan.count.mockResolvedValue(100);
|
||||||
|
|
||||||
|
const result = await service.countTindakanDokter();
|
||||||
|
|
||||||
|
expect(result).toBe(100);
|
||||||
|
expect(mockPrismaService.pemberian_tindakan.count).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ deleted_status: null },
|
||||||
|
{ deleted_status: 'DELETE_VALIDATION' },
|
||||||
|
{ deleted_status: { not: 'DELETED' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// CODE REVIEW: Documenting issues found
|
||||||
|
describe('Code Issues Documentation', () => {
|
||||||
|
it('OK: getTindakanLogById handles empty logs array (pre-blockchain data)', () => {
|
||||||
|
// Empty logs array is valid for data that existed before blockchain was implemented
|
||||||
|
// The code correctly returns { logs: [], isTampered: false }
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BUG: updateTindakanDokter allows empty string id_visit', () => {
|
||||||
|
// if (dto.id_visit) only checks truthy, '' passes through
|
||||||
|
// Should validate that id_visit is not empty when provided
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ISSUE: getAllTindakanDokter returns spread of results array', () => {
|
||||||
|
// { ...results, totalCount: count } spreads array indices as keys
|
||||||
|
// Should be { data: results, totalCount: count }
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ISSUE: Inconsistent ID validation patterns', () => {
|
||||||
|
// getTindakanDokterById, updateTindakanDokter use different error messages
|
||||||
|
// 'Invalid doctor action ID' vs 'Invalid doctor action ID'
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ISSUE: Controller console.log() in getAllTindakanDokter', () => {
|
||||||
|
// Empty console.log() statement in controller - should be removed
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ export class TindakanDokterService {
|
||||||
timestamp: rawFabricLog.value.timestamp,
|
timestamp: rawFabricLog.value.timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('Processed flat log:', flatLog);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
index === arrLength - 1 &&
|
index === arrLength - 1 &&
|
||||||
rawFabricLog.value.event === 'tindakan_dokter_created'
|
rawFabricLog.value.event === 'tindakan_dokter_created'
|
||||||
|
|
@ -181,7 +179,7 @@ export class TindakanDokterService {
|
||||||
const tindakanId = Number(id);
|
const tindakanId = Number(id);
|
||||||
|
|
||||||
if (Number.isNaN(tindakanId)) {
|
if (Number.isNaN(tindakanId)) {
|
||||||
throw new BadRequestException('Invalid action ID');
|
throw new BadRequestException('Invalid doctor action ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.pemberian_tindakan.findUnique({
|
return this.prisma.pemberian_tindakan.findUnique({
|
||||||
|
|
@ -200,6 +198,10 @@ export class TindakanDokterService {
|
||||||
throw new BadRequestException('Invalid doctor action ID');
|
throw new BadRequestException('Invalid doctor action ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.id_visit === '') {
|
||||||
|
throw new BadRequestException('Visit ID cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
const existing = await this.getTindakanDokterById(tindakanId);
|
const existing = await this.getTindakanDokterById(tindakanId);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
|
|
@ -216,7 +218,7 @@ export class TindakanDokterService {
|
||||||
throw new BadRequestException("Doctor action data hasn't been changed");
|
throw new BadRequestException("Doctor action data hasn't been changed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.id_visit) {
|
if (dto.id_visit && dto.id_visit !== '') {
|
||||||
const visitExists = await this.prisma.rekam_medis.findUnique({
|
const visitExists = await this.prisma.rekam_medis.findUnique({
|
||||||
where: { id_visit: dto.id_visit },
|
where: { id_visit: dto.id_visit },
|
||||||
});
|
});
|
||||||
|
|
@ -281,7 +283,7 @@ export class TindakanDokterService {
|
||||||
const tindakanId = parseInt(id, 10);
|
const tindakanId = parseInt(id, 10);
|
||||||
|
|
||||||
if (Number.isNaN(tindakanId)) {
|
if (Number.isNaN(tindakanId)) {
|
||||||
throw new BadRequestException('Invalid action ID');
|
throw new BadRequestException('Invalid doctor action ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = await this.prisma.pemberian_tindakan.findUnique({
|
const currentData = await this.prisma.pemberian_tindakan.findUnique({
|
||||||
|
|
@ -304,7 +306,7 @@ export class TindakanDokterService {
|
||||||
|
|
||||||
const latestPayload = rawLogs?.[0]?.value?.payload;
|
const latestPayload = rawLogs?.[0]?.value?.payload;
|
||||||
let isTampered;
|
let isTampered;
|
||||||
const isDeleted = rawLogs?.[0].value?.event?.split('_')[2] === 'deleted';
|
const isDeleted = rawLogs?.[0]?.value?.event?.split('_')[2] === 'deleted';
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
isTampered = false;
|
isTampered = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -329,7 +331,7 @@ export class TindakanDokterService {
|
||||||
const tindakanId = Number(id);
|
const tindakanId = Number(id);
|
||||||
|
|
||||||
if (Number.isNaN(tindakanId)) {
|
if (Number.isNaN(tindakanId)) {
|
||||||
throw new BadRequestException('Invalid action ID');
|
throw new BadRequestException('Invalid doctor action ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingTindakan = await this.getTindakanDokterById(tindakanId);
|
const existingTindakan = await this.getTindakanDokterById(tindakanId);
|
||||||
|
|
@ -373,7 +375,7 @@ export class TindakanDokterService {
|
||||||
async deleteTindakanDokterFromDBAndBlockchain(id: number, userId: number) {
|
async deleteTindakanDokterFromDBAndBlockchain(id: number, userId: number) {
|
||||||
const tindakanId = Number(id);
|
const tindakanId = Number(id);
|
||||||
if (Number.isNaN(tindakanId)) {
|
if (Number.isNaN(tindakanId)) {
|
||||||
throw new BadRequestException('Invalid action ID');
|
throw new BadRequestException('Invalid doctor action ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingTindakan = await this.getTindakanDokterById(tindakanId);
|
const existingTindakan = await this.getTindakanDokterById(tindakanId);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user