Files
computer-playbook/roles/web-app-moodle/files/playwright/test-mcp-guest.js
kevinveenbirkenbachandClaude Opus 5 7d8e7e87ff test(mcp): prove a switched-off surface is actually gone
Every role-local MCP spec gates itself on the service flag and skips when the surface is off, so nothing ever checked that switching it off removes it. A surface still serving after the flag went false was indistinguishable from one that was never deployed: both read as a green skip. The gating helper had no inverse at all, so there was no way to assert what a disabled service must not do.

A shared assertion now probes the endpoint with both a GET and a POST while the flag is false and fails on any answer that shows something listening. It counts 401 and 403 as listening, which is the point: a refused credential still means the surface is there. The check is transport-agnostic because the roles differ in their enabled-state probes, over SSE, Streamable HTTP or plain JSON-RPC, while an endpoint that is gone answers none of them. Each role passes its endpoint URL as a thunk, since some resolve it only once a test is running.

The coverage lint requires the registration, and derives the role set from meta/mcp.yml rather than from a filename convention, which is how the tenth role surfaced: its spec is not named like the other nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:40:15 +02:00

43 lines
1.6 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { decodeDotenvQuotedValue } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { registerMcpDisabledState } = require("./mcp-endpoint");
const mcpEndpointPath = decodeDotenvQuotedValue(process.env.MCP_ENDPOINT_PATH || "");
exports.register = function (shared) {
test("guest: the MCP endpoint rejects unauthenticated access", async ({ page }) => {
skipUnlessServiceEnabled("mcp");
expect(mcpEndpointPath, "MCP_ENDPOINT_PATH must be set in the Playwright env file").toBeTruthy();
const endpointUrl = new URL(mcpEndpointPath, shared.env.moodleBaseUrl).toString();
const response = await page.request.post(endpointUrl, {
failOnStatusCode: false,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
data: { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} },
});
const status = response.status();
const body = await response.text();
const excerpt = body.slice(0, 200);
expect(status, `${endpointUrl} must be served while the MCP service is enabled`).not.toBe(404);
expect(
body.includes('"result"'),
`an unauthenticated MCP probe must not be served a JSON-RPC result, got ${excerpt}`,
).toBe(false);
expect(
status >= 400 || body.includes('"error"'),
`an unauthenticated MCP probe must be rejected, got ${status} with ${excerpt}`,
).toBe(true);
});
registerMcpDisabledState(() =>
new URL(mcpEndpointPath, shared.env.moodleBaseUrl).toString(),
);
};