mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Merge branch 'fix/v1436-locks' into release/v1.43.6
This commit is contained in:
commit
1fef2c81d0
2 files changed
+47
-13
No files matched your search
@@ -77,7 +77,7 @@ vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: headersGet })),
|
||||
}));
|
||||
|
||||
import { POST, GET } from "../route";
|
||||
import { POST, GET, DELETE } from "../route";
|
||||
import { resolveBearerToken } from "@/lib/auth/bearer";
|
||||
import { isModuleEnabled } from "@/lib/modules/gate";
|
||||
import { checkMcpRateLimit } from "@/lib/rate-limit";
|
||||
@@ -428,24 +428,40 @@ describe("/mcp — write tools gate on health:write", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mcp — GET transport", () => {
|
||||
it("is wired for GET (SSE stream open) behind the same gates", async () => {
|
||||
describe("/mcp — GET and DELETE", () => {
|
||||
const bearer = { authorization: "Bearer hlk_" + "a".repeat(64) };
|
||||
|
||||
it("answers an authenticated GET with 405 and Allow: POST, without spending a rate-limit slot", async () => {
|
||||
validToken();
|
||||
const res = await GET(
|
||||
new Request("http://localhost/mcp", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
authorization: "Bearer hlk_" + "a".repeat(64),
|
||||
accept: "text/event-stream",
|
||||
},
|
||||
headers: { ...bearer, accept: "text/event-stream" },
|
||||
}),
|
||||
);
|
||||
// Auth + gates ran; the transport answers (a stream open or a
|
||||
// method-specific status), never a 401/404/429 on the happy path.
|
||||
expect([200, 405]).toContain(res.status);
|
||||
expect(res.status).toBe(405);
|
||||
expect(res.headers.get("allow")).toBe("POST");
|
||||
expect(resolveBearerToken).toHaveBeenCalledTimes(1);
|
||||
expect(isModuleEnabled).toHaveBeenCalledWith("user-1", "mcp");
|
||||
// Release any opened stream so the test process can exit cleanly.
|
||||
await res.body?.cancel().catch(() => {});
|
||||
expect(isModuleEnabled).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("answers DELETE the same way", async () => {
|
||||
validToken();
|
||||
const res = await DELETE(
|
||||
new Request("http://localhost/mcp", {
|
||||
method: "DELETE",
|
||||
headers: bearer,
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(405);
|
||||
expect(res.headers.get("allow")).toBe("POST");
|
||||
});
|
||||
|
||||
it("still challenges an unauthenticated GET with 401, so discovery works", async () => {
|
||||
const res = await GET(
|
||||
new Request("http://localhost/mcp", { method: "GET" }),
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.headers.get("www-authenticate")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -181,6 +181,24 @@ async function handleMcp(request: Request): Promise<Response> {
|
||||
return unauthorized(request, SCOPE_HEALTH_READ);
|
||||
}
|
||||
|
||||
// The server is stateless and answers every call as one buffered JSON
|
||||
// response, so it offers no server-to-client stream and no session to
|
||||
// end. The transport specification answers GET and DELETE with 405 and
|
||||
// `Allow: POST` in that case, and a client then stops asking. Letting
|
||||
// the SDK open an event stream here closed it again straight away; the
|
||||
// client read that as a dropped connection and reconnected about once a
|
||||
// second, each attempt spending a rate-limit slot and a module lookup.
|
||||
if (request.method !== "POST") {
|
||||
annotate({
|
||||
action: { name: "mcp.method.not_allowed" },
|
||||
meta: { method: request.method },
|
||||
});
|
||||
return Response.json(
|
||||
{ error: "method_not_allowed" },
|
||||
{ status: 405, headers: { Allow: "POST" } },
|
||||
);
|
||||
}
|
||||
|
||||
// 2. RATE LIMIT — per `<userId>:<tokenId>` credential binding.
|
||||
const rl = await checkMcpRateLimit(ctx.binding);
|
||||
if (!rl.allowed) {
|
||||
|
||||
Reference in new issue
Block a user