mirror of
https://github.com/tiennm99/rplace.git
synced 2026-09-09 02:17:35 +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.
228 lines
7.1 KiB
JavaScript
228 lines
7.1 KiB
JavaScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import {
|
|
setupDO,
|
|
placePixel,
|
|
fetchCanvas,
|
|
openWs,
|
|
nextMessage,
|
|
randomCookie,
|
|
} from '../helpers/do-harness.js';
|
|
import {
|
|
CANVAS_WIDTH,
|
|
TOTAL_PIXELS,
|
|
MAX_BATCH_SIZE,
|
|
MAX_WS_PER_IDENTITY,
|
|
} from '../../src/lib/constants.js';
|
|
|
|
let harness;
|
|
|
|
beforeAll(async () => {
|
|
harness = await setupDO();
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
await harness?.close();
|
|
});
|
|
|
|
describe('GET /api/canvas', () => {
|
|
it('returns a TOTAL_PIXELS-sized body on first call', async () => {
|
|
const { status, bytes } = await fetchCanvas(harness);
|
|
expect(status).toBe(200);
|
|
expect(bytes.length).toBe(TOTAL_PIXELS);
|
|
});
|
|
|
|
it('issues Set-Cookie when no cookie is present', async () => {
|
|
const { setCookie } = await fetchCanvas(harness);
|
|
expect(setCookie).toMatch(/^rplace_id=[0-9a-f-]{36}/);
|
|
expect(setCookie).toMatch(/HttpOnly/);
|
|
expect(setCookie).toMatch(/Secure/);
|
|
expect(setCookie).toMatch(/SameSite=Lax/);
|
|
});
|
|
|
|
it('does not re-issue Set-Cookie when a valid cookie is present', async () => {
|
|
const cookie = randomCookie();
|
|
const { setCookie } = await fetchCanvas(harness, { cookie });
|
|
expect(setCookie).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('POST /api/place — happy path', () => {
|
|
it('places a single pixel and is visible on /api/canvas', async () => {
|
|
const cookie = randomCookie();
|
|
const x = 100, y = 200, color = 42;
|
|
const res = await placePixel(harness, { x, y, color, cookie });
|
|
expect(res.status).toBe(200);
|
|
expect(res.json?.ok).toBe(true);
|
|
|
|
const canvas = await fetchCanvas(harness, { cookie });
|
|
const offset = y * CANVAS_WIDTH + x;
|
|
expect(canvas.bytes[offset]).toBe(color);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/place — cooldown', () => {
|
|
it('429s the same cookie within 1 second', async () => {
|
|
const cookie = randomCookie();
|
|
const a = await placePixel(harness, { x: 0, y: 0, color: 1, cookie });
|
|
expect(a.status).toBe(200);
|
|
const b = await placePixel(harness, { x: 1, y: 0, color: 2, cookie });
|
|
expect(b.status).toBe(429);
|
|
expect(b.json?.error).toBe('rate_limited');
|
|
});
|
|
|
|
it('does not interfere across distinct cookies', async () => {
|
|
const c1 = randomCookie();
|
|
const c2 = randomCookie();
|
|
const r1 = await placePixel(harness, { x: 10, y: 0, color: 5, cookie: c1 });
|
|
const r2 = await placePixel(harness, { x: 11, y: 0, color: 6, cookie: c2 });
|
|
expect(r1.status).toBe(200);
|
|
expect(r2.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/place — Content-Length guard', () => {
|
|
it('rejects POST with missing Content-Length (411)', async () => {
|
|
const res = await harness.fetch('/api/place', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: '{"pixels":[]}',
|
|
});
|
|
expect(res.status).toBe(411);
|
|
});
|
|
|
|
it('rejects POST with zero Content-Length (411)', async () => {
|
|
const res = await harness.fetch('/api/place', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Content-Length': '0' },
|
|
body: '',
|
|
});
|
|
expect(res.status).toBe(411);
|
|
});
|
|
|
|
it('rejects POST above the body cap (413)', async () => {
|
|
// Send a body that actually matches the declared Content-Length so the
|
|
// HTTP transport accepts it; the edge validation should still reject on
|
|
// size alone, before parsing.
|
|
const oversized = '{"pad":"' + 'x'.repeat(MAX_BATCH_SIZE * 64 + 10) + '"}';
|
|
const res = await harness.fetch('/api/place', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': String(new TextEncoder().encode(oversized).byteLength),
|
|
},
|
|
body: oversized,
|
|
});
|
|
expect(res.status).toBe(413);
|
|
});
|
|
});
|
|
|
|
describe('WS upgrade', () => {
|
|
it('rejects requests missing the Upgrade header', async () => {
|
|
const res = await harness.fetch('/api/ws');
|
|
expect(res.status).toBe(426);
|
|
});
|
|
|
|
it('accepts an upgrade with a valid cookie', async () => {
|
|
const { status, ws } = await openWs(harness, { cookie: randomCookie() });
|
|
expect(status).toBe(101);
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe('WS broadcast', () => {
|
|
it('delivers a pixels frame with monotonic seq to every connected socket', async () => {
|
|
const subCookie = randomCookie();
|
|
const placerCookie = randomCookie();
|
|
const { ws } = await openWs(harness, { cookie: subCookie });
|
|
|
|
// Pre-arm the listener BEFORE triggering the broadcast: the WS message
|
|
// can arrive faster than the HTTP response, so registering after the
|
|
// placePixel await would miss it.
|
|
const msgPromise = nextMessage(ws);
|
|
const res = await placePixel(harness, { x: 50, y: 50, color: 9, cookie: placerCookie });
|
|
expect(res.status).toBe(200);
|
|
|
|
const data = JSON.parse(await msgPromise);
|
|
expect(data.type).toBe('pixels');
|
|
expect(typeof data.seq).toBe('number');
|
|
expect(data.seq).toBeGreaterThan(0);
|
|
expect(Array.isArray(data.pixels)).toBe(true);
|
|
expect(data.pixels[0]).toMatchObject({ x: 50, y: 50, color: 9 });
|
|
|
|
ws.close();
|
|
});
|
|
|
|
it('seq increments across placements', async () => {
|
|
const subCookie = randomCookie();
|
|
const p1 = randomCookie();
|
|
const p2 = randomCookie();
|
|
const { ws } = await openWs(harness, { cookie: subCookie });
|
|
|
|
const m1Promise = nextMessage(ws);
|
|
await placePixel(harness, { x: 60, y: 0, color: 1, cookie: p1 });
|
|
const m1 = JSON.parse(await m1Promise);
|
|
|
|
const m2Promise = nextMessage(ws);
|
|
await placePixel(harness, { x: 61, y: 0, color: 2, cookie: p2 });
|
|
const m2 = JSON.parse(await m2Promise);
|
|
|
|
expect(m2.seq).toBeGreaterThan(m1.seq);
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe('WS ping/pong', () => {
|
|
it('responds to "ping" with {type:"pong"}', async () => {
|
|
const { ws } = await openWs(harness, { cookie: randomCookie() });
|
|
ws.send('ping');
|
|
const msg = await nextMessage(ws);
|
|
const data = JSON.parse(msg);
|
|
expect(data.type).toBe('pong');
|
|
ws.close();
|
|
});
|
|
});
|
|
|
|
describe('WS per-identity cap', () => {
|
|
it('accepts up to MAX_WS_PER_IDENTITY sockets, rejects the next', async () => {
|
|
const cookie = randomCookie();
|
|
const sockets = [];
|
|
for (let i = 0; i < MAX_WS_PER_IDENTITY; i++) {
|
|
const r = await openWs(harness, { cookie });
|
|
expect(r.status).toBe(101);
|
|
sockets.push(r.ws);
|
|
}
|
|
const over = await openWs(harness, { cookie });
|
|
expect(over.status).toBe(429);
|
|
for (const ws of sockets) ws.close();
|
|
});
|
|
});
|
|
|
|
describe('WS Origin allowlist (env override)', () => {
|
|
let restricted;
|
|
beforeAll(async () => {
|
|
restricted = await setupDO({
|
|
vars: { ALLOWED_ORIGINS: 'https://allowed.example', ENVIRONMENT: 'development' },
|
|
});
|
|
}, 30_000);
|
|
afterAll(async () => {
|
|
await restricted?.close();
|
|
});
|
|
|
|
it('rejects WS upgrade from a disallowed Origin (403)', async () => {
|
|
const r = await openWs(restricted, {
|
|
cookie: randomCookie(),
|
|
origin: 'https://evil.example',
|
|
});
|
|
expect(r.status).toBe(403);
|
|
});
|
|
|
|
it('accepts WS upgrade from an allowed Origin', async () => {
|
|
const r = await openWs(restricted, {
|
|
cookie: randomCookie(),
|
|
origin: 'https://allowed.example',
|
|
});
|
|
expect(r.status).toBe(101);
|
|
r.ws?.close();
|
|
});
|
|
});
|