/**
* Lambda Handler Tests
* Tests the main Lambda entry point that routes requests across
* UCP, MCP, A2A, and well-known endpoints.
*/
// Set env before import to prevent auto-start of stdio server
process.env['AWS_LAMBDA_FUNCTION_NAME'] = 'test';
vi.mock('../../src/server.js', () => ({
createMCPServer: vi.fn(() => ({ connect: vi.fn() })),
}));
vi.mock('../../src/ucp/profile.js', () => ({
generateProfile: vi.fn(() => ({ version: '2026-01-11', services: [] })),
}));
vi.mock('../../src/rest/router.js', () => ({ routeUCPv1: vi.fn() }));
vi.mock('../../src/mcp/handler.js', () => ({
handleMCPRequest: vi.fn(),
handleMCPOptions: vi.fn(() => ({ statusCode: 204, headers: {}, body: '' })),
}));
vi.mock('../../src/a2a/handler.js', () => ({
handleA2ARequest: vi.fn(),
handleA2AOptions: vi.fn(() => ({ statusCode: 204, headers: {}, body: '' })),
handleA2AGetAgentCard: vi.fn(() => ({
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: '{}',
})),
}));
vi.mock('../../src/middleware/api-auth.js', () => ({
validateApiAuth: vi.fn(() => ({ authenticated: true })),
}));
vi.mock('../../src/types.js', async (importOriginal) => {
const original = (await importOriginal()) as any;
return {
...original,
loadConfig: vi.fn(() => ({
shopify: {
apiKey: '',
apiSecret: '',
storeDomain: '',
accessToken: '',
storefrontToken: '',
},
ap2: { signingPrivateKey: '' },
gateway: {
baseUrl: 'http://localhost:3000',
feeRate: 0.005,
feeWalletAddress: '',
},
dynamodb: {
mandatesTable: 'mandates',
ledgerTable: 'ledger',
sessionsTable: 'sessions',
},
logLevel: 'info',
})),
};
});
import { handler } from '../../src/index.js';
import { routeUCPv1 } from '../../src/rest/router.js';
import { handleMCPRequest, handleMCPOptions } from '../../src/mcp/handler.js';
import {
handleA2ARequest,
handleA2AOptions,
handleA2AGetAgentCard,
} from '../../src/a2a/handler.js';
import { validateApiAuth } from '../../src/middleware/api-auth.js';
// ─── Helpers ───
function makeEvent(method: string, path: string, body?: unknown): any {
return {
httpMethod: method,
path,
headers: {},
body: body ? JSON.stringify(body) : null,
isBase64Encoded: false,
requestContext: {},
queryStringParameters: null,
pathParameters: null,
};
}
const mockContext: any = {
functionName: 'test',
awsRequestId: 'req-1',
getRemainingTimeInMillis: () => 30000,
};
// ─── Tests ───
describe('Lambda handler', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset auth mock to default (authenticated)
vi.mocked(validateApiAuth).mockReturnValue({ authenticated: true });
});
// ── CORS Preflight ──
describe('OPTIONS preflight', () => {
it('OPTIONS /mcp calls handleMCPOptions', async () => {
const result = await handler(makeEvent('OPTIONS', '/mcp'), mockContext);
expect(handleMCPOptions).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(204);
});
it('OPTIONS /a2a calls handleA2AOptions', async () => {
const result = await handler(makeEvent('OPTIONS', '/a2a'), mockContext);
expect(handleA2AOptions).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(204);
});
it('OPTIONS on other path returns 204', async () => {
const result = await handler(
makeEvent('OPTIONS', '/ucp/v1/catalog'),
mockContext,
);
expect(result.statusCode).toBe(204);
// Should not call any specific OPTIONS handler
expect(handleMCPOptions).not.toHaveBeenCalled();
expect(handleA2AOptions).not.toHaveBeenCalled();
});
});
// ── Authentication ──
describe('authentication', () => {
it('auth failure returns 401', async () => {
vi.mocked(validateApiAuth).mockReturnValue({
authenticated: false,
error: 'Missing Authorization header',
});
const result = await handler(
makeEvent('GET', '/ucp/v1/catalog'),
mockContext,
);
expect(result.statusCode).toBe(401);
const body = JSON.parse(result.body);
expect(body.error.code).toBe('unauthorized');
expect(body.error.message).toBe('Missing Authorization header');
});
});
// ── Route Dispatch ──
describe('route dispatch', () => {
it('GET /.well-known/ucp returns UCP profile with 200', async () => {
const result = await handler(
makeEvent('GET', '/.well-known/ucp'),
mockContext,
);
expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.version).toBe('2026-01-11');
expect(body.services).toEqual([]);
});
it('POST /ucp/v1/cart routes to routeUCPv1', async () => {
vi.mocked(routeUCPv1).mockResolvedValue({
statusCode: 201,
body: { cart_id: 'c-42' },
});
const result = await handler(
makeEvent('POST', '/ucp/v1/cart'),
mockContext,
);
expect(routeUCPv1).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(201);
const body = JSON.parse(result.body);
expect(body.cart_id).toBe('c-42');
});
it('POST /mcp routes to handleMCPRequest', async () => {
vi.mocked(handleMCPRequest).mockResolvedValue({
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: '{"result":"ok"}',
});
const result = await handler(
makeEvent('POST', '/mcp', { method: 'tools/list' }),
mockContext,
);
expect(handleMCPRequest).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(200);
});
it('POST /a2a routes to handleA2ARequest', async () => {
vi.mocked(handleA2ARequest).mockResolvedValue({
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: '{"jsonrpc":"2.0","result":{}}',
});
const result = await handler(
makeEvent('POST', '/a2a', { jsonrpc: '2.0', method: 'tasks/send' }),
mockContext,
);
expect(handleA2ARequest).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(200);
});
it('GET /a2a routes to handleA2AGetAgentCard', async () => {
const result = await handler(makeEvent('GET', '/a2a'), mockContext);
expect(handleA2AGetAgentCard).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(200);
});
it('GET /.well-known/agent.json routes to handleA2AGetAgentCard', async () => {
const result = await handler(
makeEvent('GET', '/.well-known/agent.json'),
mockContext,
);
expect(handleA2AGetAgentCard).toHaveBeenCalledTimes(1);
expect(result.statusCode).toBe(200);
});
});
// ── Error Handling ──
describe('error handling', () => {
it('unknown route returns 404', async () => {
const result = await handler(
makeEvent('GET', '/nonexistent'),
mockContext,
);
expect(result.statusCode).toBe(404);
const body = JSON.parse(result.body);
expect(body.error.code).toBe('not_found');
});
it('unhandled error returns 500', async () => {
vi.mocked(routeUCPv1).mockRejectedValue(
new Error('Unexpected database failure'),
);
const result = await handler(
makeEvent('POST', '/ucp/v1/cart'),
mockContext,
);
expect(result.statusCode).toBe(500);
const body = JSON.parse(result.body);
expect(body.error.code).toBe('internal_error');
expect(body.error.message).toBe('Internal server error');
});
});
// ── CORS Headers ──
describe('CORS headers', () => {
it('response includes Access-Control-Allow-Origin header', async () => {
vi.mocked(routeUCPv1).mockResolvedValue({
statusCode: 200,
body: { products: [] },
});
const result = await handler(
makeEvent('GET', '/ucp/v1/catalog'),
mockContext,
);
expect(result.headers['Access-Control-Allow-Origin']).toBeDefined();
// Default ALLOWED_ORIGIN is '*' when env var is not set
expect(result.headers['Access-Control-Allow-Origin']).toBe('*');
});
});
});