mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-14 04:16:32 +00:00
Backend: - rate-limiter: retryAfter now in seconds; ms-precision lu preserves fractional regen residue across calls (C1, C2) - redis-client: throw on Upstash 200-with-error envelope; redisRaw returns body.result (NH1) - constants: MAX_BATCH_SIZE = MAX_CREDITS = 256 (was 512 vs 256) - worker: content-length cap, gzip + s-maxage=10 on /api/canvas, broadcast via executionCtx.waitUntil with r.ok check (NC2, H4, H5) - canvas-storage: warn on truncated Upstash read instead of silent zero-pad (NH2) - get-user-id: SHA-256 (16 hex chars) replaces 32-bit string hash; missing cf-connecting-ip routes to anon:dev with warn (H1, H2); function is now async - canvas-room: log unclean WS closes and errors; defensive close on unexpected client message (NH4, N5) Frontend: - pixel buffer capped at MAX_BATCH_SIZE with toast (NC2) - Submit error UX: toast for 429/413/400/5xx/network; honor retryAfter (NC1) - committedColors allocated upfront so WS updates during initial fetch no longer null-deref (NC3) - handleWheel always renders even when zoom is clamped (C1, C2) - canvas-decoder throws on truncated input instead of reading past end with || 0 (C3) - WS reconnect refetches canvas to recover missed pixels (C4) - pixel-buffer Map cache for O(1) getColorAt/pixelCount (NH1) - cancel in-progress stroke on mode switch (NH3) - canvas load error overlay with Retry button (NH5) - DPR-aware canvas sizing (H1) - onMount cleanup is now sync (no leaked resize listener) (H2) Tests: - update for async getUserId, decoder bounds check, redis error format; add Upstash error-envelope coverage
41 lines
1.4 KiB
JavaScript
41 lines
1.4 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { getUserId } from '../../src/lib/get-user-id.js';
|
|
|
|
/** Helper to create a mock Request with headers */
|
|
function mockRequest(headers = {}) {
|
|
return new Request('http://localhost', {
|
|
headers: new Headers(headers),
|
|
});
|
|
}
|
|
|
|
describe('getUserId', () => {
|
|
it('returns anon: prefix', async () => {
|
|
const id = await getUserId(mockRequest({ 'cf-connecting-ip': '1.2.3.4' }));
|
|
expect(id).toMatch(/^anon:/);
|
|
});
|
|
|
|
it('returns deterministic ID for same IP', async () => {
|
|
const id1 = await getUserId(mockRequest({ 'cf-connecting-ip': '192.168.1.1' }));
|
|
const id2 = await getUserId(mockRequest({ 'cf-connecting-ip': '192.168.1.1' }));
|
|
expect(id1).toBe(id2);
|
|
});
|
|
|
|
it('returns different IDs for different IPs', async () => {
|
|
const id1 = await getUserId(mockRequest({ 'cf-connecting-ip': '1.1.1.1' }));
|
|
const id2 = await getUserId(mockRequest({ 'cf-connecting-ip': '2.2.2.2' }));
|
|
expect(id1).not.toBe(id2);
|
|
});
|
|
|
|
it('falls back to a shared dev bucket when header is missing', async () => {
|
|
const id = await getUserId(mockRequest({}));
|
|
expect(id).toBe('anon:dev');
|
|
// Deterministic for missing header
|
|
expect(await getUserId(mockRequest({}))).toBe(id);
|
|
});
|
|
|
|
it('uses 16-hex-char (8-byte) suffix from SHA-256', async () => {
|
|
const id = await getUserId(mockRequest({ 'cf-connecting-ip': '203.0.113.45' }));
|
|
expect(id).toMatch(/^anon:[0-9a-f]{16}$/);
|
|
});
|
|
});
|