Files
mcpedia/apps/mcp/src/auth.test.ts
T
asepharyana 76f778c10d
CI / typecheck + build (turbo) (push) Canceled after 0s
chore(testing): Phase 9 — bun:test suite + CI gating with no-DB fakes
Added a real test suite (32 tests, 0 external services) using bun:test with
in-process module mocking for @mcpedia/db, @mcpedia/queue, @mcpedia/core.

Enablers:
- apps/api: extracted createApp(deps?) factory + dashboard.ts module from
  index.ts so the HTTP surface is unit-testable (real queue is lazy-imported).
- packages/core: exported shouldCreateRevision pure predicate; restoreRevision
  gained an opts.reindex seam for the chunk-rebuild contract.
- apps/mcp: renamed smoke.test.ts -> smoke.ts (bun test now owns .test.ts),
  updated stale assertions (10 tools, 4 docs in docs section).
- infra: turbo test task (cache:false), test scripts across packages,
  @types/bun + tsconfig base types, CI 'Test' step after Build.

Packages with tests: embeddings(5), parser(5), search(8), core(4),
mcp(6 auth-gates), api(8 contracts).

All green: typecheck(4/4), test(6/6 pkgs), build(web). Live API verified
/health, /metrics, /dashboard, /hooks/* auth gate on temp port.
2026-08-20 11:12:32 +07:00

172 lines
6.0 KiB
TypeScript

import { test, expect, mock } from "bun:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
// -----------------------------------------------------------------------
// Mock the heavy dependencies so the MCP server is fully testable without
// Postgres / Redis / embeddings. We record calls to the mutating functions
// so we can assert the auth gate is (or isn't) the reason a tool errors.
// -----------------------------------------------------------------------
const calls = {
enqueueIndexDoc: [] as Array<[string, string]>,
enqueueFullIndex: [] as Array<[string]>,
restoreRevision: [] as Array<[string]>,
};
mock.module("@mcpedia/queue", () => ({
enqueueIndexDoc: (relPath: string, reason: string) => {
calls.enqueueIndexDoc.push([relPath, reason]);
return Promise.resolve({ id: `doc__${relPath}` });
},
enqueueFullIndex: (reason: string) => {
calls.enqueueFullIndex.push([reason]);
return Promise.resolve({ id: `full__${Date.now()}` });
},
getQueue: () => ({
getWaitingCount: () => Promise.resolve(0),
getActiveCount: () => Promise.resolve(0),
getCompletedCount: () => Promise.resolve(0),
getFailedCount: () => Promise.resolve(0),
getDelayedCount: () => Promise.resolve(0),
}),
INDEX_QUEUE: "mcpedia-index",
}));
mock.module("@mcpedia/core", () => ({
keywordSearch: () => Promise.resolve([]),
getDocument: () => Promise.resolve(null),
listDocuments: () => Promise.resolve([]),
getRelated: () => Promise.resolve([]),
semanticSearch: () => Promise.resolve([]),
hybridSearch: () => Promise.resolve([]),
listRevisions: () => Promise.resolve([]),
getRevision: () => Promise.resolve(null),
restoreRevision: (id: string) => {
calls.restoreRevision.push([id]);
return Promise.resolve({ slug: "docs/test", documentId: "d1" });
},
readContentFile: () => "",
}));
// Import AFTER mocking so the modules resolve to our fakes.
const { createMcpServer } = await import("../src/index");
async function connect(secret?: string) {
const server = createMcpServer(secret);
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
await server.connect(serverT);
const client = new Client({ name: "test", version: "0.0.1" });
await client.connect(clientT);
return { server, client };
}
function reset() {
calls.enqueueIndexDoc.length = 0;
calls.enqueueFullIndex.length = 0;
calls.restoreRevision.length = 0;
}
// --- Auth gates on write tools ---
test("index_document without auth secret -> tool returns isError", async () => {
reset();
const { client, server } = await connect(); // no secret
const res = await client.callTool({
name: "index_document",
arguments: { slug: "docs/foo" },
});
expect(res.isError).toBe(true);
expect(calls.enqueueIndexDoc).toHaveLength(0);
await client.close();
await server.close();
});
test("index_document WITH auth secret -> enqueues job", async () => {
reset();
const { client, server } = await connect("real-secret");
const res = await client.callTool({
name: "index_document",
arguments: { slug: "docs/foo" },
});
expect(res.isError).toBeFalsy();
const text = (res.content as any[])[0].text;
const parsed = JSON.parse(text);
expect(parsed.ok).toBe(true);
expect(parsed.jobId).toBeTruthy();
expect(calls.enqueueIndexDoc).toHaveLength(1);
expect(calls.enqueueIndexDoc[0][0]).toBe("docs/foo.md");
await client.close();
await server.close();
});
test("reindex_all without auth -> isError; with auth -> enqueues", async () => {
reset();
const { client, server } = await connect();
let res = await client.callTool({ name: "reindex_all", arguments: {} });
expect(res.isError).toBe(true);
expect(calls.enqueueFullIndex).toHaveLength(0);
await client.close();
await server.close();
const { client: c2, server: s2 } = await connect("real-secret");
res = await c2.callTool({ name: "reindex_all", arguments: {} });
expect(res.isError).toBeFalsy();
expect(calls.enqueueFullIndex).toHaveLength(1);
await c2.close();
await s2.close();
});
test("restore_revision without auth -> isError; with auth -> calls core.restoreRevision", async () => {
reset();
const { client, server } = await connect();
let res = await client.callTool({
name: "restore_revision",
arguments: { id: "rev-123" },
});
expect(res.isError).toBe(true);
expect(calls.restoreRevision).toHaveLength(0);
await client.close();
await server.close();
const { client: c2, server: s2 } = await connect("real-secret");
res = await c2.callTool({
name: "restore_revision",
arguments: { id: "rev-123" },
});
expect(res.isError).toBeFalsy();
expect(calls.restoreRevision).toHaveLength(1);
expect(calls.restoreRevision[0][0]).toBe("rev-123");
await c2.close();
await s2.close();
});
test("queue_status is public (no auth needed)", async () => {
const { client, server } = await connect(); // no secret
const res = await client.callTool({ name: "queue_status", arguments: {} });
expect(res.isError).toBeFalsy();
const text = (res.content as any[])[0].text;
expect(JSON.parse(text).queue).toBe("mcpedia-index");
await client.close();
await server.close();
});
test("tool discovery works without auth (read tools present)", async () => {
const { client, server } = await connect();
const tools = await client.listTools();
const names = tools.tools.map((t) => t.name).sort();
expect(names).toContain("search_documents");
expect(names).toContain("get_document");
expect(names).toContain("list_documents");
expect(names).toContain("semantic_search");
expect(names).toContain("hybrid_search");
expect(names).toContain("get_related_documents");
// Write tools are registered regardless of secret — the gate is in the
// handler, not registration.
expect(names).toContain("index_document");
expect(names).toContain("reindex_all");
expect(names).toContain("restore_revision");
expect(names).toContain("queue_status");
await client.close();
await server.close();
});