tests: Add unit test for obat module. fix: fix multinode implementation on docker-compose-swarm.yaml

This commit is contained in:
yosaphatprs 2025-12-10 14:23:56 +07:00
parent e6fcb80d88
commit 21f2990feb
4 changed files with 927 additions and 134 deletions

View File

@ -1,18 +1,295 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ObatController } from './obat.controller'; import { ObatController } from './obat.controller';
import { ObatService } from './obat.service';
import { AuthGuard } from '../auth/guard/auth.guard';
import { UpdateObatDto } from './dto/update-obat-dto';
import { CreateObatDto } from './dto/create-obat-dto';
import { BadRequestException } from '@nestjs/common';
import type { ActiveUserPayload } from '../auth/decorator/current-user.decorator';
describe('ObatController', () => { describe('ObatController', () => {
let controller: ObatController; let controller: ObatController;
let obatService: jest.Mocked<ObatService>;
const mockUser: ActiveUserPayload = {
sub: 1,
username: 'testuser',
role: 'admin' as any,
csrf: 'test-csrf-token',
};
const mockObat = {
id: 1,
id_visit: 'VISIT001',
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
deleted_status: null,
};
const mockObatService = {
getAllObat: jest.fn(),
getObatById: jest.fn(),
createObat: jest.fn(),
updateObat: jest.fn(),
getLogObatById: jest.fn(),
deleteObat: jest.fn(),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [ObatController], controllers: [ObatController],
}).compile(); providers: [
{
provide: ObatService,
useValue: mockObatService,
},
],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<ObatController>(ObatController); controller = module.get<ObatController>(ObatController);
obatService = module.get(ObatService);
jest.clearAllMocks();
}); });
it('should be defined', () => { it('should be defined', () => {
expect(controller).toBeDefined(); expect(controller).toBeDefined();
}); });
describe('getAllObat', () => {
it('should return all obat with pagination', async () => {
const expectedResult = {
0: mockObat,
totalCount: 1,
};
mockObatService.getAllObat.mockResolvedValue(expectedResult);
const result = await controller.getAllObat(
10,
0,
1,
'id',
'Paracetamol',
'asc',
);
expect(result).toEqual(expectedResult);
expect(obatService.getAllObat).toHaveBeenCalledWith({
take: 10,
skip: 0,
page: 1,
orderBy: { id: 'asc' },
obat: 'Paracetamol',
order: 'asc',
});
});
it('should handle undefined orderBy parameter', async () => {
const expectedResult = { 0: mockObat, totalCount: 1 };
mockObatService.getAllObat.mockResolvedValue(expectedResult);
await controller.getAllObat(
10,
0,
1,
undefined as unknown as string,
undefined as unknown as string,
undefined as unknown as 'asc' | 'desc',
);
expect(obatService.getAllObat).toHaveBeenCalledWith({
take: 10,
skip: 0,
page: 1,
orderBy: undefined,
obat: undefined,
order: undefined,
});
});
it('should pass order parameter when orderBy is provided', async () => {
mockObatService.getAllObat.mockResolvedValue({ totalCount: 0 });
await controller.getAllObat(
10,
0,
1,
'obat',
undefined as unknown as string,
'desc',
);
expect(obatService.getAllObat).toHaveBeenCalledWith(
expect.objectContaining({
orderBy: { obat: 'desc' },
order: 'desc',
}),
);
});
});
describe('getObatById', () => {
it('should return obat by id', async () => {
mockObatService.getObatById.mockResolvedValue(mockObat);
const result = await controller.getObatById(1);
expect(result).toEqual(mockObat);
expect(obatService.getObatById).toHaveBeenCalledWith(1);
});
it('should return null when obat not found', async () => {
mockObatService.getObatById.mockResolvedValue(null);
const result = await controller.getObatById(999);
expect(result).toBeNull();
});
});
describe('createObat', () => {
it('should create obat successfully', async () => {
const createDto: CreateObatDto = {
id_visit: 'VISIT001',
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
const expectedResult = { id: 1, ...createDto, status: 'PENDING' };
mockObatService.createObat.mockResolvedValue(expectedResult);
const result = await controller.createObat(createDto, mockUser);
expect(result).toEqual(expectedResult);
expect(obatService.createObat).toHaveBeenCalledWith(createDto, mockUser);
});
it('should throw BadRequestException when visit ID not found', async () => {
const createDto: CreateObatDto = {
id_visit: 'INVALID',
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
mockObatService.createObat.mockRejectedValue(
new BadRequestException('Visit ID INVALID not found'),
);
await expect(controller.createObat(createDto, mockUser)).rejects.toThrow(
BadRequestException,
);
});
});
describe('updateObatById', () => {
it('should update obat successfully', async () => {
const updateDto: UpdateObatDto = {
obat: 'Ibuprofen',
jumlah_obat: 20,
aturan_pakai: '2x1',
};
const expectedResult = { id: 1, status: 'PENDING' };
mockObatService.updateObat.mockResolvedValue(expectedResult);
const result = await controller.updateObatById(1, updateDto, mockUser);
expect(result).toEqual(expectedResult);
expect(obatService.updateObat).toHaveBeenCalledWith(
1,
updateDto,
mockUser,
);
});
it('should throw BadRequestException when obat not found', async () => {
const updateDto: UpdateObatDto = {
obat: 'Ibuprofen',
jumlah_obat: 20,
aturan_pakai: '2x1',
};
mockObatService.updateObat.mockRejectedValue(
new BadRequestException('Medicine with ID 999 not found'),
);
await expect(
controller.updateObatById(999, updateDto, mockUser),
).rejects.toThrow(BadRequestException);
});
it('should throw BadRequestException when no changes detected', async () => {
const updateDto: UpdateObatDto = {
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
mockObatService.updateObat.mockRejectedValue(
new BadRequestException('No changes in medicine data detected'),
);
await expect(
controller.updateObatById(1, updateDto, mockUser),
).rejects.toThrow('No changes in medicine data detected');
});
});
describe('getObatLogs', () => {
it('should return obat logs', async () => {
const expectedLogs = {
logs: [
{
id: 'OBAT_1',
event: 'obat_created',
status: 'ORIGINAL',
},
],
isTampered: false,
currentDataHash: 'abc123',
};
mockObatService.getLogObatById.mockResolvedValue(expectedLogs);
const result = await controller.getObatLogs('1');
expect(result).toEqual(expectedLogs);
expect(obatService.getLogObatById).toHaveBeenCalledWith('1');
});
it('should handle tampered data detection', async () => {
const expectedLogs = {
logs: [],
isTampered: true,
currentDataHash: 'abc123',
};
mockObatService.getLogObatById.mockResolvedValue(expectedLogs);
const result = await controller.getObatLogs('1');
expect(result.isTampered).toBe(true);
});
});
describe('deleteObatById', () => {
it('should delete obat successfully', async () => {
const expectedResult = { id: 1, status: 'PENDING', action: 'DELETE' };
mockObatService.deleteObat.mockResolvedValue(expectedResult);
const result = await controller.deleteObatById(1, mockUser);
expect(result).toEqual(expectedResult);
expect(obatService.deleteObat).toHaveBeenCalledWith(1, mockUser);
});
it('should throw BadRequestException when obat not found', async () => {
mockObatService.deleteObat.mockRejectedValue(
new BadRequestException('Obat with id 999 not found'),
);
await expect(controller.deleteObatById(999, mockUser)).rejects.toThrow(
BadRequestException,
);
});
});
}); });

