mirror of
https://github.com/tiennm99/rplace.git
synced 2026-09-03 18:17:06 +00:00
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.
98 lines
3.5 KiB
JavaScript
98 lines
3.5 KiB
JavaScript
/**
|
|
* Minimal stand-in for CF DO SqlStorage used in unit tests.
|
|
*
|
|
* Supports just the statements actually issued by chunk-storage.js and
|
|
* cooldown-store.js. Anything else throws so tests fail loudly when the
|
|
* source surface grows.
|
|
*
|
|
* Each table is an in-memory Map. Each exec() returns a cursor object that
|
|
* mimics the CF shape:
|
|
* - cursor.toArray() returns the rows array
|
|
* - cursor.rowsWritten is the row count touched by the last write
|
|
* - cursor[Symbol.iterator] iterates rows (for of)
|
|
*/
|
|
|
|
function makeCursor(rows, rowsWritten = 0) {
|
|
return {
|
|
toArray: () => rows,
|
|
rowsWritten,
|
|
[Symbol.iterator]: () => rows[Symbol.iterator](),
|
|
};
|
|
}
|
|
|
|
export function createFakeSql() {
|
|
/** @type {Map<number, Uint8Array>} */
|
|
const chunks = new Map();
|
|
/** @type {Map<string, number>} */ // user_id -> expires_at
|
|
const cooldowns = new Map();
|
|
|
|
function exec(query, ...params) {
|
|
const q = query.trim().replace(/\s+/g, ' ');
|
|
|
|
// ── canvas_chunks ────────────────────────────────────────────────
|
|
if (q.startsWith('SELECT bytes FROM canvas_chunks WHERE chunk_id = ?')) {
|
|
const chunkId = params[0];
|
|
const blob = chunks.get(chunkId);
|
|
return makeCursor(blob ? [{ bytes: blob }] : []);
|
|
}
|
|
if (q.startsWith('SELECT chunk_id, bytes FROM canvas_chunks WHERE chunk_id < ?')) {
|
|
const upper = params[0];
|
|
const rows = [];
|
|
for (const [chunkId, bytes] of chunks) {
|
|
if (chunkId < upper) rows.push({ chunk_id: chunkId, bytes });
|
|
}
|
|
return makeCursor(rows);
|
|
}
|
|
if (q.startsWith('INSERT INTO canvas_chunks (chunk_id, bytes) VALUES (?, ?) ON CONFLICT')) {
|
|
const [chunkId, bytes] = params;
|
|
chunks.set(chunkId, bytes);
|
|
return makeCursor([], 1);
|
|
}
|
|
|
|
// ── cooldowns ────────────────────────────────────────────────────
|
|
if (q.startsWith('UPDATE cooldowns SET expires_at = ? WHERE user_id = ? AND expires_at <= ?')) {
|
|
const [newExpires, userId, now] = params;
|
|
const current = cooldowns.get(userId);
|
|
if (current !== undefined && current <= now) {
|
|
cooldowns.set(userId, newExpires);
|
|
return makeCursor([], 1);
|
|
}
|
|
return makeCursor([], 0);
|
|
}
|
|
if (q.startsWith('INSERT INTO cooldowns (user_id, expires_at) VALUES (?, ?)')) {
|
|
const [userId, expiresAt] = params;
|
|
if (cooldowns.has(userId)) {
|
|
const err = new Error('UNIQUE constraint failed: cooldowns.user_id');
|
|
err.code = 'SQLITE_CONSTRAINT';
|
|
throw err;
|
|
}
|
|
cooldowns.set(userId, expiresAt);
|
|
return makeCursor([], 1);
|
|
}
|
|
if (q.startsWith('DELETE FROM cooldowns WHERE user_id = ?')) {
|
|
const [userId] = params;
|
|
const had = cooldowns.delete(userId);
|
|
return makeCursor([], had ? 1 : 0);
|
|
}
|
|
if (q.startsWith('DELETE FROM cooldowns WHERE expires_at <= ?')) {
|
|
const [now] = params;
|
|
let n = 0;
|
|
for (const [uid, exp] of cooldowns) {
|
|
if (exp <= now) { cooldowns.delete(uid); n++; }
|
|
}
|
|
return makeCursor([], n);
|
|
}
|
|
|
|
throw new Error(`fake-sql: unhandled query: ${q}`);
|
|
}
|
|
|
|
return {
|
|
exec,
|
|
// Test-only inspection / seeding hooks.
|
|
_chunks: chunks,
|
|
_cooldowns: cooldowns,
|
|
_seedChunk(chunkId, bytes) { chunks.set(chunkId, bytes); },
|
|
_seedCooldown(userId, expiresAt) { cooldowns.set(userId, expiresAt); },
|
|
};
|
|
}
|