finish CSS delivery and generated type remediation
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user