import { describe, expect, test } from "bun:test"; import { CAPTCHA_CONCRETE_IMAGE_STYLES, CAPTCHA_IMAGE_STYLES, normalizeCaptchaImageStyleList, resolveCaptchaImageStyle, } from "../src/challenges/styles.ts"; import { renderTextChallenge } from "../src/challenges/visual.ts"; import type { CaptchaConcreteImageStyle, CaptchaGeneratorContext, } from "../src/types.ts"; function random(seedValue = 17) { let seed = seedValue >>> 0; const next = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed; }; return { int(min: number, max: number) { return min + (next() % (max - min + 1)); }, float() { return next() / 0xffff_ffff; }, }; } function context(style: CaptchaConcreteImageStyle): CaptchaGeneratorContext { const source = random(CAPTCHA_CONCRETE_IMAGE_STYLES.indexOf(style) + 101); return { difficulty: "normal", disturbance: 50, imageStyle: style, requestedImageStyle: style, imageStylePool: [style], locale: "en", length: 6, caseSensitive: false, minCompletionMs: 800, randomInt: source.int, randomFloat: source.float, randomId: (bytes = 18) => `${style}-${bytes}`, }; } function decodeDataUri(uri: string): Uint8Array { const encoded = uri.slice(uri.indexOf(",") + 1); return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0)); } describe("CAPTCHA image renderer styles", () => { test("publishes the complete renderer catalog", () => { expect(CAPTCHA_IMAGE_STYLES).toContain("random"); expect(CAPTCHA_CONCRETE_IMAGE_STYLES).toHaveLength(18); expect(CAPTCHA_CONCRETE_IMAGE_STYLES).toEqual([ "classic", "collision", "snow", "corrosion", "spiderweb", "cross-shadow", "split", "split2", "cut", "darts", "distortion", "stitch", "striped", "wave", "grid-noise", "scribble", "pixel", "broken-lines", ]); }); test("renders a valid PNG for every concrete style", () => { const images = new Set(); for (const style of CAPTCHA_CONCRETE_IMAGE_STYLES) { const image = renderTextChallenge("A7K93P", context(style)); expect(image.startsWith("data:image/png;base64,")).toBe(true); const bytes = decodeDataUri(image); expect([...bytes.slice(0, 8)]).toEqual([137, 80, 78, 71, 13, 10, 26, 10]); expect(bytes.byteLength).toBeGreaterThan(1_000); images.add(image); } expect(images.size).toBe(CAPTCHA_CONCRETE_IMAGE_STYLES.length); }); test("normalizes comma-separated renderer pools", () => { expect(normalizeCaptchaImageStyleList("snow, wave, snow", "allowedStyles")).toEqual([ "snow", "wave", ]); expect(() => normalizeCaptchaImageStyleList("snow,unknown", "allowedStyles")).toThrow( "allowedStyles contains an unknown image style", ); }); test("selects random styles only from the active pool", () => { const resolved = resolveCaptchaImageStyle({ imageStyle: "random", allowedStyles: ["classic", "snow", "wave"], excludedStyles: ["snow"], randomInt: () => 1, }); expect(resolved.pool).toEqual(["classic", "wave"]); expect(resolved.resolved).toBe("wave"); }); });