test(auth): add unit tests for KYC presigned upload and submit handlers

Cover GenerateKycUploadUrlsHandler (10 tests) and SubmitKycHandler (10 tests):
presigned URL flow, legacy file upload, status validation, error handling.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 13:19:44 +07:00
parent 5810f0be56
commit 57db3fe388
9 changed files with 559 additions and 20 deletions

View File

@@ -5,7 +5,7 @@ import type { ReportsDeps } from '../shared/types';
type ToolResult = { content: { type: string; text: string }[]; isError?: boolean };
function makeDeps(): ReportsDeps {
return { aiServiceBaseUrl: 'http://localhost:8000' };
return { apiBaseUrl: 'http://localhost:3001/api/v1' };
}
function getToolHandler(server: ReturnType<typeof createReportsServer>, name: string) {
@@ -146,7 +146,7 @@ describe('ReportsServer', () => {
});
expect(fetchSpy).toHaveBeenCalledWith(
'http://localhost:8000/reports/generate',
'http://localhost:3001/api/v1/reports/generate',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -229,7 +229,7 @@ describe('ReportsServer', () => {
expect(data.highlights).toHaveLength(2);
});
it('sends correct request body', async () => {
it('sends correct GET request with query params', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
ok: true,
json: async () => ({ province: 'Hồ Chí Minh', data: {}, highlights: [] }),
@@ -243,11 +243,17 @@ describe('ReportsServer', () => {
toYear: 2025,
});
const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string) as Record<string, unknown>;
expect(body.province).toBe('Hồ Chí Minh');
expect(body.categories).toEqual(['fdi', 'infrastructure']);
expect(body.from_year).toBe(2020);
expect(body.to_year).toBe(2025);
const calledUrl = fetchSpy.mock.calls[0][0] as string;
expect(calledUrl).toContain('http://localhost:3001/api/v1/reports/macro-data?');
const url = new URL(calledUrl);
expect(url.searchParams.get('province')).toBe('Hồ Chí Minh');
expect(url.searchParams.getAll('categories')).toEqual(['fdi', 'infrastructure']);
expect(url.searchParams.get('fromYear')).toBe('2020');
expect(url.searchParams.get('toYear')).toBe('2025');
expect(fetchSpy.mock.calls[0][1]).toEqual(
expect.objectContaining({ method: 'GET' }),
);
});
it('returns error on service failure', async () => {

View File

@@ -61,7 +61,7 @@ export class McpRegistryService implements OnModuleInit {
this.servers.set(
'reports',
createReportsServer({
aiServiceBaseUrl: this.options.aiServiceBaseUrl,
apiBaseUrl: this.options.apiBaseUrl ?? this.options.aiServiceBaseUrl,
}),
);
}

View File

@@ -5,6 +5,8 @@ import { MCP_MODULE_OPTIONS } from './mcp.constants';
export interface McpModuleOptions {
aiServiceBaseUrl: string;
/** Base URL for the NestJS API (e.g. http://localhost:3001/api/v1). Used by MCP servers that call NestJS endpoints instead of the AI service. */
apiBaseUrl?: string;
typesenseCollectionName?: string;
/** When true, the built-in McpTransportController is NOT registered — useful when the host app provides its own authenticated controller. */
skipDefaultController?: boolean;

View File

@@ -40,7 +40,7 @@ const GetMacroDataSchema = {
};
export function createReportsServer(deps: ReportsDeps): McpServer {
const baseUrl = deps.aiServiceBaseUrl.replace(/\/$/, '');
const baseUrl = deps.apiBaseUrl.replace(/\/$/, '');
const server = new McpServer({
name: 'goodgo-reports',
@@ -108,15 +108,15 @@ export function createReportsServer(deps: ReportsDeps): McpServer {
'Retrieve macro-economic data (GDP, population, FDI, infrastructure) for a Vietnamese province.',
GetMacroDataSchema,
async (params: z.infer<z.ZodObject<typeof GetMacroDataSchema>>) => {
const response = await fetch(`${baseUrl}/reports/macro-data`, {
method: 'POST',
const qs = new URLSearchParams();
qs.set('province', params.province);
for (const cat of params.categories) qs.append('categories', cat);
qs.set('fromYear', String(params.fromYear));
qs.set('toYear', String(params.toYear));
const response = await fetch(`${baseUrl}/reports/macro-data?${qs.toString()}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
province: params.province,
categories: params.categories,
from_year: params.fromYear,
to_year: params.toYear,
}),
});
if (!response.ok) {

View File

@@ -21,7 +21,7 @@ export interface IndustrialParksDeps {
}
export interface ReportsDeps {
aiServiceBaseUrl: string;
apiBaseUrl: string;
}
export interface McpServerConfig {