Files
tiennm99 5765055588 test: add DO storage + integration coverage via wrangler unstable_dev
Pure-function unit tests for chunk-storage (BLOB-grow, orphan-row read,
multi-chunk grouping) and cooldown-store (TTL math, INSERT cursor drain,
GC sampling, release/refund) cover the bugs Phase 1 fixed.

Integration suite boots a local Worker + real DO via wrangler
unstable_dev and asserts cookie issuance, cooldown isolation across
identities, Content-Length 411/413 guards, WS upgrade semantics, WS
broadcast frame shape with monotonic seq, ping/pong, per-identity WS
cap, and Origin allowlist. WS uses the ws package directly since
unstable_dev's fetch strips CF's webSocket Response field.

156 tests pass in ~3.3s; verified stable across 3 consecutive runs.
2026-05-11 16:17:35 +07:00

145 lines
5.0 KiB
JavaScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { tryAcquire, release, gc } from '../../src/durable-objects/lib/cooldown-store.js';
import { REQUEST_COOLDOWN_SEC } from '../../src/lib/constants.js';
import { createFakeSql } from '../helpers/fake-sql.js';
const TTL_MS = REQUEST_COOLDOWN_SEC * 1000;
afterEach(() => {
vi.restoreAllMocks();
});
describe('tryAcquire — first call', () => {
it('inserts a fresh row and returns allowed', () => {
const sql = createFakeSql();
const now = 1_000_000;
const r = tryAcquire(sql, 'cookie:abc', now);
expect(r).toEqual({ allowed: true, retryAfter: 0 });
expect(sql._cooldowns.get('cookie:abc')).toBe(now + TTL_MS);
});
});
describe('tryAcquire — within window', () => {
it('rejects a second call inside TTL', () => {
const sql = createFakeSql();
const now = 1_000_000;
tryAcquire(sql, 'cookie:abc', now);
const r = tryAcquire(sql, 'cookie:abc', now + 500);
expect(r.allowed).toBe(false);
expect(r.retryAfter).toBe(REQUEST_COOLDOWN_SEC);
// The active row should remain untouched.
expect(sql._cooldowns.get('cookie:abc')).toBe(now + TTL_MS);
});
});
describe('tryAcquire — after window expires', () => {
it('accepts a second call after expires_at', () => {
const sql = createFakeSql();
const now = 1_000_000;
tryAcquire(sql, 'cookie:abc', now);
const later = now + TTL_MS + 1;
const r = tryAcquire(sql, 'cookie:abc', later);
expect(r.allowed).toBe(true);
expect(sql._cooldowns.get('cookie:abc')).toBe(later + TTL_MS);
});
it('accepts exactly at expires_at boundary (<=)', () => {
const sql = createFakeSql();
const now = 1_000_000;
tryAcquire(sql, 'cookie:abc', now);
const r = tryAcquire(sql, 'cookie:abc', now + TTL_MS);
expect(r.allowed).toBe(true);
});
});
describe('tryAcquire — independent users', () => {
it('does not interfere across user IDs', () => {
const sql = createFakeSql();
const now = 1_000_000;
expect(tryAcquire(sql, 'cookie:a', now).allowed).toBe(true);
expect(tryAcquire(sql, 'cookie:b', now).allowed).toBe(true);
expect(tryAcquire(sql, 'cookie:a', now).allowed).toBe(false);
expect(tryAcquire(sql, 'cookie:b', now).allowed).toBe(false);
});
});
describe('tryAcquire — INSERT branch executes (cursor drain)', () => {
it('persists the INSERT even when no UPDATE preceded it', () => {
const sql = createFakeSql();
let inserts = 0;
const origExec = sql.exec;
sql.exec = (q, ...p) => {
if (q.startsWith('INSERT INTO cooldowns')) inserts++;
return origExec(q, ...p);
};
tryAcquire(sql, 'cookie:fresh', 1_000_000);
expect(inserts).toBe(1);
expect(sql._cooldowns.has('cookie:fresh')).toBe(true);
});
});
describe('tryAcquire — GC sweep sampling', () => {
it('deletes expired rows when the sample fires', () => {
const sql = createFakeSql();
// Seed expired rows.
sql._seedCooldown('cookie:dead1', 100);
sql._seedCooldown('cookie:dead2', 200);
// Force the sample to fire.
vi.spyOn(Math, 'random').mockReturnValue(0);
tryAcquire(sql, 'cookie:fresh', 1_000_000);
expect(sql._cooldowns.has('cookie:dead1')).toBe(false);
expect(sql._cooldowns.has('cookie:dead2')).toBe(false);
expect(sql._cooldowns.has('cookie:fresh')).toBe(true);
});
it('does not run GC when sample misses', () => {
const sql = createFakeSql();
sql._seedCooldown('cookie:dead', 100);
vi.spyOn(Math, 'random').mockReturnValue(0.99);
tryAcquire(sql, 'cookie:fresh', 1_000_000);
expect(sql._cooldowns.has('cookie:dead')).toBe(true);
});
it('continues if GC throws (best-effort)', () => {
const sql = createFakeSql();
vi.spyOn(Math, 'random').mockReturnValue(0);
const origExec = sql.exec;
sql.exec = (q, ...p) => {
if (q.startsWith('DELETE FROM cooldowns WHERE expires_at')) {
throw new Error('boom');
}
return origExec(q, ...p);
};
const r = tryAcquire(sql, 'cookie:fresh', 1_000_000);
expect(r.allowed).toBe(true);
});
});
describe('release', () => {
it('removes the cooldown row so the next acquire succeeds immediately', () => {
const sql = createFakeSql();
tryAcquire(sql, 'cookie:abc', 1_000_000);
expect(sql._cooldowns.has('cookie:abc')).toBe(true);
release(sql, 'cookie:abc');
expect(sql._cooldowns.has('cookie:abc')).toBe(false);
// Second acquire at the same instant should succeed now.
expect(tryAcquire(sql, 'cookie:abc', 1_000_000).allowed).toBe(true);
});
it('is a no-op when the row does not exist', () => {
const sql = createFakeSql();
expect(() => release(sql, 'cookie:missing')).not.toThrow();
});
});
describe('gc', () => {
it('deletes only expired rows', () => {
const sql = createFakeSql();
sql._seedCooldown('cookie:dead', 100);
sql._seedCooldown('cookie:alive', 1_000_000_000);
gc(sql, 5_000_000);
expect(sql._cooldowns.has('cookie:dead')).toBe(false);
expect(sql._cooldowns.has('cookie:alive')).toBe(true);
});
});