feat(api): add field encryption, health check specs, and KYC encryption script

- Add field-level encryption service for PII data with AES-256-GCM
- Add health check specs for Prisma and Redis indicators
- Add MCP controller specs
- Add encrypt-existing-kyc migration script for existing KYC data

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 09:44:00 +07:00
parent e927385ed5
commit 2250e17a09
10 changed files with 592 additions and 5 deletions

View File

@@ -0,0 +1,120 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { McpTransportController } from '../mcp-transport.controller';
// Mock SSEServerTransport as a class
vi.mock('@goodgo/mcp-servers', () => {
return {
SSEServerTransport: class MockSSEServerTransport {
sessionId = 'mock-session-id';
handlePostMessage = vi.fn().mockResolvedValue(undefined);
constructor(public path: string, public res: unknown) {}
},
};
});
describe('McpTransportController', () => {
let controller: McpTransportController;
let mockRegistry: {
getServerNames: ReturnType<typeof vi.fn>;
getServer: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
vi.clearAllMocks();
mockRegistry = {
getServerNames: vi.fn(),
getServer: vi.fn(),
};
controller = new McpTransportController(mockRegistry as any);
});
describe('listServers', () => {
it('returns list of server names from registry', () => {
mockRegistry.getServerNames.mockReturnValue(['search', 'listings']);
const result = controller.listServers();
expect(result).toEqual({ servers: ['search', 'listings'] });
expect(mockRegistry.getServerNames).toHaveBeenCalledOnce();
});
it('returns empty array when no servers registered', () => {
mockRegistry.getServerNames.mockReturnValue([]);
const result = controller.listServers();
expect(result).toEqual({ servers: [] });
});
});
describe('handleSse', () => {
const mockUser = { sub: 'user-1', email: 'test@example.com' };
let mockReq: { on: ReturnType<typeof vi.fn> };
let mockRes: Record<string, unknown>;
beforeEach(() => {
mockReq = { on: vi.fn() };
mockRes = {};
});
it('throws NOT_FOUND when server does not exist', async () => {
mockRegistry.getServer.mockReturnValue(null);
await expect(
controller.handleSse('nonexistent', mockUser as any, mockReq as any, mockRes as any),
).rejects.toThrow(HttpException);
try {
await controller.handleSse('nonexistent', mockUser as any, mockReq as any, mockRes as any);
} catch (error) {
expect((error as HttpException).getStatus()).toBe(HttpStatus.NOT_FOUND);
expect((error as HttpException).message).toContain('nonexistent');
}
});
it('creates transport and connects to server', async () => {
const mockServer = { connect: vi.fn().mockResolvedValue(undefined) };
mockRegistry.getServer.mockReturnValue(mockServer);
await controller.handleSse('search', mockUser as any, mockReq as any, mockRes as any);
expect(mockRegistry.getServer).toHaveBeenCalledWith('search');
expect(mockServer.connect).toHaveBeenCalledOnce();
expect(mockReq.on).toHaveBeenCalledWith('close', expect.any(Function));
});
});
describe('handleMessage', () => {
const mockUser = { sub: 'user-1', email: 'test@example.com' };
it('throws BAD_REQUEST when sessionId query parameter is missing', async () => {
const mockReq = { query: {} } as any;
const mockRes = {} as any;
await expect(
controller.handleMessage('search', mockUser as any, mockReq, mockRes),
).rejects.toThrow(HttpException);
try {
await controller.handleMessage('search', mockUser as any, mockReq, mockRes);
} catch (error) {
expect((error as HttpException).getStatus()).toBe(HttpStatus.BAD_REQUEST);
}
});
it('throws NOT_FOUND when session does not exist', async () => {
const mockReq = { query: { sessionId: 'nonexistent-session' } } as any;
const mockRes = {} as any;
await expect(
controller.handleMessage('search', mockUser as any, mockReq, mockRes),
).rejects.toThrow(HttpException);
try {
await controller.handleMessage('search', mockUser as any, mockReq, mockRes);
} catch (error) {
expect((error as HttpException).getStatus()).toBe(HttpStatus.NOT_FOUND);
}
});
});
});