72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { createContext } from "@wrnexus/core";
|
|
import { v } from "@wrnexus/validation";
|
|
import {
|
|
captchaPlugin,
|
|
defineCaptchaProvider,
|
|
parseWithCaptcha,
|
|
type CaptchaProvider,
|
|
} from "../src/index.ts";
|
|
import type { PluginContext, TransformContext } from "@wrnexus/plugin";
|
|
|
|
describe("CAPTCHA validation and development audit", () => {
|
|
test("combines schema and action-bound provider verification failures", async () => {
|
|
const requests: unknown[] = [];
|
|
const provider: CaptchaProvider = defineCaptchaProvider({
|
|
name: "fixture",
|
|
client: { responseField: "captcha-response" },
|
|
async verify(input) {
|
|
requests.push(input);
|
|
return { success: false, provider: "fixture", action: input.action, message: "Try again" };
|
|
},
|
|
});
|
|
const ctx = createContext(
|
|
new Request("https://app.test/signup"),
|
|
new URL("https://app.test/signup"),
|
|
);
|
|
const result = await parseWithCaptcha(
|
|
v.object({ email: v.string().email() }),
|
|
{ email: "invalid", "captcha-response": "token" },
|
|
ctx,
|
|
{ action: "signup", provider, bindIp: true },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.errors.email).toBeDefined();
|
|
expect(result.errors["captcha-response"]).toBe("Try again");
|
|
expect(requests[0]).toEqual(
|
|
expect.objectContaining({ action: "signup", hostname: "app.test", providerToken: "token" }),
|
|
);
|
|
});
|
|
|
|
test("rejects incomplete custom providers", () => {
|
|
expect(() =>
|
|
defineCaptchaProvider({ name: "", client: { responseField: "x" } } as never),
|
|
).toThrow("stable name");
|
|
expect(() =>
|
|
defineCaptchaProvider({ name: "x", client: { responseField: "" } } as never),
|
|
).toThrow("responseField");
|
|
expect(() =>
|
|
defineCaptchaProvider({ name: "x", client: { responseField: "token" } } as never),
|
|
).toThrow("verify()");
|
|
});
|
|
|
|
test("flags exposed secrets, missing provider keys, and inaccessible hard challenges", async () => {
|
|
const metadata = new Map<string, unknown>();
|
|
const plugin = captchaPlugin();
|
|
const context = {
|
|
mode: "development",
|
|
file: "signup.wrn",
|
|
metadata,
|
|
} as TransformContext;
|
|
await plugin.transformCode?.(
|
|
`<Captcha provider="turnstile" secretKey="leaked" action="signup" disturbance="90" showAudio="false" />`,
|
|
context,
|
|
);
|
|
const panels = await plugin.devToolbarPanels?.({ metadata } as PluginContext);
|
|
const ids = panels?.[0]?.issues?.map((issue) => String((issue as { id: string }).id)) ?? [];
|
|
expect(ids.some((id) => id.startsWith("client-secret:"))).toBe(true);
|
|
expect(ids.some((id) => id.startsWith("missing-site-key:"))).toBe(true);
|
|
expect(ids.some((id) => id.startsWith("hard-without-audio:"))).toBe(true);
|
|
});
|
|
});
|