103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { createContext, type Context } from "@wrnexus/core";
|
|
import { 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 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");
|
|
});
|
|
});
|