View File

@ -7,14 +7,6 @@ import { CreateObatDto } from './dto/create-obat-dto';
import { UpdateObatDto } from './dto/update-obat-dto'; import { UpdateObatDto } from './dto/update-obat-dto';
import { ObatService } from './obat.service'; import { ObatService } from './obat.service';
type PrismaDelegate<T> = {
findMany: jest.Mock;
findUnique: jest.Mock;
count: jest.Mock;
create: jest.Mock;
update: jest.Mock;
};
const createPrismaMock = () => ({ const createPrismaMock = () => ({
pemberian_obat: { pemberian_obat: {
findMany: jest.fn(), findMany: jest.fn(),
@ -22,10 +14,14 @@ const createPrismaMock = () => ({
count: jest.fn(), count: jest.fn(),
create: jest.fn(), create: jest.fn(),
update: jest.fn(), update: jest.fn(),
} as PrismaDelegate<any>, },
rekam_medis: { rekam_medis: {
findUnique: jest.fn(), findUnique: jest.fn(),
}, },
validation_queue: {
create: jest.fn(),
},
$transaction: jest.fn(),
}); });
const createLogServiceMock = () => ({ const createLogServiceMock = () => ({
@ -60,161 +56,234 @@ describe('ObatService', () => {
service = module.get<ObatService>(ObatService); service = module.get<ObatService>(ObatService);
}); });
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
describe('createHashingPayload', () => {
it('should create consistent hash for same data', () => {
const data = {
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
const hash1 = service.createHashingPayload(data);
const hash2 = service.createHashingPayload(data);
expect(hash1).toBe(hash2);
expect(typeof hash1).toBe('string');
expect(hash1.length).toBeGreaterThan(0);
});
it('should create different hash for different data', () => {
const data1 = {
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
const data2 = { obat: 'Ibuprofen', jumlah_obat: 10, aturan_pakai: '3x1' };
expect(service.createHashingPayload(data1)).not.toBe(
service.createHashingPayload(data2),
);
});
});
describe('determineStatus', () => {
it('should return ORIGINAL for last item with obat_created event', () => {
const rawLog = {
value: {
event: 'obat_created',
payload: 'hash123',
timestamp: '2024-01-01',
user_id: 1,
},
txId: 'tx123',
};
const result = service.determineStatus(rawLog, 0, 1);
expect(result.status).toBe('ORIGINAL');
expect(result.txId).toBe('tx123');
});
it('should return UPDATED for non-last items', () => {
const rawLog = {
value: {
event: 'obat_updated',
payload: 'hash123',
timestamp: '2024-01-01',
user_id: 1,
},
txId: 'tx123',
};
const result = service.determineStatus(rawLog, 0, 2);
expect(result.status).toBe('UPDATED');
});
it('should return UPDATED for last item with non-created event', () => {
const rawLog = {
value: {
event: 'obat_updated',
payload: 'hash123',
timestamp: '2024-01-01',
user_id: 1,
},
txId: 'tx123',
};
const result = service.determineStatus(rawLog, 0, 1);
expect(result.status).toBe('UPDATED');
});
});
describe('getAllObat', () => { describe('getAllObat', () => {
it('returns paginated data and total count', async () => { const mockObatList = [
prisma.pemberian_obat.findMany.mockResolvedValueOnce([ { id: 1, obat: 'Paracetamol', deleted_status: null },
{ id: 1, obat: 'Paracetamol' }, { id: 2, obat: 'Ibuprofen', deleted_status: null },
]); ];
prisma.pemberian_obat.count.mockResolvedValueOnce(10);
it('should return paginated data with total count', async () => {
prisma.pemberian_obat.findMany.mockResolvedValue(mockObatList);
prisma.pemberian_obat.count.mockResolvedValue(10);
const result = await service.getAllObat({ const result = await service.getAllObat({
take: 10, take: 10,
page: 1, page: 1,
orderBy: { id: 'asc' },
order: 'asc', order: 'asc',
obat: 'Para',
}); });
expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith({ expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith({
skip: 0, skip: 0,
take: 10, take: 10,
where: { where: {
obat: { contains: 'Para' }, obat: undefined,
OR: [
{ deleted_status: null },
{ deleted_status: 'DELETE_VALIDATION' },
{ deleted_status: { not: 'DELETED' } },
],
}, },
orderBy: { id: 'asc' }, orderBy: { id: 'asc' },
}); });
expect(prisma.pemberian_obat.count).toHaveBeenCalledWith({ expect(result.totalCount).toBe(10);
where: {
obat: { contains: 'Para' },
},
});
expect(result).toEqual({
0: { id: 1, obat: 'Paracetamol' },
totalCount: 10,
});
}); });
});
describe('createObat', () => { it('should filter by obat name', async () => {
const payload: CreateObatDto = { prisma.pemberian_obat.findMany.mockResolvedValue([mockObatList[0]]);
id_visit: 'VISIT-1', prisma.pemberian_obat.count.mockResolvedValue(1);
obat: 'Amoxicillin',
jumlah_obat: 2,
aturan_pakai: '3x1',
};
it('throws when visit not found', async () => { await service.getAllObat({ obat: 'Para' });
prisma.rekam_medis.findUnique.mockResolvedValueOnce(null);
await expect(service.createObat(payload, mockUser)).rejects.toThrow( expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith(
BadRequestException, expect.objectContaining({
where: expect.objectContaining({
obat: { contains: 'Para' },
}),
}),
); );
expect(prisma.pemberian_obat.create).not.toHaveBeenCalled();
}); });
it('creates obat and stores log', async () => { it('should handle skip parameter', async () => {
prisma.rekam_medis.findUnique.mockResolvedValueOnce({ prisma.pemberian_obat.findMany.mockResolvedValue([]);
id_visit: 'VISIT-1', prisma.pemberian_obat.count.mockResolvedValue(0);
});
prisma.pemberian_obat.create.mockResolvedValueOnce({
id: 42,
...payload,
});
logService.storeLog.mockResolvedValueOnce({ txId: 'abc' });
const result = await service.createObat(payload, mockUser); await service.getAllObat({ skip: 5 });
expect(prisma.pemberian_obat.create).toHaveBeenCalledWith({ expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith(
data: { expect.objectContaining({
id_visit: 'VISIT-1', skip: 5,
obat: 'Amoxicillin', }),
jumlah_obat: 2, );
aturan_pakai: '3x1', });
},
});
expect(logService.storeLog).toHaveBeenCalledWith({ it('should calculate skip from page when skip not provided', async () => {
id: 'OBAT_42', prisma.pemberian_obat.findMany.mockResolvedValue([]);
event: 'obat_created', prisma.pemberian_obat.count.mockResolvedValue(0);
user_id: mockUser.sub,
payload: expect.any(String),
});
expect(result).toEqual({ await service.getAllObat({ take: 10, page: 3 });
id: 42,
id_visit: 'VISIT-1', expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith(
obat: 'Amoxicillin', expect.objectContaining({
jumlah_obat: 2, skip: 20,
aturan_pakai: '3x1', take: 10,
txId: 'abc', }),
}); );
});
it('should use default take of 10 when not provided', async () => {
prisma.pemberian_obat.findMany.mockResolvedValue([]);
prisma.pemberian_obat.count.mockResolvedValue(0);
await service.getAllObat({});
expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith(
expect.objectContaining({
take: 10,
}),
);
});
it('should handle custom orderBy', async () => {
prisma.pemberian_obat.findMany.mockResolvedValue([]);
prisma.pemberian_obat.count.mockResolvedValue(0);
await service.getAllObat({ orderBy: { obat: 'desc' }, order: 'desc' });
expect(prisma.pemberian_obat.findMany).toHaveBeenCalledWith(
expect.objectContaining({
orderBy: { obat: 'desc' },
}),
);
}); });
}); });
describe('updateObatById', () => { describe('getObatById', () => {
const updatePayload: UpdateObatDto = { it('should return obat by id', async () => {
id_visit: 'VISIT-1', const mockObat = { id: 1, obat: 'Paracetamol' };
obat: 'Ibuprofen', prisma.pemberian_obat.findUnique.mockResolvedValue(mockObat);
jumlah_obat: 1,
aturan_pakai: '2x1',
};
it('updates obat and stores log', async () => { const result = await service.getObatById(1);
prisma.pemberian_obat.update.mockResolvedValueOnce({
id: 99, expect(result).toEqual(mockObat);
...updatePayload, expect(prisma.pemberian_obat.findUnique).toHaveBeenCalledWith({
id_visit: 'VISIT-1', where: { id: 1 },
}); });
logService.storeLog.mockResolvedValueOnce({ txId: 'updated' }); });
const result = await service.updateObat(99, updatePayload, mockUser); it('should return null when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
expect(prisma.pemberian_obat.update).toHaveBeenCalledWith({ const result = await service.getObatById(999);
where: { id: 99 },
data: {
obat: 'Ibuprofen',
jumlah_obat: 1,
aturan_pakai: '2x1',
},
});
expect(logService.storeLog).toHaveBeenCalledWith({ expect(result).toBeNull();
id: 'OBAT_99',
event: 'obat_updated',
user_id: mockUser.sub,
payload: expect.any(String),
});
expect(result).toEqual({
id: 99,
id_visit: 'VISIT-1',
obat: 'Ibuprofen',
jumlah_obat: 1,
aturan_pakai: '2x1',
txId: 'updated',
});
}); });
}); });
describe('getLogObatById', () => { describe('getLogObatById', () => {
it('returns processed logs and tamper status', async () => { const mockObat = {
prisma.pemberian_obat.findUnique.mockResolvedValueOnce({ id: 5,
id: 5, obat: 'Paracetamol',
obat: 'Paracetamol', jumlah_obat: 1,
jumlah_obat: 1, aturan_pakai: '3x1',
aturan_pakai: '3x1', };
});
it('should return logs with tamper status false when hashes match', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(mockObat);
const expectedHash = service.createHashingPayload({ const expectedHash = service.createHashingPayload({
obat: 'Paracetamol', obat: mockObat.obat,
jumlah_obat: 1, jumlah_obat: mockObat.jumlah_obat,
aturan_pakai: '3x1', aturan_pakai: mockObat.aturan_pakai,
}); });
logService.getLogById.mockResolvedValueOnce([ logService.getLogById.mockResolvedValue([
{ {
value: { value: {
event: 'obat_created', event: 'obat_created',
@ -229,19 +298,435 @@ describe('ObatService', () => {
const result = await service.getLogObatById('5'); const result = await service.getLogObatById('5');
expect(logService.getLogById).toHaveBeenCalledWith('OBAT_5'); expect(logService.getLogById).toHaveBeenCalledWith('OBAT_5');
expect(result).toEqual({ expect(result.isTampered).toBe(false);
logs: [ expect(result.currentDataHash).toBe(expectedHash);
{ });
it('should detect tampered data when hashes do not match', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(mockObat);
logService.getLogById.mockResolvedValue([
{
value: {
event: 'obat_created', event: 'obat_created',
payload: expectedHash, payload: 'different_hash',
timestamp: '2024-01-01T00:00:00Z', timestamp: '2024-01-01T00:00:00Z',
user_id: 1, user_id: 1,
txId: 'abc',
status: 'ORIGINAL',
}, },
], txId: 'abc',
isTampered: false, },
currentDataHash: expectedHash, ]);
const result = await service.getLogObatById('5');
expect(result.isTampered).toBe(true);
});
it('should throw error when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
logService.getLogById.mockResolvedValue([{ value: { payload: 'hash' } }]);
await expect(service.getLogObatById('999')).rejects.toThrow(
'Obat with id 999 not found',
);
});
// BUG TEST: This test exposes the bug where empty logs array causes crash
it('should handle empty logs array gracefully', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(mockObat);
logService.getLogById.mockResolvedValue([]);
// This will fail because the code tries to access rawLogs[0] without checking
await expect(service.getLogObatById('5')).rejects.toThrow();
});
});
describe('isIdVisitExists', () => {
it('should return true when visit exists', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue({ id_visit: 'VISIT001' });
const result = await service.isIdVisitExists('VISIT001');
expect(result).toBe(true);
});
it('should return false when visit does not exist', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue(null);
const result = await service.isIdVisitExists('INVALID');
expect(result).toBe(false);
});
});
describe('createObat', () => {
const createDto: CreateObatDto = {
id_visit: 'VISIT001',
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
it('should create validation queue entry for new obat', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue({ id_visit: 'VISIT001' });
prisma.validation_queue.create.mockResolvedValue({
id: 1,
table_name: 'pemberian_obat',
action: 'CREATE',
status: 'PENDING',
});
const result = await service.createObat(createDto, mockUser);
expect(prisma.validation_queue.create).toHaveBeenCalledWith({
data: {
table_name: 'pemberian_obat',
action: 'CREATE',
dataPayload: createDto,
status: 'PENDING',
user_id_request: mockUser.sub,
},
});
expect(result.status).toBe('PENDING');
});
it('should throw BadRequestException when visit ID not found', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue(null);
await expect(service.createObat(createDto, mockUser)).rejects.toThrow(
BadRequestException,
);
await expect(service.createObat(createDto, mockUser)).rejects.toThrow(
'Visit ID VISIT001 not found',
);
});
it('should propagate database errors', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue({ id_visit: 'VISIT001' });
prisma.validation_queue.create.mockRejectedValue(
new Error('Database error'),
);
await expect(service.createObat(createDto, mockUser)).rejects.toThrow(
'Database error',
);
});
});
describe('createObatToDBAndBlockchain', () => {
const createDto: CreateObatDto = {
id_visit: 'VISIT001',
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
it('should create obat and store log in transaction', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue({ id_visit: 'VISIT001' });
const mockTx = {
pemberian_obat: {
create: jest.fn().mockResolvedValue({ id: 1, ...createDto }),
},
};
prisma.$transaction.mockImplementation(async (callback) =>
callback(mockTx),
);
logService.storeLog.mockResolvedValue({ txId: 'blockchain_tx_123' });
const result = await service.createObatToDBAndBlockchain(createDto, 1);
expect(mockTx.pemberian_obat.create).toHaveBeenCalledWith({
data: createDto,
});
expect(logService.storeLog).toHaveBeenCalledWith({
id: 'OBAT_1',
event: 'obat_created',
user_id: '1',
payload: expect.any(String),
});
expect(result.txId).toBe('blockchain_tx_123');
});
it('should throw when visit ID not found', async () => {
prisma.rekam_medis.findUnique.mockResolvedValue(null);
await expect(
service.createObatToDBAndBlockchain(createDto, 1),
).rejects.toThrow(BadRequestException);
});
});
describe('updateObat', () => {
const existingObat = {
id: 1,
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
const updateDto: UpdateObatDto = {
obat: 'Ibuprofen',
jumlah_obat: 20,
aturan_pakai: '2x1',
};
it('should create validation queue entry for update', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
prisma.validation_queue.create.mockResolvedValue({
id: 1,
action: 'UPDATE',
status: 'PENDING',
});
const result = await service.updateObat(1, updateDto, mockUser);
expect(prisma.validation_queue.create).toHaveBeenCalledWith({
data: {
table_name: 'pemberian_obat',
action: 'UPDATE',
dataPayload: updateDto,
record_id: '1',
user_id_request: mockUser.sub,
status: 'PENDING',
},
});
expect(result.status).toBe('PENDING');
});
it('should throw when ID is invalid (NaN)', async () => {
await expect(
service.updateObat(NaN, updateDto, mockUser),
).rejects.toThrow('Medicine ID not valid');
});
it('should throw when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
await expect(
service.updateObat(999, updateDto, mockUser),
).rejects.toThrow('Medicine with ID 999 not found');
});
it('should throw when no changes detected', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
const noChangeDto: UpdateObatDto = {
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
await expect(
service.updateObat(1, noChangeDto, mockUser),
).rejects.toThrow('No changes in medicine data detected');
});
it('should detect change in obat field only', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
prisma.validation_queue.create.mockResolvedValue({ id: 1 });
const partialChangeDto: UpdateObatDto = {
obat: 'Different',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
await service.updateObat(1, partialChangeDto, mockUser);
expect(prisma.validation_queue.create).toHaveBeenCalled();
});
it('should detect change in jumlah_obat field only', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
prisma.validation_queue.create.mockResolvedValue({ id: 1 });
const partialChangeDto: UpdateObatDto = {
obat: 'Paracetamol',
jumlah_obat: 99,
aturan_pakai: '3x1',
};
await service.updateObat(1, partialChangeDto, mockUser);
expect(prisma.validation_queue.create).toHaveBeenCalled();
});
});
describe('updateObatToDBAndBlockchain', () => {
const updateDto: UpdateObatDto = {
obat: 'Ibuprofen',
jumlah_obat: 20,
aturan_pakai: '2x1',
};
it('should update obat and store log in transaction', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue({ id: 1 });
const mockTx = {
pemberian_obat: {
update: jest.fn().mockResolvedValue({ id: 1, ...updateDto }),
},
};
prisma.$transaction.mockImplementation(async (callback) =>
callback(mockTx),
);
logService.storeLog.mockResolvedValue({ txId: 'blockchain_tx_456' });
const result = await service.updateObatToDBAndBlockchain(1, updateDto, 1);
expect(mockTx.pemberian_obat.update).toHaveBeenCalledWith({
where: { id: 1 },
data: updateDto,
});
expect(logService.storeLog).toHaveBeenCalledWith({
id: 'OBAT_1',
event: 'obat_updated',
user_id: '1',
payload: expect.any(String),
});
expect(result.txId).toBe('blockchain_tx_456');
});
it('should throw when ID is invalid', async () => {
await expect(
service.updateObatToDBAndBlockchain(NaN, updateDto, 1),
).rejects.toThrow('ID medicine not valid');
});
it('should throw when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
await expect(
service.updateObatToDBAndBlockchain(999, updateDto, 1),
).rejects.toThrow('Medicine with id 999 not found');
});
});
describe('deleteObat', () => {
const existingObat = {
id: 1,
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
it('should create validation queue and mark as DELETE_VALIDATION', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
const mockTx = {
pemberian_obat: {
update: jest.fn().mockResolvedValue({
...existingObat,
deleted_status: 'DELETE_VALIDATION',
}),
},
};
prisma.$transaction.mockImplementation(async (callback) =>
callback(mockTx),
);
prisma.validation_queue.create.mockResolvedValue({
id: 1,
action: 'DELETE',
status: 'PENDING',
});
const result = await service.deleteObat(1, mockUser);
expect(prisma.validation_queue.create).toHaveBeenCalledWith({
data: {
table_name: 'pemberian_obat',
action: 'DELETE',
dataPayload: existingObat,
record_id: '1',
user_id_request: mockUser.sub,
status: 'PENDING',
},
});
expect(mockTx.pemberian_obat.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { deleted_status: 'DELETE_VALIDATION' },
});
});
it('should throw when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
await expect(service.deleteObat(999, mockUser)).rejects.toThrow(
'Obat with id 999 not found',
);
});
});
describe('deleteObatFromDBAndBlockchain', () => {
const existingObat = {
id: 1,
obat: 'Paracetamol',
jumlah_obat: 10,
aturan_pakai: '3x1',
};
it('should mark as deleted and store log', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(existingObat);
const mockTx = {
pemberian_obat: {
update: jest.fn().mockResolvedValue({
...existingObat,
deleted_status: 'DELETED',
}),
},
};
prisma.$transaction.mockImplementation(async (callback) =>
callback(mockTx),
);
logService.storeLog.mockResolvedValue({ txId: 'blockchain_delete_tx' });
const result = await service.deleteObatFromDBAndBlockchain(1, 1);
expect(mockTx.pemberian_obat.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { deleted_status: 'DELETED' },
});
expect(logService.storeLog).toHaveBeenCalledWith({
id: 'OBAT_1',
event: 'obat_deleted',
user_id: '1',
payload: expect.any(String),
});
expect(result.txId).toBe('blockchain_delete_tx');
});
it('should throw when ID is invalid', async () => {
await expect(
service.deleteObatFromDBAndBlockchain(NaN, 1),
).rejects.toThrow('Medicine ID not valid');
});
it('should throw when obat not found', async () => {
prisma.pemberian_obat.findUnique.mockResolvedValue(null);
await expect(
service.deleteObatFromDBAndBlockchain(999, 1),
).rejects.toThrow('Medicine with ID 999 not found');
});
});
describe('countObat', () => {
it('should return count excluding deleted records', async () => {
prisma.pemberian_obat.count.mockResolvedValue(42);
const result = await service.countObat();
expect(result).toBe(42);
expect(prisma.pemberian_obat.count).toHaveBeenCalledWith({
where: {
OR: [
{ deleted_status: null },
{ deleted_status: 'DELETE_VALIDATION' },
{ deleted_status: { not: 'DELETED' } },
],
},
}); });
}); });
}); });

