tests: add unit test fo fabric.service.ts. fix: add better handling for connection failure and better handling when disconnecting from fabric on fabric service
This commit is contained in:
parent
7b0873e0da
commit
87e20b6848
|
|
@ -28,7 +28,6 @@ export class AuthGuard implements CanActivate {
|
|||
const payload = await this.jwtService.verifyAsync(jwtToken, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
});
|
||||
console.log(payload);
|
||||
|
||||
if (payload.csrf !== csrfToken) {
|
||||
throw new UnauthorizedException(['Invalid CSRF token']);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, Logger } from '@nestjs/common';
|
||||
import { FabricService } from './fabric.service';
|
||||
import FabricGateway, { fabricGateway } from '../../common/fabric-gateway';
|
||||
|
||||
@Module({
|
||||
providers: [FabricService],
|
||||
providers: [
|
||||
FabricService,
|
||||
Logger,
|
||||
{
|
||||
provide: FabricGateway,
|
||||
useValue: fabricGateway,
|
||||
},
|
||||
],
|
||||
exports: [FabricService],
|
||||
})
|
||||
export class FabricModule {}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,426 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { FabricService } from './fabric.service';
|
||||
import FabricGateway from '@api/common/fabric-gateway';
|
||||
|
||||
describe('FabricService', () => {
|
||||
let service: FabricService;
|
||||
let mockGateway: {
|
||||
connect: jest.Mock;
|
||||
disconnect: jest.Mock;
|
||||
storeLog: jest.Mock;
|
||||
getLogById: jest.Mock;
|
||||
getAllLogs: jest.Mock;
|
||||
getLogsWithPagination: jest.Mock;
|
||||
};
|
||||
let mockLogger: {
|
||||
log: jest.Mock;
|
||||
error: jest.Mock;
|
||||
warn: jest.Mock;
|
||||
debug: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset all mocks before each test
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create mock gateway
|
||||
mockGateway = {
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
storeLog: jest.fn(),
|
||||
getLogById: jest.fn(),
|
||||
getAllLogs: jest.fn(),
|
||||
getLogsWithPagination: jest.fn(),
|
||||
};
|
||||
|
||||
// Create mock logger
|
||||
mockLogger = {
|
||||
log: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FabricService],
|
||||
providers: [
|
||||
FabricService,
|
||||
{
|
||||
provide: FabricGateway,
|
||||
useValue: mockGateway,
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FabricService>(FabricService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
describe('constructor', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// LIFECYCLE HOOKS
|
||||
// =====================================================================
|
||||
|
||||
describe('onModuleInit', () => {
|
||||
it('should connect to Fabric network on module init', async () => {
|
||||
mockGateway.connect.mockResolvedValue(true);
|
||||
|
||||
await service.onModuleInit();
|
||||
|
||||
expect(mockGateway.connect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw error when connection fails', async () => {
|
||||
const connectionError = new Error('Connection refused');
|
||||
mockGateway.connect.mockRejectedValue(connectionError);
|
||||
|
||||
await expect(service.onModuleInit()).rejects.toThrow(
|
||||
'Failed to connect to Fabric network: Connection refused',
|
||||
);
|
||||
expect(mockGateway.connect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should include original error message in thrown error', async () => {
|
||||
const originalError = new Error('ECONNREFUSED: localhost:7051');
|
||||
mockGateway.connect.mockRejectedValue(originalError);
|
||||
|
||||
try {
|
||||
await service.onModuleInit();
|
||||
fail('Should have thrown');
|
||||
} catch (error: any) {
|
||||
expect(error.message).toContain('ECONNREFUSED');
|
||||
expect(error.message).toBe(
|
||||
'Failed to connect to Fabric network: ECONNREFUSED: localhost:7051',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-Error objects gracefully', async () => {
|
||||
mockGateway.connect.mockRejectedValue('String error');
|
||||
|
||||
await expect(service.onModuleInit()).rejects.toThrow(
|
||||
'Failed to connect to Fabric network: Unknown error',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onApplicationShutdown', () => {
|
||||
it('should disconnect from Fabric network on shutdown', async () => {
|
||||
mockGateway.disconnect.mockResolvedValue(undefined);
|
||||
|
||||
await service.onApplicationShutdown();
|
||||
|
||||
expect(mockGateway.disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should disconnect with signal parameter', async () => {
|
||||
mockGateway.disconnect.mockResolvedValue(undefined);
|
||||
|
||||
await service.onApplicationShutdown('SIGTERM');
|
||||
|
||||
expect(mockGateway.disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw error if disconnect fails - just log it', async () => {
|
||||
const disconnectError = new Error('Disconnect failed');
|
||||
mockGateway.disconnect.mockRejectedValue(disconnectError);
|
||||
|
||||
// Should NOT throw - graceful shutdown
|
||||
await expect(service.onApplicationShutdown()).resolves.not.toThrow();
|
||||
expect(mockGateway.disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle non-Error objects during disconnect gracefully', async () => {
|
||||
mockGateway.disconnect.mockRejectedValue('String error');
|
||||
|
||||
await expect(service.onApplicationShutdown()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// storeLog
|
||||
// =====================================================================
|
||||
|
||||
describe('storeLog', () => {
|
||||
const mockStoreLogResult = {
|
||||
transactionId: 'tx123',
|
||||
status: 'COMMITTED',
|
||||
};
|
||||
|
||||
it('should store log with all parameters', async () => {
|
||||
mockGateway.storeLog.mockResolvedValue(mockStoreLogResult);
|
||||
|
||||
const result = await service.storeLog(
|
||||
'log-1',
|
||||
'CREATE',
|
||||
'user-1',
|
||||
'{"data": "test"}',
|
||||
);
|
||||
|
||||
expect(mockGateway.storeLog).toHaveBeenCalledWith(
|
||||
'log-1',
|
||||
'CREATE',
|
||||
'user-1',
|
||||
'{"data": "test"}',
|
||||
);
|
||||
expect(result).toEqual(mockStoreLogResult);
|
||||
});
|
||||
|
||||
it('should propagate errors from gateway', async () => {
|
||||
const storeError = new Error('Transaction failed');
|
||||
mockGateway.storeLog.mockRejectedValue(storeError);
|
||||
|
||||
await expect(
|
||||
service.storeLog('log-1', 'CREATE', 'user-1', '{}'),
|
||||
).rejects.toThrow('Transaction failed');
|
||||
});
|
||||
|
||||
it('should not validate empty id (NO VALIDATION)', async () => {
|
||||
mockGateway.storeLog.mockResolvedValue(mockStoreLogResult);
|
||||
|
||||
// Empty ID passes through without validation
|
||||
await service.storeLog('', 'CREATE', 'user-1', '{}');
|
||||
|
||||
expect(mockGateway.storeLog).toHaveBeenCalledWith(
|
||||
'',
|
||||
'CREATE',
|
||||
'user-1',
|
||||
'{}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not validate empty event (NO VALIDATION)', async () => {
|
||||
mockGateway.storeLog.mockResolvedValue(mockStoreLogResult);
|
||||
|
||||
await service.storeLog('log-1', '', 'user-1', '{}');
|
||||
|
||||
expect(mockGateway.storeLog).toHaveBeenCalledWith(
|
||||
'log-1',
|
||||
'',
|
||||
'user-1',
|
||||
'{}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not validate empty user_id (NO VALIDATION)', async () => {
|
||||
mockGateway.storeLog.mockResolvedValue(mockStoreLogResult);
|
||||
|
||||
await service.storeLog('log-1', 'CREATE', '', '{}');
|
||||
|
||||
expect(mockGateway.storeLog).toHaveBeenCalledWith(
|
||||
'log-1',
|
||||
'CREATE',
|
||||
'',
|
||||
'{}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not validate malformed JSON payload (NO VALIDATION)', async () => {
|
||||
mockGateway.storeLog.mockResolvedValue(mockStoreLogResult);
|
||||
|
||||
// Invalid JSON passes through
|
||||
await service.storeLog('log-1', 'CREATE', 'user-1', 'not-valid-json');
|
||||
|
||||
expect(mockGateway.storeLog).toHaveBeenCalledWith(
|
||||
'log-1',
|
||||
'CREATE',
|
||||
'user-1',
|
||||
'not-valid-json',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// getLogById
|
||||
// =====================================================================
|
||||
|
||||
describe('getLogById', () => {
|
||||
const mockLog = {
|
||||
id: 'log-1',
|
||||
event: 'CREATE',
|
||||
user_id: 'user-1',
|
||||
payload: '{}',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
it('should retrieve log by id', async () => {
|
||||
mockGateway.getLogById.mockResolvedValue(mockLog);
|
||||
|
||||
const result = await service.getLogById('log-1');
|
||||
|
||||
expect(mockGateway.getLogById).toHaveBeenCalledWith('log-1');
|
||||
expect(result).toEqual(mockLog);
|
||||
});
|
||||
|
||||
it('should propagate errors from gateway', async () => {
|
||||
const notFoundError = new Error('Log not found');
|
||||
mockGateway.getLogById.mockRejectedValue(notFoundError);
|
||||
|
||||
await expect(service.getLogById('non-existent')).rejects.toThrow(
|
||||
'Log not found',
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* ISSUE FOUND: No validation for empty or null id.
|
||||
*/
|
||||
it('should not validate empty id (NO VALIDATION)', async () => {
|
||||
mockGateway.getLogById.mockResolvedValue(null);
|
||||
|
||||
await service.getLogById('');
|
||||
|
||||
expect(mockGateway.getLogById).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// getAllLogs
|
||||
// =====================================================================
|
||||
|
||||
describe('getAllLogs', () => {
|
||||
const mockLogs = [
|
||||
{ id: 'log-1', event: 'CREATE' },
|
||||
{ id: 'log-2', event: 'UPDATE' },
|
||||
];
|
||||
|
||||
it('should retrieve all logs', async () => {
|
||||
mockGateway.getAllLogs.mockResolvedValue(mockLogs);
|
||||
|
||||
const result = await service.getAllLogs();
|
||||
|
||||
expect(mockGateway.getAllLogs).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockLogs);
|
||||
});
|
||||
|
||||
it('should return empty array when no logs exist', async () => {
|
||||
mockGateway.getAllLogs.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getAllLogs();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should propagate errors from gateway', async () => {
|
||||
const queryError = new Error('Query failed');
|
||||
mockGateway.getAllLogs.mockRejectedValue(queryError);
|
||||
|
||||
await expect(service.getAllLogs()).rejects.toThrow('Query failed');
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// getLogsWithPagination
|
||||
// =====================================================================
|
||||
|
||||
describe('getLogsWithPagination', () => {
|
||||
const mockPaginatedResult = {
|
||||
records: [{ id: 'log-1' }, { id: 'log-2' }],
|
||||
bookmark: 'next-page-bookmark',
|
||||
fetchedRecordsCount: 2,
|
||||
};
|
||||
|
||||
it('should retrieve logs with pagination', async () => {
|
||||
mockGateway.getLogsWithPagination.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
const result = await service.getLogsWithPagination(10, '');
|
||||
|
||||
expect(mockGateway.getLogsWithPagination).toHaveBeenCalledWith(10, '');
|
||||
expect(result).toEqual(mockPaginatedResult);
|
||||
});
|
||||
|
||||
it('should pass bookmark for subsequent pages', async () => {
|
||||
mockGateway.getLogsWithPagination.mockResolvedValue(mockPaginatedResult);
|
||||
|
||||
await service.getLogsWithPagination(10, 'page-2-bookmark');
|
||||
|
||||
expect(mockGateway.getLogsWithPagination).toHaveBeenCalledWith(
|
||||
10,
|
||||
'page-2-bookmark',
|
||||
);
|
||||
});
|
||||
|
||||
it('should propagate errors from gateway', async () => {
|
||||
const paginationError = new Error('Pagination failed');
|
||||
mockGateway.getLogsWithPagination.mockRejectedValue(paginationError);
|
||||
|
||||
await expect(service.getLogsWithPagination(10, '')).rejects.toThrow(
|
||||
'Pagination failed',
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* ISSUE FOUND: No validation for pageSize.
|
||||
* Negative, zero, or extremely large values pass through.
|
||||
*/
|
||||
it('should not validate zero pageSize (NO VALIDATION)', async () => {
|
||||
mockGateway.getLogsWithPagination.mockResolvedValue({
|
||||
records: [],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.getLogsWithPagination(0, '');
|
||||
|
||||
expect(mockGateway.getLogsWithPagination).toHaveBeenCalledWith(0, '');
|
||||
});
|
||||
|
||||
it('should not validate negative pageSize (NO VALIDATION)', async () => {
|
||||
mockGateway.getLogsWithPagination.mockResolvedValue({
|
||||
records: [],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.getLogsWithPagination(-5, '');
|
||||
|
||||
expect(mockGateway.getLogsWithPagination).toHaveBeenCalledWith(-5, '');
|
||||
});
|
||||
|
||||
it('should not validate extremely large pageSize (NO VALIDATION)', async () => {
|
||||
mockGateway.getLogsWithPagination.mockResolvedValue({
|
||||
records: [],
|
||||
bookmark: '',
|
||||
});
|
||||
|
||||
await service.getLogsWithPagination(999999999, '');
|
||||
|
||||
expect(mockGateway.getLogsWithPagination).toHaveBeenCalledWith(
|
||||
999999999,
|
||||
'',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Code Review Issues', () => {
|
||||
it('should use dependency injection for FabricGateway', () => {
|
||||
expect(service).toBeDefined();
|
||||
// Gateway is now injected, we can test it directly
|
||||
expect(mockGateway.connect).toBeDefined();
|
||||
});
|
||||
|
||||
it('should document that errors are not transformed (MISSING ERROR HANDLING)', async () => {
|
||||
const rawError = new Error('Raw gateway error');
|
||||
mockGateway.storeLog.mockRejectedValue(rawError);
|
||||
|
||||
// Error passes through unchanged - no NestJS exception wrapping
|
||||
await expect(
|
||||
service.storeLog('log-1', 'CREATE', 'user-1', '{}'),
|
||||
).rejects.toThrow('Raw gateway error');
|
||||
});
|
||||
|
||||
it('should accept signal parameter for shutdown logging', async () => {
|
||||
mockGateway.disconnect.mockResolvedValue(undefined);
|
||||
|
||||
// Signal is now logged (though we can't verify without mocking Logger)
|
||||
await service.onApplicationShutdown('SIGTERM');
|
||||
|
||||
expect(mockGateway.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import {
|
|||
|
||||
@Injectable()
|
||||
export class FabricService implements OnModuleInit, OnApplicationShutdown {
|
||||
private readonly logger = new Logger(FabricService.name);
|
||||
private readonly gateway = new FabricGateway();
|
||||
constructor(
|
||||
private readonly gateway: FabricGateway,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log('Attempting to connect to Fabric network...');
|
||||
|
|
@ -17,16 +19,43 @@ export class FabricService implements OnModuleInit, OnApplicationShutdown {
|
|||
await this.gateway.connect();
|
||||
this.logger.log('Successfully connected to Fabric network.');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to connect to Fabric network:', error);
|
||||
throw new Error('Failed to connect to Fabric network');
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error(
|
||||
`Failed to connect to Fabric network: ${errorMessage}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
throw new Error(`Failed to connect to Fabric network: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal?: string) {
|
||||
this.logger.log('Disconnecting from Fabric network...');
|
||||
await this.gateway.disconnect();
|
||||
this.logger.log(
|
||||
`Disconnecting from Fabric network...${signal ? ` (signal: ${signal})` : ''}`,
|
||||
);
|
||||
try {
|
||||
await this.gateway.disconnect();
|
||||
this.logger.log('Successfully disconnected from Fabric network.');
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
this.logger.error(
|
||||
`Failed to disconnect from Fabric network: ${errorMessage}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menyimpan entri log ke blockchain Fabric.
|
||||
*
|
||||
* @param id - ID unik log (harus tidak kosong, divalidasi oleh method pemanggil). Contoh nilai: 'REKAM_12XX'
|
||||
* @param event - Jenis event (harus tidak kosong, divalidasi oleh method pemanggil). Contoh Nilai: 'CREATE'
|
||||
* @param user_id - ID pengguna (harus valid, divalidasi oleh method pemanggil). Contoh Nilai: '1'
|
||||
* @param payload - Payload string berupa Hash dari payload data (method hanya menerima string berupa hash). Contoh Nilai: '4f9075ab9fc724a0xxxx'
|
||||
*
|
||||
* @throws Error if Fabric gateway fails
|
||||
*/
|
||||
async storeLog(id: string, event: string, user_id: string, payload: string) {
|
||||
this.logger.log(`Storing log with ID: ${id}`);
|
||||
return this.gateway.storeLog(id, event, user_id, payload);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user