release: WRNexusJS 0.4.0
This commit is contained in:
@@ -33,46 +33,110 @@ function fixture() {
|
||||
}
|
||||
return bytes;
|
||||
},
|
||||
audioRenderer: { contentType: "audio/wav", async render() { return new Uint8Array([82, 73, 70, 70]); } },
|
||||
audioRenderer: {
|
||||
contentType: "audio/wav",
|
||||
async render() {
|
||||
return new Uint8Array([82, 73, 70, 70]);
|
||||
},
|
||||
},
|
||||
});
|
||||
return { engine, advance: (milliseconds: number) => { now += milliseconds; } };
|
||||
return {
|
||||
engine,
|
||||
advance: (milliseconds: number) => {
|
||||
now += milliseconds;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("self-hosted CAPTCHA engine", () => {
|
||||
test("creates, solves, and consumes a challenge and response token", async () => {
|
||||
const { engine } = fixture();
|
||||
const challenge = await engine.create({ action: "signup", hostname: "example.test", sessionId: "s1" });
|
||||
const solved = await engine.verify({ challengeId: challenge.id, action: "signup", answer: "42", hostname: "example.test", sessionId: "s1" });
|
||||
const challenge = await engine.create({
|
||||
action: "signup",
|
||||
hostname: "example.test",
|
||||
sessionId: "s1",
|
||||
});
|
||||
const solved = await engine.verify({
|
||||
challengeId: challenge.id,
|
||||
action: "signup",
|
||||
answer: "42",
|
||||
hostname: "example.test",
|
||||
sessionId: "s1",
|
||||
});
|
||||
expect(solved.success).toBe(true);
|
||||
expect(solved.responseToken).toBeString();
|
||||
|
||||
const accepted = await engine.verifyResponseToken({ responseToken: solved.responseToken, action: "signup", hostname: "example.test", sessionId: "s1" });
|
||||
const accepted = await engine.verifyResponseToken({
|
||||
responseToken: solved.responseToken,
|
||||
action: "signup",
|
||||
hostname: "example.test",
|
||||
sessionId: "s1",
|
||||
});
|
||||
expect(accepted.success).toBe(true);
|
||||
|
||||
const replay = await engine.verifyResponseToken({ responseToken: solved.responseToken, action: "signup", hostname: "example.test", sessionId: "s1" });
|
||||
const replay = await engine.verifyResponseToken({
|
||||
responseToken: solved.responseToken,
|
||||
action: "signup",
|
||||
hostname: "example.test",
|
||||
sessionId: "s1",
|
||||
});
|
||||
expect(replay).toMatchObject({ success: false, code: "already-used" });
|
||||
});
|
||||
|
||||
test("rejects wrong answers and enforces attempt limits", async () => {
|
||||
const { engine } = fixture();
|
||||
const challenge = await engine.create({ action: "login", maxAttempts: 2 });
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "login", answer: "1" })).toMatchObject({ success: false, code: "incorrect-answer" });
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "login", answer: "2" })).toMatchObject({ success: false, code: "attempts-exhausted" });
|
||||
expect(
|
||||
await engine.verify({ challengeId: challenge.id, action: "login", answer: "1" }),
|
||||
).toMatchObject({ success: false, code: "incorrect-answer" });
|
||||
expect(
|
||||
await engine.verify({ challengeId: challenge.id, action: "login", answer: "2" }),
|
||||
).toMatchObject({ success: false, code: "attempts-exhausted" });
|
||||
});
|
||||
|
||||
test("binds challenges to actions, hosts, and sessions", async () => {
|
||||
const { engine } = fixture();
|
||||
const challenge = await engine.create({ action: "checkout", hostname: "shop.test", sessionId: "abc" });
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "login", answer: "42", hostname: "shop.test", sessionId: "abc" })).toMatchObject({ code: "action-mismatch" });
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "checkout", answer: "42", hostname: "other.test", sessionId: "abc" })).toMatchObject({ code: "hostname-mismatch" });
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "checkout", answer: "42", hostname: "shop.test", sessionId: "wrong" })).toMatchObject({ code: "session-mismatch" });
|
||||
const challenge = await engine.create({
|
||||
action: "checkout",
|
||||
hostname: "shop.test",
|
||||
sessionId: "abc",
|
||||
});
|
||||
expect(
|
||||
await engine.verify({
|
||||
challengeId: challenge.id,
|
||||
action: "login",
|
||||
answer: "42",
|
||||
hostname: "shop.test",
|
||||
sessionId: "abc",
|
||||
}),
|
||||
).toMatchObject({ code: "action-mismatch" });
|
||||
expect(
|
||||
await engine.verify({
|
||||
challengeId: challenge.id,
|
||||
action: "checkout",
|
||||
answer: "42",
|
||||
hostname: "other.test",
|
||||
sessionId: "abc",
|
||||
}),
|
||||
).toMatchObject({ code: "hostname-mismatch" });
|
||||
expect(
|
||||
await engine.verify({
|
||||
challengeId: challenge.id,
|
||||
action: "checkout",
|
||||
answer: "42",
|
||||
hostname: "shop.test",
|
||||
sessionId: "wrong",
|
||||
}),
|
||||
).toMatchObject({ code: "session-mismatch" });
|
||||
});
|
||||
|
||||
test("expires challenges", async () => {
|
||||
const { engine, advance } = fixture();
|
||||
const challenge = await engine.create({ action: "contact", expiresInMs: 1000 });
|
||||
advance(1001);
|
||||
expect(await engine.verify({ challengeId: challenge.id, action: "contact", answer: "42" })).toMatchObject({ success: false, code: "expired" });
|
||||
expect(
|
||||
await engine.verify({ challengeId: challenge.id, action: "contact", answer: "42" }),
|
||||
).toMatchObject({ success: false, code: "expired" });
|
||||
});
|
||||
|
||||
test("protects audio with an unguessable challenge key", async () => {
|
||||
@@ -166,7 +230,9 @@ describe("self-hosted CAPTCHA engine", () => {
|
||||
imageStyle: "random",
|
||||
allowedStyles: ["snow", "wave"],
|
||||
});
|
||||
expect(["snow", "wave"]).toContain(pooled.metadata?.imageStyle);
|
||||
const pooledStyle = pooled.metadata?.imageStyle;
|
||||
expect(typeof pooledStyle).toBe("string");
|
||||
expect(["snow", "wave"]).toContain(pooledStyle as string);
|
||||
expect(pooled.metadata?.imageStylePool).toEqual(["snow", "wave"]);
|
||||
|
||||
const forced = await engine.create({
|
||||
@@ -175,29 +241,36 @@ describe("self-hosted CAPTCHA engine", () => {
|
||||
randomizeStyle: true,
|
||||
allowedStyles: "cut,striped",
|
||||
});
|
||||
expect(["cut", "striped"]).toContain(forced.metadata?.imageStyle);
|
||||
const forcedStyle = forced.metadata?.imageStyle;
|
||||
expect(typeof forcedStyle).toBe("string");
|
||||
expect(["cut", "striped"]).toContain(forcedStyle as string);
|
||||
expect(forced.metadata?.requestedImageStyle).toBe("classic");
|
||||
});
|
||||
|
||||
test("validates image renderer style pools", async () => {
|
||||
const { engine } = fixture();
|
||||
|
||||
await expect(engine.create({
|
||||
action: "unknown-style",
|
||||
imageStyle: "unknown" as never,
|
||||
})).rejects.toThrow("imageStyle must be one of");
|
||||
await expect(
|
||||
engine.create({
|
||||
action: "unknown-style",
|
||||
imageStyle: "unknown" as never,
|
||||
}),
|
||||
).rejects.toThrow("imageStyle must be one of");
|
||||
|
||||
await expect(engine.create({
|
||||
action: "empty-style-pool",
|
||||
allowedStyles: ["snow"],
|
||||
excludedStyles: ["snow"],
|
||||
})).rejects.toThrow("No CAPTCHA image styles remain");
|
||||
await expect(
|
||||
engine.create({
|
||||
action: "empty-style-pool",
|
||||
allowedStyles: ["snow"],
|
||||
excludedStyles: ["snow"],
|
||||
}),
|
||||
).rejects.toThrow("No CAPTCHA image styles remain");
|
||||
|
||||
await expect(engine.create({
|
||||
action: "excluded-explicit-style",
|
||||
imageStyle: "classic",
|
||||
excludedStyles: ["classic"],
|
||||
})).rejects.toThrow("is not available in the configured style pool");
|
||||
await expect(
|
||||
engine.create({
|
||||
action: "excluded-explicit-style",
|
||||
imageStyle: "classic",
|
||||
excludedStyles: ["classic"],
|
||||
}),
|
||||
).rejects.toThrow("is not available in the configured style pool");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user