View File

@ -102,6 +102,20 @@ export class ObatService {
throw new Error(`Obat with id ${id} not found`); throw new Error(`Obat with id ${id} not found`);
} }
if (!rawLogs || rawLogs.length === 0) {
const currentDataHash = this.createHashingPayload({
obat: currentData.obat,
jumlah_obat: currentData.jumlah_obat,
aturan_pakai: currentData.aturan_pakai,
});
return {
logs: [],
isTampered: true,
currentDataHash: currentDataHash,
};
}
const currentDataHash = this.createHashingPayload({ const currentDataHash = this.createHashingPayload({
obat: currentData.obat, obat: currentData.obat,
jumlah_obat: currentData.jumlah_obat, jumlah_obat: currentData.jumlah_obat,

View File

@ -97,6 +97,9 @@ services:
- /home/labai1/josafat/hospital-log/backend/blockchain/chaincode:/opt/gopath/src/github.com/hyperledger/fabric/peer/chaincode - /home/labai1/josafat/hospital-log/backend/blockchain/chaincode:/opt/gopath/src/github.com/hyperledger/fabric/peer/chaincode
- /home/labai1/josafat/hospital-log/backend/blockchain/network/organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations - /home/labai1/josafat/hospital-log/backend/blockchain/network/organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations
- /home/labai1/josafat/hospital-log/backend/blockchain/network/channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts - /home/labai1/josafat/hospital-log/backend/blockchain/network/channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts
extra_hosts:
- "peer1.hospital.com:192.168.11.94"
- "peer2.hospital.com:192.168.11.63"
depends_on: depends_on:
- orderer - orderer
- peer0 - peer0
@ -128,8 +131,14 @@ services:
- /var/run/docker.sock:/host/var/run/docker.sock - /var/run/docker.sock:/host/var/run/docker.sock
- /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/msp:/etc/hyperledger/fabric/msp - /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/msp:/etc/hyperledger/fabric/msp
- /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/tls:/etc/hyperledger/fabric/tls - /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/tls:/etc/hyperledger/fabric/tls
extra_hosts:
- "peer0.hospital.com:192.168.11.211"
- "peer2.hospital.com:192.168.11.63"
ports: ports:
- "8051:8051" - target: 8051
published: 8051
protocol: tcp
mode: host
networks: networks:
- hospital_net - hospital_net
deploy: deploy:
@ -152,14 +161,22 @@ services:
- CORE_PEER_CHAINCODEADDRESS=peer2.hospital.com:7052 - CORE_PEER_CHAINCODEADDRESS=peer2.hospital.com:7052
- CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052 - CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.hospital.com:7051 - CORE_PEER_GOSSIP_BOOTSTRAP=peer0.hospital.com:7051
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer2.hospital.com:9051 - CORE_PEER_GOSSIP_EXTERNALENDPOINT=192.168.11.63:9051
- CORE_PEER_LOCALMSPID=HospitalMSP - CORE_PEER_LOCALMSPID=HospitalMSP
volumes: volumes:
- /var/run/docker.sock:/host/var/run/docker.sock - /var/run/docker.sock:/host/var/run/docker.sock
- /run/desktop/mnt/host/c/fabric-data/network/organizations/peerOrganizations/hospital.com/peers/peer2.hospital.com/msp:/etc/hyperledger/fabric/msp - /home/my_device/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer2.hospital.com/msp:/etc/hyperledger/fabric/msp
- /run/desktop/mnt/host/c/fabric-data/network/organizations/peerOrganizations/hospital.com/peers/peer2.hospital.com/tls:/etc/hyperledger/fabric/tls - /home/my_device/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer2.hospital.com/tls:/etc/hyperledger/fabric/tls
- /home/my_device/josafat/hospital-log/backend/blockchain/data:/var/hyperledger/production
extra_hosts:
- "peer0.hospital.com:192.168.11.211"
- "orderer.hospital.com:192.168.11.211"
- "peer1.hospital.com:192.168.11.94"
ports: ports:
- "9051:9051" - target: 9051
published: 9051
protocol: tcp
mode: host
networks: networks:
- hospital_net - hospital_net
deploy: deploy: