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
This commit is contained in:
188
packages/core/tests/unit/auth/mcp-discovery-post.test.ts
Normal file
188
packages/core/tests/unit/auth/mcp-discovery-post.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user