Files
2026-07-29 12:51:10 +05:30

337 lines
11 KiB
TypeScript

import { expect, test } from "bun:test";
import type { Context } from "@wrnexus/core";
import { createAuthEngine } from "../src/engine.ts";
import { createAuthHttpHandlers } from "../src/http/index.ts";
import { MemoryAuthStore } from "../src/stores/memory.ts";
function context(request: Request): Context {
const values = new Map<string, unknown>();
return {
req: request,
url: new URL(request.url),
params: {},
locals: {},
lang: "en",
t: (key: string) => key,
ip: "127.0.0.1",
user: null,
cookies: {} as Context["cookies"],
localStorage: {} as Context["localStorage"],
session: {
id: () => "http-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("login handler never trusts a browser captchaVerified field", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "http-handler-secret-that-is-longer-than-thirty-two-characters",
captchaThreshold: 0,
});
await engine.register({ email: "captcha@example.com", password: "StrongPassword123" });
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
identifier: "captcha@example.com",
password: "StrongPassword123",
captchaVerified: true,
}),
}),
);
const response = await handlers.login(ctx);
expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({ code: "captcha-required" });
});
test("server-populated CAPTCHA verification permits the login attempt", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "http-handler-secret-that-is-longer-than-thirty-two-characters",
captchaThreshold: 0,
});
await engine.register({ email: "verified@example.com", password: "StrongPassword123" });
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ identifier: "verified@example.com", password: "StrongPassword123" }),
}),
);
ctx.locals.captcha = { success: true };
const response = await handlers.login(ctx);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ ok: true });
});
test("login API returns a safe actionable invalid-credentials message", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "http-login-message-secret-longer-than-thirty-two-characters",
});
const handlers = createAuthHttpHandlers({ engine });
const response = await handlers.login(
context(
new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
identifier: "missing@example.com",
password: "WrongPassword999",
}),
}),
),
);
expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({
ok: false,
code: "invalid-credentials",
message: "The email, phone, username, or password you entered is incorrect.",
});
});
test("HTTP handlers use navigation hooks configured on the auth engine", async () => {
const calls: string[] = [];
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "engine-navigation-hooks-secret-longer-than-thirty-two-characters",
onSignedIn(ctx, returnTo) {
calls.push(`in:${returnTo}`);
return Response.redirect(new URL(returnTo ?? "/account", ctx.url), 303);
},
onSignedOut(ctx) {
calls.push("out");
return Response.redirect(new URL("/sign-in", ctx.url), 303);
},
});
await engine.register({
email: "navigation@example.com",
password: "StrongPassword123",
});
const handlers = createAuthHttpHandlers({ engine });
const loginResponse = await handlers.login(
context(
new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
identifier: "navigation@example.com",
password: "StrongPassword123",
returnTo: "/dashboard",
}),
}),
),
);
expect(loginResponse.status).toBe(303);
expect(loginResponse.headers.get("location")).toBe("https://example.test/dashboard");
const logoutResponse = await handlers.logout(
context(new Request("https://example.test/api/auth/logout", { method: "POST" })),
);
expect(logoutResponse.status).toBe(303);
expect(logoutResponse.headers.get("location")).toBe("https://example.test/sign-in");
expect(calls).toEqual(["in:/dashboard", "out"]);
});
test("register handler returns validation field errors before calling the engine", async () => {
let registerCalls = 0;
const engine = {
register: async () => {
registerCalls += 1;
return { ok: true };
},
} as unknown as ReturnType<typeof createAuthEngine>;
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ displayName: "A", email: "bad", password: "short" }),
}),
);
const response = await handlers.register(ctx);
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({
ok: false,
errors: {
displayName: expect.any(String),
email: expect.any(String),
password: expect.any(String),
},
});
expect(registerCalls).toBe(0);
});
test("successful signup redirects to sign-in by default", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "default-signup-redirect-secret-longer-than-thirty-two-characters",
});
const handlers = createAuthHttpHandlers({ engine });
const response = await handlers.register(
context(
new Request("https://example.test/api/auth/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
displayName: "Default Redirect",
email: "default-signup@example.com",
password: "StrongPassword123",
consent: true,
}),
}),
),
);
expect(response.status).toBe(303);
expect(response.headers.get("location")).toBe("https://example.test/sign-in");
});
test("onSuccessfulSignUp can safely auto-sign-in and redirect the new user", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "automatic-signup-login-secret-longer-than-thirty-two-characters",
onSuccessfulSignUp() {
return { autoSignIn: true, redirectTo: "/welcome" };
},
});
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
displayName: "Automatic Login",
email: "automatic-signup@example.com",
password: "StrongPassword123",
consent: true,
}),
}),
);
const response = await handlers.register(ctx);
expect(response.status).toBe(303);
expect(response.headers.get("location")).toBe("https://example.test/welcome");
expect(ctx.locals.authUser).toMatchObject({ displayName: "Automatic Login" });
expect(ctx.locals.authSession).toBeDefined();
});
test("signup auto-sign-in does not bypass verification policy", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "verified-signup-policy-secret-longer-than-thirty-two-characters",
requireVerifiedEmail: true,
onSuccessfulSignUp() {
return { autoSignIn: true, redirectTo: "/account" };
},
});
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
displayName: "Verification Required",
email: "verify-signup@example.com",
password: "StrongPassword123",
consent: true,
}),
}),
);
const response = await handlers.register(ctx);
expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({
ok: false,
code: "email-unverified",
message: "Verify your email address before signing in.",
});
expect(ctx.locals.authSession).toBeUndefined();
});
test("authenticated OTP issue rejects an unlinked destination without throwing", async () => {
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "http-otp-secret-that-is-longer-than-thirty-two-characters",
});
const registered = await engine.register({
email: "owner@example.com",
password: "StrongPassword123",
});
const handlers = createAuthHttpHandlers({ engine });
const ctx = context(
new Request("https://example.test/api/auth/otp", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ method: "email-otp", destination: "other@example.com" }),
}),
);
ctx.user = registered.user!;
ctx.locals.authUser = registered.user!;
const response = await handlers.issueOtp(ctx);
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({ ok: false });
});
test("verification resend is generic and can resolve an unauthenticated identifier", async () => {
const messages: Array<{ template: string }> = [];
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "verification-resend-secret-that-is-longer-than-thirty-two-characters",
delivery: {
async send(message) {
messages.push(message);
},
},
});
await engine.register({
email: "resend@example.com",
password: "StrongPassword123",
});
const handlers = createAuthHttpHandlers({ engine });
const before = messages.length;
const known = await handlers.requestVerification(
context(
new Request("https://example.test/api/auth/verification/request", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "email", identifier: "resend@example.com" }),
}),
),
);
expect(known.status).toBe(200);
expect(await known.json()).toEqual({ ok: true });
expect(messages).toHaveLength(before + 1);
const unknown = await handlers.requestVerification(
context(
new Request("https://example.test/api/auth/verification/request", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "email", identifier: "missing@example.com" }),
}),
),
);
expect(unknown.status).toBe(200);
expect(await unknown.json()).toEqual({ ok: true });
expect(messages).toHaveLength(before + 1);
});