New Captcha Package added
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user