113 lines
3.2 KiB
TypeScript
113 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");
|
|
});
|
|
});
|