muddle/tests/modules.test.ts
2026-10-23 09:12:49 -04:00

141 lines
4.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { astroComponent } from "../modules/astro/index.ts";
import { TemplateContext } from "../core/context.ts";
import { createViews } from "../views/api.ts";
import {
authFromRequest,
authGuard,
authIntegration,
} from "../integrations/auth.ts";
import { App } from "../core/app.ts";
import { resolveConfig } from "../core/config.ts";
import { renderMdx } from "../modules/mdx/index.ts";
import { compileCss } from "../views/css/index.ts";
test("Astro imports compose with typed props and SPUG slots", async () => {
const root = await mkdtemp(join(tmpdir(), "astro-compose-"));
try {
await writeFile(
join(root, "Title.astro"),
"---\nconst { title } = Astro.props;\n---\n<h2>{title}</h2>",
);
await writeFile(
join(root, "Card.astro"),
'---\nimport Title from "./Title.astro";\nconst { title } = Astro.props;\n---\n<article><Title title={title} /><slot /></article>',
);
await writeFile(
join(root, "index.spug"),
'component Card\n title: "Composed"\n render content\n p Slot body',
);
const context = new TemplateContext().import(
"Card",
astroComponent(join(root, "Card.astro")),
);
const views = createViews({ viewsDir: root, context });
const html = await views.renderFile("index");
expect(html).toContain("<h2>Composed</h2>");
expect(html).toContain("<p>Slot body</p>");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("form authentication issues an opaque session and rejects forged identity cookies", async () => {
const app = await App.create({
config: resolveConfig(),
integrations: [
authIntegration({
login: (email, password) =>
password === "secret" ? { id: email } : undefined,
}),
],
});
const response = await app.fetch(
new Request("https://example.test/auth/login", {
method: "POST",
headers: { origin: "https://example.test" },
body: new URLSearchParams({ email: "ada", password: "secret" }),
}),
);
expect(response.status).toBe(302);
const cookie = response.headers.get("set-cookie")!;
expect(
(
await authFromRequest(
new Request("https://example.test", { headers: { cookie } }),
)
)?.id,
).toBe("ada");
expect(
await authFromRequest(
new Request("https://example.test", {
headers: { cookie: "muddle_auth=%7B%22id%22%3A%22admin%22%7D" },
}),
),
).toBeUndefined();
});
test("OAuth callback validates browser-bound state and exchanges a PKCE code", async () => {
const original = globalThis.fetch;
let exchanged = false;
globalThis.fetch = (async (
input: string | URL | Request,
init?: RequestInit,
) => {
if (String(input).endsWith("/token")) {
const body = init?.body as URLSearchParams;
expect(body.get("code_verifier")?.length).toBeGreaterThan(40);
exchanged = true;
return Response.json({ access_token: "test-token" });
}
return Response.json({ sub: "oauth-user" });
}) as typeof fetch;
try {
const app = await App.create({
config: resolveConfig(),
integrations: [
authIntegration({
providers: [
{
name: "test",
authorizationUrl: "https://provider.test/authorize",
tokenUrl: "https://provider.test/token",
userInfoUrl: "https://provider.test/user",
clientId: "test",
redirectUri: "https://app.test/auth/test/callback",
identity: (profile) => ({ id: String(profile.sub) }),
},
],
}),
],
});
const start = await app.fetch(new Request("https://app.test/auth/test"));
const authorization = new URL(start.headers.get("location")!);
expect(authorization.searchParams.get("code_challenge_method")).toBe(
"S256",
);
const callback = `https://app.test/auth/test/callback?code=test-code&state=${authorization.searchParams.get("state")}`;
expect((await app.fetch(new Request(callback))).status).toBe(400);
const response = await app.fetch(
new Request(callback, {
headers: { cookie: start.headers.get("set-cookie")! },
}),
);
expect(response.status).toBe(302);
expect(exchanged).toBe(true);
expect(
(
await app.fetch(
new Request(callback, {
headers: { cookie: start.headers.get("set-cookie")! },
}),
)
).status,
).toBe(400);
} finally {
globalThis.fetch = original;
}
});