Files
WRNexusJS/packages/captcha/test/middleware.test.ts
T
2026-07-29 12:51:10 +05:30

158 lines
4.6 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createContext, type Context } from "@wrnexus/core";
import { captchaGuard, captchaPageGate } from "../src/middleware.ts";
import type { CaptchaEngine } from "../src/types.ts";
function fakeEngine(): CaptchaEngine {
return {
provider: "self-hosted",
basePath: "/api/captcha",
async create() {
throw new Error("not used");
},
async verify() {
throw new Error("not used");
},
async verifyResponseToken(input) {
return input.responseToken === "verified-token"
? { success: true, provider: "self-hosted", action: input.action }
: { success: false, provider: "self-hosted", action: input.action, code: "invalid-input" };
},
async renderAudio() {
return undefined;
},
async gc() {},
};
}
function sessionFixture() {
const values = new Map<string, unknown>();
let sessionId = "session-1";
return {
id(): string {
return sessionId;
},
get<T>(key: string): T | undefined {
return values.get(key) as T | undefined;
},
getAll(): Record<string, unknown> {
return Object.fromEntries(values);
},
set(key: string, value: unknown): void {
values.set(key, value);
},
delete(key: string): void {
values.delete(key);
},
regenerate(): void {
sessionId = `${sessionId}-regenerated`;
},
clear(): void {
values.clear();
},
};
}
describe("CAPTCHA request guard", () => {
test("reuses an action-bound verified session grant for retryable requests", async () => {
const session = sessionFixture();
const guard = captchaGuard({
action: "auth-login",
engine: fakeEngine(),
verifiedForMs: 5 * 60_000,
});
const firstRequest = new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ "wrn-captcha-response": "verified-token" }),
});
const firstContext: Context = {
...createContext(firstRequest, new URL(firstRequest.url)),
session,
ip: "127.0.0.1",
};
const first = await guard(
firstContext,
() => new Response("credentials-invalid", { status: 401 }),
);
expect(first.status).toBe(401);
const retryRequest = new Request("https://example.test/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
});
const retryContext: Context = {
...createContext(retryRequest, new URL(retryRequest.url)),
session,
ip: "127.0.0.1",
};
const retry = await guard(retryContext, () => new Response("retry-allowed"));
expect(await retry.text()).toBe("retry-allowed");
expect(retryContext.locals.captcha).toMatchObject({
success: true,
action: "auth-login",
});
expect(retryContext.locals.captchaVerified).toBe(true);
});
test("rejects invalid verified-session durations", () => {
expect(() =>
captchaGuard({
action: "auth-login",
engine: fakeEngine(),
verifiedForMs: Number.NaN,
}),
).toThrow("verifiedForMs");
});
});
describe("CAPTCHA page gate", () => {
test("accepts a verified form token and grants the protected route", async () => {
const session = sessionFixture();
const gate = captchaPageGate({
action: "protected-page-access",
engine: fakeEngine(),
challengePath: "/captcha",
policy: {
mode: "session",
verifiedForMs: 15 * 60_000,
routeGroups: ["/protected"],
},
});
const body = new URLSearchParams({
returnTo: "/protected",
"wrn-captcha-response": "verified-token",
});
const request = new Request("https://example.test/api/page-grant", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body,
});
const requestUrl = new URL(request.url);
const context: Context = {
...createContext(request, requestUrl),
session,
ip: "127.0.0.1",
};
const granted = await gate(context, () =>
Response.redirect("https://example.test/protected", 303),
);
expect(granted.status).toBe(303);
const protectedRequest = new Request("https://example.test/protected");
const protectedUrl = new URL(protectedRequest.url);
const protectedContext: Context = {
...createContext(protectedRequest, protectedUrl),
session,
ip: "127.0.0.1",
};
const allowed = await gate(protectedContext, () => new Response("unlocked"));
expect(await allowed.text()).toBe("unlocked");
});
});