212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const showcaseRoot = join(import.meta.dir, "..");
|
|
|
|
function path(relativePath: string): string {
|
|
return join(showcaseRoot, relativePath);
|
|
}
|
|
|
|
function read(relativePath: string): string {
|
|
return readFileSync(path(relativePath), "utf8");
|
|
}
|
|
|
|
describe("CAPTCHA showcase structure", () => {
|
|
test("ships the required pages, routes, middleware, and component", () => {
|
|
const requiredFiles = [
|
|
"app/pages/index.wrn",
|
|
"app/pages/captcha.wrn",
|
|
"app/pages/protected.wrn",
|
|
"app/pages/styles.wrn",
|
|
"app/components/Captcha.wrn",
|
|
"app/api/contact.ts",
|
|
"app/api/page-grant.ts",
|
|
"app/schemas/contact.ts",
|
|
"app/lib/captcha.ts",
|
|
"app/middleware/captcha-page-gate.ts",
|
|
"app/styles/global.css",
|
|
"wrnexus.config.ts",
|
|
];
|
|
|
|
for (const relativePath of requiredFiles) {
|
|
expect(existsSync(path(relativePath))).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("configures Tailwind and Lucide Iconify utilities", () => {
|
|
const packageJson = read("package.json");
|
|
const styles = read("app/styles/global.css");
|
|
|
|
expect(packageJson).toContain('"@iconify-json/lucide"');
|
|
expect(packageJson).toContain('"@iconify/tailwind4"');
|
|
expect(styles).toContain('@plugin "@iconify/tailwind4";');
|
|
expect(styles).toContain('@source "../../../packages/captcha/components/*.wrn";');
|
|
});
|
|
|
|
test("documents every bundled challenge mode", () => {
|
|
const source = read("app/pages/index.wrn");
|
|
const expectedExamples = [
|
|
'type="number"',
|
|
'type="alpha"',
|
|
'type="alphanumeric"',
|
|
'type="calculation"',
|
|
'type="image"',
|
|
'type="timing"',
|
|
'type="not-robot"',
|
|
'presentation="audio"',
|
|
'presentation="invisible"',
|
|
];
|
|
|
|
for (const example of expectedExamples) {
|
|
expect(source).toContain(example);
|
|
}
|
|
|
|
expect(source.match(/<Captcha\b/g)?.length ?? 0).toBeGreaterThanOrEqual(18);
|
|
expect(source).toContain('disturbance="25"');
|
|
expect(source).toContain('disturbance="50"');
|
|
expect(source).toContain('disturbance="75"');
|
|
expect(source).toContain('size="compact"');
|
|
expect(source).toContain('size="normal"');
|
|
expect(source).toContain('size="big"');
|
|
expect(source).toContain('showListen="false"');
|
|
});
|
|
|
|
|
|
test("documents every generated image renderer and random pool controls", () => {
|
|
const source = read("app/pages/styles.wrn");
|
|
const styles = [
|
|
"classic",
|
|
"collision",
|
|
"snow",
|
|
"corrosion",
|
|
"spiderweb",
|
|
"cross-shadow",
|
|
"split",
|
|
"split2",
|
|
"cut",
|
|
"darts",
|
|
"distortion",
|
|
"stitch",
|
|
"striped",
|
|
"wave",
|
|
"grid-noise",
|
|
"scribble",
|
|
"pixel",
|
|
"broken-lines",
|
|
];
|
|
|
|
for (const style of styles) {
|
|
expect(source).toContain(`imageStyle="${style}"`);
|
|
}
|
|
|
|
expect(source).toContain('imageStyle="random"');
|
|
expect(source).toContain('allowedStyles="classic,snow,distortion,wave"');
|
|
expect(source).toContain('excludedStyles="collision,split,split2,broken-lines"');
|
|
expect(source).toContain('difficulty="easy"');
|
|
expect(source).toContain('difficulty="normal"');
|
|
expect(source).toContain('difficulty="hard"');
|
|
expect(source.match(/<Captcha\b/g)?.length ?? 0).toBeGreaterThanOrEqual(23);
|
|
});
|
|
|
|
test("includes protected form and external-provider examples", () => {
|
|
const source = read("app/pages/index.wrn");
|
|
|
|
expect(source).toContain('action="/api/contact"');
|
|
expect(source).toContain('data-schema="contact"');
|
|
expect(source).toContain('action="contact-submit"');
|
|
expect(source).toContain('data-error="email"');
|
|
expect(source).toContain('data-error="message"');
|
|
expect(source).toContain('provider="turnstile"');
|
|
expect(source).toContain('provider="recaptcha"');
|
|
expect(source).toContain('provider="hcaptcha"');
|
|
});
|
|
});
|
|
|
|
describe("CAPTCHA showcase integration", () => {
|
|
test("configures the self-hosted engine and HTTP handlers", () => {
|
|
const source = read("app/lib/captcha.ts");
|
|
|
|
expect(source).toContain("createCaptchaEngine");
|
|
expect(source).toContain("createCaptchaHttpHandlers");
|
|
expect(source).toContain('basePath: "/api/captcha"');
|
|
expect(source).toContain("challengeTtlMs: 2 * 60_000");
|
|
expect(source).toContain("responseTokenTtlMs: 5 * 60_000");
|
|
expect(source).toContain("maxAttempts: 3");
|
|
});
|
|
|
|
test("protects the contact form with a server-side guard", () => {
|
|
const source = read("app/api/contact.ts");
|
|
|
|
expect(source).toContain("captchaGuard");
|
|
expect(source).toContain('action: "contact-submit"');
|
|
expect(source).toContain('responseField: "wrn-captcha-response"');
|
|
expect(source).toContain("bindHostname: true");
|
|
expect(source).toContain("bindSession: true");
|
|
expect(source).toContain("parseBody<ContactInput>");
|
|
expect(source).toContain("contactSchema");
|
|
expect(source).toContain("ctx.req.clone()");
|
|
});
|
|
|
|
test("protects the page route with a session grant", () => {
|
|
const middleware = read("app/middleware/captcha-page-gate.ts");
|
|
const challengePage = read("app/pages/captcha.wrn");
|
|
|
|
expect(middleware).toContain("captchaPageGate");
|
|
expect(middleware).toContain('action: "protected-page-access"');
|
|
expect(middleware).toContain('mode: "session"');
|
|
expect(middleware).toContain("verifiedForMs: 15 * 60_000");
|
|
expect(middleware).toContain('routeGroups: ["/protected"]');
|
|
expect(middleware).toContain('ctx.url.pathname === "/api/page-grant"');
|
|
|
|
expect(challengePage).toContain('action="protected-page-access"');
|
|
expect(challengePage).toContain('action="/api/page-grant"');
|
|
expect(challengePage).toContain('name="returnTo"');
|
|
expect(challengePage).toContain("value='{returnTo}'");
|
|
|
|
const grantRoute = read("app/api/page-grant.ts");
|
|
expect(grantRoute).toContain("safeReturnPath");
|
|
expect(grantRoute).toContain('form.get("returnTo")');
|
|
expect(grantRoute).toContain("Response.redirect");
|
|
});
|
|
|
|
test("registers the CAPTCHA plugin and DevToolbar audit", () => {
|
|
const source = read("wrnexus.config.ts");
|
|
|
|
expect(source).toContain("captchaPlugin");
|
|
expect(source).toContain("enableDevToolbar: true");
|
|
});
|
|
|
|
test("ships the expected component events and response field", () => {
|
|
const source = read("app/components/Captcha.wrn");
|
|
const expectedEvents = [
|
|
"ready",
|
|
"challenge",
|
|
"input",
|
|
"verify",
|
|
"success",
|
|
"failure",
|
|
"expired",
|
|
"refresh",
|
|
"audioStart",
|
|
"audioEnd",
|
|
"error",
|
|
];
|
|
|
|
expect(source).toContain('name = "wrn-captcha-response"');
|
|
expect(source).toContain('responseField = "wrn-captcha-response"');
|
|
expect(source).toContain('disturbance = 50');
|
|
expect(source).toContain('size = "normal"');
|
|
expect(source).toContain('showListen = true');
|
|
expect(source).toContain('imageStyle = "random"');
|
|
expect(source).toContain('allowedStyles = ""');
|
|
expect(source).toContain('excludedStyles = ""');
|
|
expect(source).toContain('randomizeStyle = false');
|
|
expect(source).toContain('data-captcha-not-robot-button');
|
|
|
|
for (const eventName of expectedEvents) {
|
|
expect(source).toContain(`@event ${eventName} = function`);
|
|
}
|
|
});
|
|
});
|