test: add jest test suite with controller specs and mocks
- add jest, ts-jest, supertest, and related dev dependencies - add test scripts (test, test:debug, test:watch) to package.json - add jest.config.js with ts-jest transform and module name mappers - add controller specs for auth, browser, environment, mcp, scenario, session - add mocks for jsdom, playwright, and readability - add test/tsconfig.json and exclude test dir from main tsconfig
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { buildTestApp } from './app.harness';
|
||||
|
||||
/**
|
||||
* MCP controller integration tests.
|
||||
*
|
||||
* The MCP endpoint speaks the Model Context Protocol (streamable HTTP
|
||||
* transport). We test:
|
||||
* - that the endpoint is reachable and returns a recognised MCP response
|
||||
* - that tool invocations for read-only, non-browser tools work end-to-end
|
||||
* - that tools with bad inputs return error payloads (not HTTP 5xx)
|
||||
*
|
||||
* Browser-dependent tools (open_url, exec_code) require a live Playwright
|
||||
* session and are not covered here.
|
||||
*/
|
||||
describe('McpController', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await buildTestApp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
/** Send a single MCP tool call and return the parsed response body. */
|
||||
async function mcpCall(toolName: string, args: Record<string, unknown> = {}) {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/mcp')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json, text/event-stream')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/call',
|
||||
params: { name: toolName, arguments: args },
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Connectivity ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /mcp — connectivity', () => {
|
||||
it('is reachable and returns a non-5xx status', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/mcp')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json, text/event-stream')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test', version: '0' },
|
||||
},
|
||||
});
|
||||
expect(res.status).toBeLessThan(500);
|
||||
});
|
||||
});
|
||||
|
||||
// ── list_keys tool ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('list_keys', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
const res = await mcpCall('list_keys');
|
||||
// MCP may respond with 200 (JSON body) or SSE stream; both are acceptable
|
||||
expect(res.status).toBeLessThan(500);
|
||||
|
||||
if (res.status === 200 && res.body?.result) {
|
||||
const text = res.body.result.content?.[0]?.text;
|
||||
expect(() => JSON.parse(text)).not.toThrow();
|
||||
expect(Array.isArray(JSON.parse(text))).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── list_sessions tool ─────────────────────────────────────────────────────
|
||||
|
||||
describe('list_sessions', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
const res = await mcpCall('list_sessions');
|
||||
expect(res.status).toBeLessThan(500);
|
||||
|
||||
if (res.status === 200 && res.body?.result) {
|
||||
const text = res.body.result.content?.[0]?.text;
|
||||
expect(() => JSON.parse(text)).not.toThrow();
|
||||
expect(Array.isArray(JSON.parse(text))).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── list_environments tool ─────────────────────────────────────────────────
|
||||
|
||||
describe('list_environments', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
const res = await mcpCall('list_environments');
|
||||
expect(res.status).toBeLessThan(500);
|
||||
|
||||
if (res.status === 200 && res.body?.result) {
|
||||
const text = res.body.result.content?.[0]?.text;
|
||||
expect(() => JSON.parse(text)).not.toThrow();
|
||||
expect(Array.isArray(JSON.parse(text))).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── create_environment tool ────────────────────────────────────────────────
|
||||
|
||||
describe('create_environment', () => {
|
||||
it('creates an environment via MCP', async () => {
|
||||
const res = await mcpCall('create_environment', {
|
||||
name: 'mcp-test-env',
|
||||
urls: { id_url: 'https://id.example.com' },
|
||||
});
|
||||
expect(res.status).toBeLessThan(500);
|
||||
|
||||
if (res.status === 200 && res.body?.result) {
|
||||
const text = res.body.result.content?.[0]?.text;
|
||||
const created = JSON.parse(text);
|
||||
expect(created.name).toBe('mcp-test-env');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── delete_session tool with unknown id ────────────────────────────────────
|
||||
|
||||
describe('delete_session', () => {
|
||||
it('returns an MCP error result for a non-existent session id', async () => {
|
||||
const res = await mcpCall('delete_session', { id: 999999 });
|
||||
expect(res.status).toBeLessThan(500);
|
||||
// MCP wraps service errors as isError:true content, not HTTP errors
|
||||
if (res.status === 200 && res.body?.result) {
|
||||
expect(res.body.result.isError).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user