import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("virtual:emdash/auth", () => ({ authenticate: vi.fn() })); vi.mock("virtual:emdash/config", () => ({ default: {} })); vi.mock("astro:middleware", () => ({ defineMiddleware: (handler: unknown) => handler, })); vi.mock("@emdash-cms/auth", () => ({ TOKEN_PREFIXES: {}, generatePrefixedToken: vi.fn(), hashPrefixedToken: vi.fn(), VALID_SCOPES: [], validateScopes: vi.fn(), hasScope: vi.fn(() => false), computeS256Challenge: vi.fn(), Role: { ADMIN: 50 }, })); vi.mock("@emdash-cms/auth/adapters/kysely", () => ({ createKyselyAdapter: vi.fn(() => ({ getUserById: vi.fn(async (id: string) => ({ id, email: "admin@test.com", name: "Admin", role: 50, disabled: 0, })), getUserByEmail: vi.fn(), })), })); type AuthMiddlewareModule = typeof import("../../../src/astro/middleware/auth.js"); let onRequest: AuthMiddlewareModule["onRequest"]; beforeAll(async () => { ({ onRequest } = await import("../../../src/astro/middleware/auth.js")); }); beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); async function runAuthMiddleware(opts: { pathname: string; method?: string; headers?: HeadersInit; sessionUserId?: string | null; siteUrl?: string; }) { const url = new URL(opts.pathname, "https://example.com"); const session = { get: vi.fn().mockResolvedValue(opts.sessionUserId ? { id: opts.sessionUserId } : null), set: vi.fn(), destroy: vi.fn(), }; const next = vi.fn(async () => new Response("ok")); const response = await onRequest( { url, request: new Request(url, { method: opts.method ?? "POST", headers: opts.headers, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "debug", version: "1.0" }, }, }), }), locals: { emdash: { db: {}, config: opts.siteUrl ? { siteUrl: opts.siteUrl } : {}, }, }, session, redirect: (location: string) => new Response(null, { status: 302, headers: { Location: location }, }), } as Parameters[0], next, ); return { response, next, session }; } describe("MCP discovery auth middleware", () => { it("returns 401 with discovery metadata for unauthenticated MCP POST requests", async () => { const { response, next } = await runAuthMiddleware({ pathname: "/_emdash/api/mcp", headers: { "Content-Type": "application/json" }, }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); expect(response.headers.get("WWW-Authenticate")).toBe( 'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"', ); await expect(response.json()).resolves.toEqual({ error: { code: "NOT_AUTHENTICATED", message: "Not authenticated" }, }); }); it("does not read the session for anonymous MCP POST discovery requests", async () => { const { response, next, session } = await runAuthMiddleware({ pathname: "/_emdash/api/mcp", headers: { "Content-Type": "application/json" }, }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); expect(session.get).not.toHaveBeenCalled(); }); it("uses the configured public origin for anonymous MCP POST discovery responses", async () => { const { response, next } = await runAuthMiddleware({ pathname: "/_emdash/api/mcp", headers: { "Content-Type": "application/json" }, siteUrl: "https://public.example.com", }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); expect(response.headers.get("WWW-Authenticate")).toBe( 'Bearer resource_metadata="https://public.example.com/.well-known/oauth-protected-resource"', ); }); it("returns 401 with discovery metadata for invalid bearer tokens on MCP POST", async () => { const { response, next } = await runAuthMiddleware({ pathname: "/_emdash/api/mcp", headers: { Authorization: "Bearer invalid", "Content-Type": "application/json", }, }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); expect(response.headers.get("WWW-Authenticate")).toBe( 'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"', ); await expect(response.json()).resolves.toEqual({ error: { code: "INVALID_TOKEN", message: "Invalid or expired token" }, }); }); it("rejects MCP POST requests that only have session auth", async () => { const { response, next, session } = await runAuthMiddleware({ pathname: "/_emdash/api/mcp", headers: { "Content-Type": "application/json", "X-EmDash-Request": "1", }, sessionUserId: "user_1", }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); expect(session.get).not.toHaveBeenCalled(); await expect(response.json()).resolves.toEqual({ error: { code: "NOT_AUTHENTICATED", message: "Not authenticated" }, }); }); it("still rejects non-MCP API POST requests without the CSRF header", async () => { const { response, next } = await runAuthMiddleware({ pathname: "/_emdash/api/content/posts", headers: { "Content-Type": "application/json" }, }); expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(403); await expect(response.json()).resolves.toEqual({ error: { code: "CSRF_REJECTED", message: "Missing required header" }, }); }); });