Files
emdash-patch-imageupload/packages/core/tests/unit/auth/mcp-discovery-post.test.ts
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

189 lines
5.4 KiB
TypeScript

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<AuthMiddlewareModule["onRequest"]>[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" },
});
});
});