Files
WRNexusJS/packages/captcha/test/visual-styles.test.ts
T
2026-07-27 12:42:18 +05:30

110 lines
3.2 KiB
TypeScript

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<string>();
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");
});
});