120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import type { Context } from "@wrnexus/core";
|
|
import { createAuthEngine } from "../src/engine.ts";
|
|
import { setDefaultAuthEngine } from "../src/runtime.ts";
|
|
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
|
import { POST as requestPasswordReset } from "../src/routes/api/password-request.ts";
|
|
import { GET as listSessions } from "../src/routes/api/sessions.ts";
|
|
import { POST as passkeyLoginOptions } from "../src/routes/api/passkeys-login-options.ts";
|
|
|
|
function context(request: Request): Context {
|
|
const values = new Map<string, unknown>();
|
|
return {
|
|
req: request,
|
|
// Deliberately use unrelated values. A route-specific package entry must
|
|
// not infer its endpoint from ctx.url, ctx.req.url, params, or locals.
|
|
url: new URL("https://example.test/__wrnexus/rewritten"),
|
|
params: {},
|
|
locals: {},
|
|
lang: "en",
|
|
t: (key: string) => key,
|
|
ip: "127.0.0.1",
|
|
user: null,
|
|
cookies: {
|
|
get: (name: string) => (name === "wrn-csrf" ? "route-csrf-token" : undefined),
|
|
} as Context["cookies"],
|
|
localStorage: {} as Context["localStorage"],
|
|
session: {
|
|
id: () => "route-test-session",
|
|
get: <T>(key: string) => values.get(key) as T | undefined,
|
|
getAll: () => Object.fromEntries(values),
|
|
set: (key: string, value: unknown) => {
|
|
values.set(key, value);
|
|
},
|
|
delete: (key: string) => {
|
|
values.delete(key);
|
|
},
|
|
regenerate: () => {},
|
|
clear: () => {
|
|
values.clear();
|
|
},
|
|
},
|
|
} as Context;
|
|
}
|
|
|
|
test("route-specific password recovery entry cannot fall through to Not Found", async () => {
|
|
setDefaultAuthEngine(
|
|
createAuthEngine({
|
|
store: new MemoryAuthStore(),
|
|
secret: "route-specific-entry-secret-longer-than-thirty-two-characters",
|
|
}),
|
|
);
|
|
|
|
const request = new Request("https://example.test/completely/unrelated", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-csrf-token": "route-csrf-token",
|
|
},
|
|
body: JSON.stringify({ identifier: "missing@example.test" }),
|
|
});
|
|
|
|
const response = await requestPasswordReset(context(request));
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toEqual({ ok: true });
|
|
});
|
|
|
|
test("package auth routes reject unsafe requests without CSRF verification", async () => {
|
|
const request = new Request("https://example.test/api/auth/password/request", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ identifier: "missing@example.test" }),
|
|
});
|
|
const response = await requestPasswordReset(context(request));
|
|
expect(response.status).toBe(403);
|
|
expect(await response.json()).toMatchObject({ ok: false, error: "Invalid CSRF token" });
|
|
});
|
|
|
|
test("safe package auth routes do not require a CSRF token", async () => {
|
|
const engine = createAuthEngine({
|
|
store: new MemoryAuthStore(),
|
|
secret: "safe-route-secret-that-is-longer-than-thirty-two-characters",
|
|
});
|
|
const registered = await engine.register({
|
|
email: "sessions@example.test",
|
|
password: "StrongPassword123",
|
|
});
|
|
setDefaultAuthEngine(engine);
|
|
const ctx = context(new Request("https://example.test/api/auth/sessions", { method: "GET" }));
|
|
ctx.user = registered.user!;
|
|
ctx.locals.authUser = registered.user!;
|
|
|
|
const response = await listSessions(ctx);
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toMatchObject({ ok: true, sessions: [] });
|
|
});
|
|
|
|
test("passkey routes return a controlled response when no provider is configured", async () => {
|
|
setDefaultAuthEngine(
|
|
createAuthEngine({
|
|
store: new MemoryAuthStore(),
|
|
secret: "missing-passkey-provider-secret-longer-than-thirty-two-characters",
|
|
}),
|
|
);
|
|
const request = new Request("https://example.test/api/auth/passkeys/login/options", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-csrf-token": "route-csrf-token",
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
const response = await passkeyLoginOptions(context(request));
|
|
expect(response.status).toBe(503);
|
|
expect(await response.json()).toMatchObject({
|
|
ok: false,
|
|
code: "passkey-provider-not-configured",
|
|
});
|
|
});
|