Files
WRNexusJS/packages/captcha/test/durable-stores.test.ts
Clintchiz bca9549f3a
Quality / quality (ubuntu-latest) (push) Failing after 12m54s
Quality / quality (windows-latest) (push) Canceled after 0s
finish CSS delivery and generated type remediation
2026-08-10 00:17:25 +05:30

131 lines
4.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import {
RedisCaptchaStore,
SqliteCaptchaStore,
managedCaptchaProvider,
type CaptchaChallengeRecord,
type CaptchaResponseTokenRecord,
type RedisCaptchaClient,
type SqliteDatabaseLike,
} from "../src/index.ts";
const challenge = (id: string, expiresAt = Date.now() + 60_000): CaptchaChallengeRecord => ({
id,
provider: "self-hosted",
type: "text",
presentation: "visual",
action: "signup",
publicChallenge: {
id,
provider: "self-hosted",
type: "text",
presentation: "visual",
action: "signup",
prompt: "Type the text",
createdAt: Date.now(),
expiresAt,
responseField: "wrn-captcha-response",
},
answerDigest: "digest",
answerSalt: "salt",
answerKind: "text",
caseSensitive: false,
attempts: 0,
maxAttempts: 3,
metadata: {},
createdAt: Date.now(),
expiresAt,
});
const token = (tokenHash: string): CaptchaResponseTokenRecord => ({
tokenHash,
provider: "self-hosted",
action: "signup",
createdAt: Date.now(),
expiresAt: Date.now() + 60_000,
});
describe("CAPTCHA durable stores", () => {
test("SQLite atomically mutates, consumes, deletes, and collects records", async () => {
const db = new Database(":memory:");
const store = new SqliteCaptchaStore(db as unknown as SqliteDatabaseLike);
await store.createChallenge(challenge("c1"));
expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
expect(await store.consumeChallenge("c1", Date.now())).toMatchObject({
consumedAt: expect.any(Number),
});
expect(await store.consumeChallenge("c1", Date.now())).toBeUndefined();
await store.createToken(token("t1"));
expect(await store.consumeToken("t1", Date.now())).toMatchObject({
consumedAt: expect.any(Number),
});
expect(await store.consumeToken("t1", Date.now())).toBeUndefined();
await store.deleteChallenge("c1");
await store.deleteToken("t1");
await store.createChallenge(challenge("expired", Date.now() - 1));
await store.gc(Date.now());
expect(await store.getChallenge("expired")).toBeUndefined();
db.close();
});
test("Redis fallback locking preserves one-time consumption without Lua", async () => {
const values = new Map<string, string>();
const redis: RedisCaptchaClient = {
get: (key) => values.get(key) ?? null,
set: (key, value) => void values.set(key, value),
del: (key) => (values.delete(key) ? 1 : 0),
};
const store = new RedisCaptchaStore(redis, { prefix: "test:" });
await store.createChallenge(challenge("c1"));
expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
const consumed = await Promise.all([
store.consumeChallenge("c1", Date.now()),
store.consumeChallenge("c1", Date.now()),
]);
expect(consumed.filter(Boolean)).toHaveLength(1);
await store.createToken(token("t1"));
const tokens = await Promise.all([
store.consumeToken("t1", Date.now()),
store.consumeToken("t1", Date.now()),
]);
expect(tokens.filter(Boolean)).toHaveLength(1);
await store.deleteChallenge("c1");
await store.deleteToken("t1");
expect(await store.getChallenge("c1")).toBeUndefined();
});
test("managed provider separates public creation from secret verification", async () => {
const requests: RequestInit[] = [];
const provider = managedCaptchaProvider({
baseUrl: "https://captcha.test/",
siteKey: "site",
secretKey: "secret",
fetch: (async (_url, init) => {
requests.push(init ?? {});
return Response.json(
requests.length === 1
? {
id: "c1",
provider: "managed",
type: "text",
presentation: "visual",
action: "signup",
prompt: "Type",
createdAt: 1,
expiresAt: 2,
responseField: "token",
}
: { success: true, provider: "managed", action: "signup" },
);
}) as typeof fetch,
});
await provider.createChallenge({ action: "signup" });
await provider.verify({ action: "signup", providerToken: "answer" });
expect(new Headers(requests[0]?.headers).has("authorization")).toBe(false);
expect(new Headers(requests[1]?.headers).get("authorization")).toBe("Bearer secret");
});
});