tests: create unit tests for audit modules (controller, gateway, service). fix: add error handling with throwing error instead of logging only
This commit is contained in:
parent
fda1d5d92a
commit
74d5da7475
|
|
@ -1,18 +1,233 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuditController } from './audit.controller';
|
||||
import { AuditService } from './audit.service';
|
||||
import { AuthGuard } from '../auth/guard/auth.guard';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('AuditController', () => {
|
||||
let controller: AuditController;
|
||||
let auditService: jest.Mocked<AuditService>;
|
||||
|
||||
const mockAuditService = {
|
||||
getAuditTrails: jest.fn(),
|
||||
storeAuditTrail: jest.fn(),
|
||||
getCountAuditTamperedData: jest.fn(),
|
||||
};
|
||||
|
||||
const mockJwtService = {
|
||||
verifyAsync: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuditController],
|
||||
}).compile();
|
||||
providers: [
|
||||
{ provide: AuditService, useValue: mockAuditService },
|
||||
{ provide: JwtService, useValue: mockJwtService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<AuditController>(AuditController);
|
||||
auditService = module.get(AuditService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAuditTrail', () => {
|
||||
const mockAuditLogs = {
|
||||
0: {
|
||||
id: 'REKAM_1',
|
||||
event: 'rekam_medis_created',
|
||||
result: 'non_tampered',
|
||||
},
|
||||
1: { id: 'OBAT_1', event: 'obat_created', result: 'tampered' },
|
||||
totalCount: 2,
|
||||
};
|
||||
|
||||
it('should return audit trails with default parameters', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockAuditLogs);
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass search parameter', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
await controller.getAuditTrail('REKAM', 1, 10, '', '', '', 'desc');
|
||||
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'REKAM',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass type filter parameter', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
await controller.getAuditTrail('', 1, 10, 'rekam_medis', '', '', 'desc');
|
||||
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'rekam_medis',
|
||||
'',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass tampered filter parameter', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
await controller.getAuditTrail('', 1, 10, '', 'tampered', '', 'desc');
|
||||
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'tampered',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass orderBy and order parameters', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
await controller.getAuditTrail('', 1, 10, '', '', 'last_sync', 'desc');
|
||||
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'',
|
||||
'last_sync',
|
||||
'desc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass all parameters together', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue(mockAuditLogs);
|
||||
|
||||
await controller.getAuditTrail(
|
||||
'search',
|
||||
2,
|
||||
25,
|
||||
'obat',
|
||||
'non_tampered',
|
||||
'timestamp',
|
||||
'asc',
|
||||
);
|
||||
|
||||
expect(mockAuditService.getAuditTrails).toHaveBeenCalledWith(
|
||||
'search',
|
||||
2,
|
||||
25,
|
||||
'obat',
|
||||
'non_tampered',
|
||||
'timestamp',
|
||||
'asc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty results', async () => {
|
||||
mockAuditService.getAuditTrails.mockResolvedValue({ totalCount: 0 });
|
||||
|
||||
const result = await controller.getAuditTrail(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'desc',
|
||||
);
|
||||
|
||||
expect(result).toEqual({ totalCount: 0 });
|
||||
});
|
||||
|
||||
it('should propagate service errors', async () => {
|
||||
mockAuditService.getAuditTrails.mockRejectedValue(
|
||||
new Error('Database error'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
controller.getAuditTrail('', 1, 10, '', '', '', 'desc'),
|
||||
).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAuditTrail', () => {
|
||||
it('should start audit trail process and return status', () => {
|
||||
mockAuditService.storeAuditTrail.mockResolvedValue(undefined);
|
||||
|
||||
const result = controller.createAuditTrail();
|
||||
|
||||
expect(result).toEqual({
|
||||
message: 'Proses audit trail dijalankan',
|
||||
status: 'STARTED',
|
||||
});
|
||||
expect(mockAuditService.storeAuditTrail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not wait for storeAuditTrail to complete', () => {
|
||||
// storeAuditTrail is fire-and-forget (not awaited)
|
||||
let resolved = false;
|
||||
mockAuditService.storeAuditTrail.mockImplementation(async () => {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
const result = controller.createAuditTrail();
|
||||
|
||||
expect(result.status).toBe('STARTED');
|
||||
expect(resolved).toBe(false); // Should return before async completes
|
||||
});
|
||||
|
||||
it('should call storeAuditTrail without parameters', () => {
|
||||
controller.createAuditTrail();
|
||||
|
||||
expect(mockAuditService.storeAuditTrail).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ export class AuditController {
|
|||
@Query('pageSize') pageSize: number,
|
||||
@Query('type') type: string,
|
||||
@Query('tampered') tampered: string,
|
||||
@Query('orderBy') orderBy: string,
|
||||
@Query('order') order: 'asc' | 'desc',
|
||||
) {
|
||||
const result = await this.auditService.getAuditTrails(
|
||||
search,
|
||||
|
|
@ -29,6 +31,8 @@ export class AuditController {
|
|||
pageSize,
|
||||
type,
|
||||
tampered,
|
||||
orderBy,
|
||||
order,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,198 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuditGateway } from './audit.gateway';
|
||||
import { Server } from 'socket.io';
|
||||
import { WebsocketGuard } from '../auth/guard/websocket.guard';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('AuditGateway', () => {
|
||||
let gateway: AuditGateway;
|
||||
let mockServer: jest.Mocked<Server>;
|
||||
|
||||
const mockJwtService = {
|
||||
verifyAsync: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuditGateway],
|
||||
providers: [
|
||||
AuditGateway,
|
||||
WebsocketGuard,
|
||||
{ provide: JwtService, useValue: mockJwtService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
gateway = module.get<AuditGateway>(AuditGateway);
|
||||
|
||||
// Mock the WebSocket server
|
||||
mockServer = {
|
||||
emit: jest.fn(),
|
||||
} as unknown as jest.Mocked<Server>;
|
||||
|
||||
// Inject mock server
|
||||
gateway.server = mockServer;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(gateway).toBeDefined();
|
||||
});
|
||||
|
||||
describe('sendProgress', () => {
|
||||
it('should emit audit.progress event with progress data', () => {
|
||||
const progressData = { status: 'RUNNING', progress_count: 50 };
|
||||
|
||||
gateway.sendProgress(progressData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith(
|
||||
'audit.progress',
|
||||
progressData,
|
||||
);
|
||||
expect(mockServer.emit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should emit progress with zero count', () => {
|
||||
const progressData = { status: 'RUNNING', progress_count: 0 };
|
||||
|
||||
gateway.sendProgress(progressData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith(
|
||||
'audit.progress',
|
||||
progressData,
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit progress with large count', () => {
|
||||
const progressData = { status: 'RUNNING', progress_count: 10000 };
|
||||
|
||||
gateway.sendProgress(progressData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith(
|
||||
'audit.progress',
|
||||
progressData,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendComplete', () => {
|
||||
it('should emit audit.complete event with complete data', () => {
|
||||
const completeData = { status: 'COMPLETED' };
|
||||
|
||||
gateway.sendComplete(completeData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith(
|
||||
'audit.complete',
|
||||
completeData,
|
||||
);
|
||||
expect(mockServer.emit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should emit complete with additional metadata', () => {
|
||||
const completeData = {
|
||||
status: 'COMPLETED',
|
||||
total_processed: 100,
|
||||
duration_ms: 5000,
|
||||
};
|
||||
|
||||
gateway.sendComplete(completeData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith(
|
||||
'audit.complete',
|
||||
completeData,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendError', () => {
|
||||
it('should emit audit.error event with error data', () => {
|
||||
const errorData = {
|
||||
message: 'Database connection failed',
|
||||
code: 'DB_ERROR',
|
||||
};
|
||||
|
||||
gateway.sendError(errorData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('audit.error', errorData);
|
||||
expect(mockServer.emit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should emit error with stack trace', () => {
|
||||
const errorData = {
|
||||
message: 'Unexpected error',
|
||||
stack: 'Error: Unexpected error\n at AuditService...',
|
||||
};
|
||||
|
||||
gateway.sendError(errorData);
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('audit.error', errorData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleConnection', () => {
|
||||
it('should log client connection', () => {
|
||||
const mockClient = { id: 'test-client-123' } as any;
|
||||
const loggerSpy = jest.spyOn(gateway['logger'], 'log');
|
||||
|
||||
gateway.handleConnection(mockClient);
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith(
|
||||
'Klien terhubung: test-client-123',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDisconnect', () => {
|
||||
it('should log client disconnection', () => {
|
||||
const mockClient = { id: 'test-client-456' } as any;
|
||||
const loggerSpy = jest.spyOn(gateway['logger'], 'log');
|
||||
|
||||
gateway.handleDisconnect(mockClient);
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith('Klien terputus: test-client-456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple emissions', () => {
|
||||
it('should handle multiple progress emissions', () => {
|
||||
gateway.sendProgress({ status: 'RUNNING', progress_count: 10 });
|
||||
gateway.sendProgress({ status: 'RUNNING', progress_count: 20 });
|
||||
gateway.sendProgress({ status: 'RUNNING', progress_count: 30 });
|
||||
|
||||
expect(mockServer.emit).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle progress followed by complete', () => {
|
||||
gateway.sendProgress({ status: 'RUNNING', progress_count: 100 });
|
||||
gateway.sendComplete({ status: 'COMPLETED' });
|
||||
|
||||
expect(mockServer.emit).toHaveBeenNthCalledWith(1, 'audit.progress', {
|
||||
status: 'RUNNING',
|
||||
progress_count: 100,
|
||||
});
|
||||
expect(mockServer.emit).toHaveBeenNthCalledWith(2, 'audit.complete', {
|
||||
status: 'COMPLETED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle progress followed by error', () => {
|
||||
gateway.sendProgress({ status: 'RUNNING', progress_count: 50 });
|
||||
gateway.sendError({ message: 'Process failed' });
|
||||
|
||||
expect(mockServer.emit).toHaveBeenNthCalledWith(1, 'audit.progress', {
|
||||
status: 'RUNNING',
|
||||
progress_count: 50,
|
||||
});
|
||||
expect(mockServer.emit).toHaveBeenNthCalledWith(2, 'audit.error', {
|
||||
message: 'Process failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,653 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuditService } from './audit.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { LogService } from '../log/log.service';
|
||||
import { ObatService } from '../obat/obat.service';
|
||||
import { RekammedisService } from '../rekammedis/rekammedis.service';
|
||||
import { TindakanDokterService } from '../tindakandokter/tindakandokter.service';
|
||||
import { AuditGateway } from './audit.gateway';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
describe('AuditService', () => {
|
||||
let service: AuditService;
|
||||
let prisma: jest.Mocked<PrismaService>;
|
||||
let logService: jest.Mocked<LogService>;
|
||||
let obatService: jest.Mocked<ObatService>;
|
||||
let rekamMedisService: jest.Mocked<RekammedisService>;
|
||||
let tindakanService: jest.Mocked<TindakanDokterService>;
|
||||
let auditGateway: jest.Mocked<AuditGateway>;
|
||||
|
||||
const mockPrisma = {
|
||||
audit: {
|
||||
findMany: jest.fn(),
|
||||
count: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
},
|
||||
$transaction: jest.fn(),
|
||||
};
|
||||
|
||||
const mockLogService = {
|
||||
getLogsWithPagination: jest.fn(),
|
||||
};
|
||||
|
||||
const mockObatService = {
|
||||
getObatById: jest.fn(),
|
||||
createHashingPayload: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRekamMedisService = {
|
||||
getRekamMedisById: jest.fn(),
|
||||
createHashingPayload: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTindakanService = {
|
||||
getTindakanDokterById: jest.fn(),
|
||||
createHashingPayload: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAuditGateway = {
|
||||
sendProgress: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
sendError: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
// Suppress logger output during tests
|
||||
jest.spyOn(Logger.prototype, 'debug').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuditService],
|
||||
providers: [
|
||||
AuditService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ObatService, useValue: mockObatService },
|
||||
{ provide: RekammedisService, useValue: mockRekamMedisService },
|
||||
{ provide: TindakanDokterService, useValue: mockTindakanService },
|
||||
{ provide: AuditGateway, useValue: mockAuditGateway },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuditService>(AuditService);
|
||||
prisma = module.get(PrismaService);
|
||||
logService = module.get(LogService);
|
||||
obatService = module.get(ObatService);
|
||||
rekamMedisService = module.get(RekammedisService);
|
||||
tindakanService = module.get(TindakanDokterService);
|
||||
auditGateway = module.get(AuditGateway);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAuditTrails', () => {
|
||||
const mockAuditLogs = [
|
||||
{ id: 'REKAM_1', event: 'rekam_medis_created', result: 'non_tampered' },
|
||||
{ id: 'OBAT_1', event: 'obat_created', result: 'tampered' },
|
||||
];
|
||||
|
||||
it('should return paginated audit logs', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue(mockAuditLogs);
|
||||
mockPrisma.audit.count.mockResolvedValue(2);
|
||||
|
||||
const result = await service.getAuditTrails('', 1, 10);
|
||||
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 0,
|
||||
orderBy: { timestamp: 'desc' },
|
||||
where: {
|
||||
id: undefined,
|
||||
result: undefined,
|
||||
OR: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter by rekam_medis type', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, 'rekam_medis');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: { startsWith: 'REKAM' },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by tindakan type', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, 'tindakan');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: { startsWith: 'TINDAKAN' },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by obat type', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, 'obat');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: { startsWith: 'OBAT' },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by tampered status', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, undefined, 'tampered');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
result: 'tampered',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by non_tampered status', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, undefined, 'non_tampered');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
result: 'non_tampered',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore "all" type filter', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, 'all');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore "initial" type filter', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 1, 10, 'initial');
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
id: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should search by id', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('REKAM_123', 1, 10);
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: [{ id: { contains: 'REKAM_123' } }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply custom orderBy and order', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails(
|
||||
'',
|
||||
1,
|
||||
10,
|
||||
undefined,
|
||||
undefined,
|
||||
'last_sync',
|
||||
'asc',
|
||||
);
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { last_sync: 'asc' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should calculate correct skip for pagination', async () => {
|
||||
mockPrisma.audit.findMany.mockResolvedValue([]);
|
||||
mockPrisma.audit.count.mockResolvedValue(0);
|
||||
|
||||
await service.getAuditTrails('', 3, 10);
|
||||
|
||||
expect(mockPrisma.audit.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
skip: 20, // (page - 1) * pageSize = (3 - 1) * 10
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCountAuditTamperedData', () => {
|
||||
it('should return all tampered counts', async () => {
|
||||
mockPrisma.audit.count
|
||||
.mockResolvedValueOnce(10) // auditTamperedCount
|
||||
.mockResolvedValueOnce(90) // auditNonTamperedCount
|
||||
.mockResolvedValueOnce(3) // rekamMedisTamperedCount
|
||||
.mockResolvedValueOnce(4) // tindakanDokterTamperedCount
|
||||
.mockResolvedValueOnce(3); // obatTamperedCount
|
||||
|
||||
const result = await service.getCountAuditTamperedData();
|
||||
|
||||
expect(result).toEqual({
|
||||
auditTamperedCount: 10,
|
||||
auditNonTamperedCount: 90,
|
||||
rekamMedisTamperedCount: 3,
|
||||
tindakanDokterTamperedCount: 4,
|
||||
obatTamperedCount: 3,
|
||||
});
|
||||
expect(mockPrisma.audit.count).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareData', () => {
|
||||
it('should return true when hashes match', async () => {
|
||||
const hash = 'abc123def456';
|
||||
const result = await service.compareData(hash, hash);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when hashes differ', async () => {
|
||||
const result = await service.compareData('hash1', 'hash2');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty strings comparison with non-empty', async () => {
|
||||
const result = await service.compareData('', 'somehash');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeAuditTrail', () => {
|
||||
it('should process logs and send complete when done', async () => {
|
||||
mockLogService.getLogsWithPagination.mockResolvedValue({
|
||||
logs: [],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockLogService.getLogsWithPagination).toHaveBeenCalledWith(25, '');
|
||||
});
|
||||
|
||||
it('should process rekam_medis logs correctly', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_123',
|
||||
event: 'rekam_medis_created',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'blockchain_hash_123',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRekamMedis = {
|
||||
id_visit: '123',
|
||||
anamnese: 'test',
|
||||
jenis_kasus: 'test',
|
||||
tindak_lanjut: 'test',
|
||||
deleted_status: null,
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue(mockRekamMedis);
|
||||
mockRekamMedisService.createHashingPayload.mockReturnValue(
|
||||
'blockchain_hash_123',
|
||||
);
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockRekamMedisService.getRekamMedisById).toHaveBeenCalledWith(
|
||||
'123',
|
||||
);
|
||||
expect(mockAuditGateway.sendProgress).toHaveBeenCalled();
|
||||
expect(mockAuditGateway.sendComplete).toHaveBeenCalledWith({
|
||||
status: 'COMPLETED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process obat logs correctly', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'OBAT_456',
|
||||
event: 'obat_created',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'obat_hash',
|
||||
},
|
||||
};
|
||||
|
||||
const mockObat = {
|
||||
id: 456,
|
||||
obat: 'Paracetamol',
|
||||
jumlah_obat: 10,
|
||||
aturan_pakai: '3x1',
|
||||
deleted_status: null,
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockObatService.getObatById.mockResolvedValue(mockObat);
|
||||
mockObatService.createHashingPayload.mockReturnValue('obat_hash');
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockObatService.getObatById).toHaveBeenCalledWith(456);
|
||||
});
|
||||
|
||||
it('should process tindakan logs correctly', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'TINDAKAN_789',
|
||||
event: 'tindakan_dokter_created',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'tindakan_hash',
|
||||
},
|
||||
};
|
||||
|
||||
const mockTindakan = {
|
||||
id: 789,
|
||||
id_visit: '123',
|
||||
tindakan: 'Pemeriksaan',
|
||||
kategori_tindakan: 'Umum',
|
||||
kelompok_tindakan: 'Poliklinik',
|
||||
deleted_status: null,
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockTindakanService.getTindakanDokterById.mockResolvedValue(mockTindakan);
|
||||
mockTindakanService.createHashingPayload.mockReturnValue('tindakan_hash');
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockTindakanService.getTindakanDokterById).toHaveBeenCalledWith(
|
||||
789,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle pagination with bookmark', async () => {
|
||||
mockLogService.getLogsWithPagination
|
||||
.mockResolvedValueOnce({
|
||||
logs: [{ value: null }],
|
||||
bookmark: 'next_page',
|
||||
})
|
||||
.mockResolvedValueOnce({ logs: [], bookmark: '' });
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockLogService.getLogsWithPagination).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogService.getLogsWithPagination).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
25,
|
||||
'',
|
||||
);
|
||||
expect(mockLogService.getLogsWithPagination).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
25,
|
||||
'next_page',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when blockchain service fails', async () => {
|
||||
mockLogService.getLogsWithPagination.mockRejectedValue(
|
||||
new Error('Blockchain error'),
|
||||
);
|
||||
|
||||
await expect(service.storeAuditTrail()).rejects.toThrow(
|
||||
'Failed to store audit trail',
|
||||
);
|
||||
|
||||
// Verify error was sent via WebSocket
|
||||
expect(mockAuditGateway.sendError).toHaveBeenCalledWith({
|
||||
status: 'ERROR',
|
||||
message: 'Blockchain error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tamper detection logic', () => {
|
||||
it('should mark as non_tampered when delete event and no DB row', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_999',
|
||||
event: 'rekam_medis_deleted',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'hash',
|
||||
},
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue(null);
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
// When delete event and no DB row, should be non_tampered
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
const transactionCall = mockPrisma.$transaction.mock.calls[0][0];
|
||||
// Transaction is called with array of upsert promises
|
||||
expect(transactionCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('should mark as tampered when no DB row and not delete event', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_999',
|
||||
event: 'rekam_medis_created',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'hash',
|
||||
},
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue(null);
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
// Transaction should be called with tampered result
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should mark as non_tampered when delete event and deleted_status is DELETED', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_123',
|
||||
event: 'rekam_medis_deleted',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'hash',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRekamMedis = {
|
||||
id_visit: '123',
|
||||
deleted_status: 'DELETED',
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue(mockRekamMedis);
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should mark as tampered when hashes do not match', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_123',
|
||||
event: 'rekam_medis_created',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
user_id: 1,
|
||||
payload: 'blockchain_hash',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRekamMedis = {
|
||||
id_visit: '123',
|
||||
anamnese: 'modified',
|
||||
jenis_kasus: 'test',
|
||||
tindak_lanjut: 'test',
|
||||
deleted_status: null,
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue(mockRekamMedis);
|
||||
mockRekamMedisService.createHashingPayload.mockReturnValue(
|
||||
'different_hash',
|
||||
);
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should skip log entries without value', async () => {
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [{ noValue: true }],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).not.toHaveBeenCalled();
|
||||
expect(mockAuditGateway.sendComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip log entries without id', async () => {
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [{ value: { event: 'test' } }],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip log entries without payload', async () => {
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [{ value: { id: 'REKAM_1', event: 'test' } }],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip log entries with unknown prefix', async () => {
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [{ value: { id: 'UNKNOWN_1', event: 'test', payload: 'hash' } }],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle invalid timestamp gracefully', async () => {
|
||||
const mockLog = {
|
||||
value: {
|
||||
id: 'REKAM_123',
|
||||
event: 'rekam_medis_created',
|
||||
timestamp: 'invalid-date',
|
||||
user_id: 1,
|
||||
payload: 'hash',
|
||||
},
|
||||
};
|
||||
|
||||
mockLogService.getLogsWithPagination.mockResolvedValueOnce({
|
||||
logs: [mockLog],
|
||||
bookmark: '',
|
||||
});
|
||||
mockRekamMedisService.getRekamMedisById.mockResolvedValue({
|
||||
id_visit: '123',
|
||||
deleted_status: null,
|
||||
});
|
||||
mockRekamMedisService.createHashingPayload.mockReturnValue('hash');
|
||||
mockPrisma.$transaction.mockResolvedValue([]);
|
||||
|
||||
await service.storeAuditTrail();
|
||||
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { LogService } from '../log/log.service';
|
||||
import { ObatService } from '../obat/obat.service';
|
||||
|
|
@ -39,7 +43,13 @@ export class AuditService {
|
|||
pageSize: number,
|
||||
type?: string,
|
||||
tampered?: string,
|
||||
orderBy?: string,
|
||||
order?: 'asc' | 'desc',
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Fetching audit trails: page=${page}, pageSize=${pageSize}, type=${type}`,
|
||||
);
|
||||
|
||||
if (type === 'all' || type === 'initial') {
|
||||
type = undefined;
|
||||
} else if (type === 'rekam_medis') {
|
||||
|
|
@ -54,10 +64,13 @@ export class AuditService {
|
|||
tampered = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const auditLogs = await this.prisma.audit.findMany({
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
orderBy: { timestamp: 'asc' },
|
||||
orderBy: orderBy
|
||||
? { [orderBy]: order || 'asc' }
|
||||
: { timestamp: 'desc' },
|
||||
where: {
|
||||
id: type && type !== 'all' ? { startsWith: type } : undefined,
|
||||
result: tampered ? (tampered as ResultStatus) : undefined,
|
||||
|
|
@ -77,6 +90,10 @@ export class AuditService {
|
|||
...auditLogs,
|
||||
totalCount: count,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch audit trails', error.stack);
|
||||
throw new InternalServerErrorException('Failed to fetch audit trails');
|
||||
}
|
||||
}
|
||||
|
||||
async storeAuditTrail() {
|
||||
|
|
@ -113,7 +130,7 @@ export class AuditService {
|
|||
).filter((record): record is AuditRecordPayload => record !== null);
|
||||
|
||||
if (records.length > 0) {
|
||||
console.log(records);
|
||||
this.logger.debug(`Processing ${records.length} audit records`);
|
||||
await this.prisma.$transaction(
|
||||
records.map((record) =>
|
||||
this.prisma.audit.upsert({
|
||||
|
|
@ -142,50 +159,41 @@ export class AuditService {
|
|||
bookmark = nextBookmark;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error storing audit trail:', error);
|
||||
throw error;
|
||||
this.logger.error('Error storing audit trail', error.stack);
|
||||
this.auditGateway.sendError({
|
||||
status: 'ERROR',
|
||||
message: error.message || 'Failed to store audit trail',
|
||||
});
|
||||
throw new InternalServerErrorException('Failed to store audit trail');
|
||||
}
|
||||
}
|
||||
|
||||
async getCountAuditTamperedData() {
|
||||
const auditTamperedCount = await this.prisma.audit.count({
|
||||
where: {
|
||||
result: 'tampered',
|
||||
},
|
||||
});
|
||||
try {
|
||||
const [
|
||||
auditTamperedCount,
|
||||
auditNonTamperedCount,
|
||||
rekamMedisTamperedCount,
|
||||
tindakanDokterTamperedCount,
|
||||
obatTamperedCount,
|
||||
] = await Promise.all([
|
||||
this.prisma.audit.count({
|
||||
where: { result: 'tampered' },
|
||||
}),
|
||||
this.prisma.audit.count({
|
||||
where: { result: 'non_tampered' },
|
||||
}),
|
||||
this.prisma.audit.count({
|
||||
where: { result: 'tampered', id: { startsWith: 'REKAM' } },
|
||||
}),
|
||||
this.prisma.audit.count({
|
||||
where: { result: 'tampered', id: { startsWith: 'TINDAKAN' } },
|
||||
}),
|
||||
this.prisma.audit.count({
|
||||
where: { result: 'tampered', id: { startsWith: 'OBAT' } },
|
||||
}),
|
||||
]);
|
||||
|
||||
const auditNonTamperedCount = await this.prisma.audit.count({
|
||||
where: {
|
||||
result: 'non_tampered',
|
||||
},
|
||||
});
|
||||
|
||||
const rekamMedisTamperedCount = await this.prisma.audit.count({
|
||||
where: {
|
||||
result: 'tampered',
|
||||
id: {
|
||||
startsWith: 'REKAM',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tindakanDokterTamperedCount = await this.prisma.audit.count({
|
||||
where: {
|
||||
result: 'tampered',
|
||||
id: {
|
||||
startsWith: 'TINDAKAN',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const obatTamperedCount = await this.prisma.audit.count({
|
||||
where: {
|
||||
result: 'tampered',
|
||||
id: {
|
||||
startsWith: 'OBAT',
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
auditTamperedCount,
|
||||
auditNonTamperedCount,
|
||||
|
|
@ -193,6 +201,10 @@ export class AuditService {
|
|||
tindakanDokterTamperedCount,
|
||||
obatTamperedCount,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get audit tampered count', error.stack);
|
||||
throw new InternalServerErrorException('Failed to get audit statistics');
|
||||
}
|
||||
}
|
||||
|
||||
private async buildAuditRecord(
|
||||
|
|
@ -270,7 +282,9 @@ export class AuditService {
|
|||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to resolve related data for log ${logId}:`, err);
|
||||
this.logger.warn(
|
||||
`Failed to resolve related data for log ${logId}: ${err.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
let isNotTampered = false;
|
||||
|
|
@ -295,7 +309,6 @@ export class AuditService {
|
|||
progress_count: index ?? 0,
|
||||
};
|
||||
|
||||
// this.logger.log('Mengirim progres via WebSocket:', progressData);
|
||||
this.auditGateway.sendProgress(progressData);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ interface AuditLogEntry extends BlockchainLog {
|
|||
tamperedLabel: string;
|
||||
last_sync: string;
|
||||
isTampered: boolean;
|
||||
timestamp: string;
|
||||
txId?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ export const SORT_OPTIONS = {
|
|||
created_at: "Waktu Dibuat",
|
||||
processed_at: "Waktu Diproses",
|
||||
},
|
||||
AUDIT_TRAIL: {
|
||||
last_sync: "Last Sync",
|
||||
timestamp: "Timestamp",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const REKAM_MEDIS_TABLE_COLUMNS = [
|
||||
|
|
@ -302,6 +306,7 @@ export const AUDIT_TABLE_COLUMNS = [
|
|||
{ key: "last_sync", label: "Last Sync", class: "text-dark" },
|
||||
{ key: "userId", label: "User ID", class: "text-dark" },
|
||||
{ key: "status", label: "Status Data", class: "text-dark" },
|
||||
{ key: "timestamp", label: "Timestamp", class: "text-dark" },
|
||||
] satisfies Array<{
|
||||
key: keyof AuditLogEntry;
|
||||
label: string;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
DEBOUNCE_DELAY,
|
||||
ITEMS_PER_PAGE_OPTIONS,
|
||||
AUDIT_TABLE_COLUMNS,
|
||||
SORT_OPTIONS,
|
||||
} from "../../../constants/pagination";
|
||||
import ButtonDark from "../../../components/dashboard/ButtonDark.vue";
|
||||
import DialogConfirm from "../../../components/DialogConfirm.vue";
|
||||
|
|
@ -23,6 +24,7 @@ import type {
|
|||
AuditLogType,
|
||||
} from "../../../constants/interfaces";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import SortDropdown from "../../../components/dashboard/SortDropdown.vue";
|
||||
|
||||
interface AuditLogResponse {
|
||||
data: AuditLogEntry[];
|
||||
|
|
@ -32,6 +34,10 @@ interface AuditLogResponse {
|
|||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const api = useApi();
|
||||
const sortBy = ref("last_sync");
|
||||
const sortOrder = ref<"asc" | "desc">(
|
||||
(route.query.order as "asc" | "desc") || "desc"
|
||||
);
|
||||
const { debounce } = useDebounce();
|
||||
|
||||
const pagination = usePagination({
|
||||
|
|
@ -47,6 +53,17 @@ const filters = ref({
|
|||
tampered: (route.query.tampered as string) || "initial",
|
||||
});
|
||||
|
||||
const handleSortChange = (newSortBy: string) => {
|
||||
sortBy.value = newSortBy;
|
||||
pagination.reset();
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
console.log("Toggling sort order from", sortOrder.value);
|
||||
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
|
||||
};
|
||||
|
||||
const formatTimestamp = (rawValue?: string) => {
|
||||
if (!rawValue) {
|
||||
return "-";
|
||||
|
|
@ -184,10 +201,16 @@ const updateQueryParams = () => {
|
|||
query.search = searchId.value;
|
||||
}
|
||||
|
||||
if (sortBy.value !== "initial") {
|
||||
query.sortBy = sortBy.value;
|
||||
}
|
||||
|
||||
if (filters.value.type !== "all") {
|
||||
query.type = filters.value.type;
|
||||
}
|
||||
|
||||
query.order = sortOrder.value;
|
||||
|
||||
if (filters.value.tampered !== "all") {
|
||||
query.tampered = filters.value.tampered;
|
||||
}
|
||||
|
|
@ -200,6 +223,8 @@ const fetchData = async () => {
|
|||
const params = new URLSearchParams({
|
||||
page: pagination.page.value.toString(),
|
||||
pageSize: pageSize.value.toString(),
|
||||
orderBy: sortBy.value,
|
||||
order: sortOrder.value,
|
||||
});
|
||||
|
||||
if (searchId.value) {
|
||||
|
|
@ -265,6 +290,11 @@ const handleResetFilters = () => {
|
|||
fetchData();
|
||||
};
|
||||
|
||||
watch(sortOrder, () => {
|
||||
pagination.reset();
|
||||
fetchData();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pagination.page.value,
|
||||
() => {
|
||||
|
|
@ -415,11 +445,40 @@ onBeforeUnmount(() => {
|
|||
<div
|
||||
class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 px-4 pt-4 pb-2"
|
||||
>
|
||||
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
|
||||
<SearchInput
|
||||
v-model="searchId"
|
||||
placeholder="Cari berdasarkan ID Log"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<div class="flex items-center gap-2 md:ml-4">
|
||||
<SortDropdown
|
||||
v-model="sortBy"
|
||||
:options="SORT_OPTIONS.AUDIT_TRAIL"
|
||||
label="Urut berdasarkan:"
|
||||
@change="handleSortChange"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50"
|
||||
@click="toggleSortOrder"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
class="inline-block size-4"
|
||||
>
|
||||
<path d="M7 7l3 -3l3 3"></path>
|
||||
<path d="M10 4v16"></path>
|
||||
<path d="M17 17l-3 3l-3 -3"></path>
|
||||
<path d="M14 20v-16"></path>
|
||||
</svg>
|
||||
<span class="ml-2 uppercase">{{ sortOrder }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogConfirm
|
||||
ref="auditDialog"
|
||||
title="Konfirmasi"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user