Skip to content

Commit 9a06ce7

Browse files
ABCrimsonclaude
andcommitted
test: v1.2.2 — comprehensive test overhaul across entire monorepo
Add 30+ new test files with 1,400+ new tests, bringing the total from ~900 to 2,300+ tests across all packages: API (491 tests, 6 files): - dataloaders: 42 tests for all 8 DataLoader factories - cursor-pagination: 27 tests for Relay-style cursors - errors: 66 tests for 9 custom GraphQL error classes + sanitizeError - validation: 131 tests for Zod schemas + validateInput helper - cache: 64 tests for Redis cache utilities Web (266 tests, 12 files): - calculator-store: 55 tests for Zustand store with Immer - redis: 39 tests for cache/rate-limit/session/query utilities - rbac: 24 tests for role-based access control - roles: 4 tests for ROLE_HIERARCHY - bookmarks-store, settings-store, worksheet-store, collab-store Math-engine (1,039 tests, 19 files): - algebra: 190 tests for groups, rings, fields, homomorphisms - logic-core: 112 tests for propositional logic prover - simplify: 91 tests for symbolic simplification engine - Fixed parser test: extractVariables correctly excludes pi Plot-engine (323 tests, 13 files): - buffer-pool: 22 tests for WebGL buffer allocation - marching-squares: 19 tests for implicit contour finding - shader-cache: 17 tests for GLSL shader compilation/caching - csv/svg export: 48 tests for data export Workers (187 tests, 5 files): - sliding-window: 46 tests for rate limiting algorithms - r2: 40 tests for R2 storage utilities Infrastructure: - Vitest configs standardized across all packages - Disabled typecheck in math-engine vitest (causes indefinite hang) - Cleaned up web vitest.setup.ts unused mocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 34a1f70 commit 9a06ce7

34 files changed

Lines changed: 11273 additions & 61 deletions

apps/api/src/__tests__/lib/cache.test.ts

Lines changed: 794 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
encodeCursor,
4+
decodeCursor,
5+
buildCursorParams,
6+
buildConnection,
7+
} from '../../lib/cursor-pagination';
8+
9+
// ---------------------------------------------------------------------------
10+
// encodeCursor / decodeCursor
11+
// ---------------------------------------------------------------------------
12+
13+
describe('encodeCursor / decodeCursor', () => {
14+
it('round-trips an id correctly', () => {
15+
const id = 'abc-123';
16+
const cursor = encodeCursor(id);
17+
expect(decodeCursor(cursor)).toBe(id);
18+
});
19+
20+
it('round-trips a UUID', () => {
21+
const id = '550e8400-e29b-41d4-a716-446655440000';
22+
expect(decodeCursor(encodeCursor(id))).toBe(id);
23+
});
24+
25+
it('round-trips an empty string id', () => {
26+
expect(decodeCursor(encodeCursor(''))).toBe('');
27+
});
28+
29+
it('returns null for a completely invalid cursor', () => {
30+
expect(decodeCursor('not-a-valid-cursor!!!')).toBeNull();
31+
});
32+
33+
it('returns null for a valid base64url string missing the "cursor:" prefix', () => {
34+
// Encode a string that does NOT start with "cursor:"
35+
const noPrefixCursor = Buffer.from('nope:abc').toString('base64url');
36+
expect(decodeCursor(noPrefixCursor)).toBeNull();
37+
});
38+
39+
it('returns null for an empty string', () => {
40+
expect(decodeCursor('')).toBeNull();
41+
});
42+
43+
it('produces a base64url-safe string (no +, /, = characters)', () => {
44+
const cursor = encodeCursor('id-with-special-chars/+=');
45+
expect(cursor).not.toMatch(/[+/=]/);
46+
});
47+
});
48+
49+
// ---------------------------------------------------------------------------
50+
// buildCursorParams
51+
// ---------------------------------------------------------------------------
52+
53+
describe('buildCursorParams', () => {
54+
it('uses default forward pagination when no args are supplied', () => {
55+
const params = buildCursorParams({});
56+
57+
expect(params.isBackward).toBe(false);
58+
expect(params.requestedSize).toBe(20); // defaultSize
59+
expect(params.take).toBe(21); // requestedSize + 1 for boundary detection
60+
expect(params.skip).toBe(0);
61+
expect(params.cursor).toBeUndefined();
62+
});
63+
64+
it('applies custom first with an after cursor', () => {
65+
const afterCursor = encodeCursor('item-42');
66+
const params = buildCursorParams({ first: 10, after: afterCursor });
67+
68+
expect(params.isBackward).toBe(false);
69+
expect(params.requestedSize).toBe(10);
70+
expect(params.take).toBe(11);
71+
expect(params.skip).toBe(1); // skip the cursor item itself
72+
expect(params.cursor).toEqual({ id: 'item-42' });
73+
});
74+
75+
it('supports backward pagination with last and before', () => {
76+
const beforeCursor = encodeCursor('item-99');
77+
const params = buildCursorParams({ last: 5, before: beforeCursor });
78+
79+
expect(params.isBackward).toBe(true);
80+
expect(params.requestedSize).toBe(5);
81+
expect(params.take).toBe(-6); // -(requestedSize + 1) for backward
82+
expect(params.skip).toBe(1);
83+
expect(params.cursor).toEqual({ id: 'item-99' });
84+
});
85+
86+
it('clamps requested size to maxSize (default 100)', () => {
87+
const params = buildCursorParams({ first: 500 });
88+
89+
expect(params.requestedSize).toBe(100);
90+
expect(params.take).toBe(101);
91+
});
92+
93+
it('clamps requested size to a custom maxSize', () => {
94+
const params = buildCursorParams({ first: 50 }, 25);
95+
96+
expect(params.requestedSize).toBe(25);
97+
expect(params.take).toBe(26);
98+
});
99+
100+
it('enforces a minimum size of 1', () => {
101+
const params = buildCursorParams({ first: 0 });
102+
103+
expect(params.requestedSize).toBe(1);
104+
expect(params.take).toBe(2);
105+
});
106+
107+
it('enforces minimum size of 1 for negative values', () => {
108+
const params = buildCursorParams({ first: -10 });
109+
110+
expect(params.requestedSize).toBe(1);
111+
expect(params.take).toBe(2);
112+
});
113+
114+
it('forward pagination takes precedence when both first and last are provided', () => {
115+
const params = buildCursorParams({ first: 10, last: 5 });
116+
117+
expect(params.isBackward).toBe(false);
118+
expect(params.requestedSize).toBe(10);
119+
expect(params.take).toBe(11);
120+
});
121+
122+
it('uses a custom defaultSize when no first/last is provided', () => {
123+
const params = buildCursorParams({}, 100, 50);
124+
125+
expect(params.requestedSize).toBe(50);
126+
expect(params.take).toBe(51);
127+
});
128+
129+
it('backward pagination without a cursor sets skip to 0 and cursor to undefined', () => {
130+
const params = buildCursorParams({ last: 10 });
131+
132+
expect(params.isBackward).toBe(true);
133+
expect(params.skip).toBe(0);
134+
expect(params.cursor).toBeUndefined();
135+
});
136+
});
137+
138+
// ---------------------------------------------------------------------------
139+
// buildConnection
140+
// ---------------------------------------------------------------------------
141+
142+
describe('buildConnection', () => {
143+
function makeItems(count: number) {
144+
return Array.from({ length: count }, (_, i) => ({
145+
id: `item-${i + 1}`,
146+
title: `Item ${i + 1}`,
147+
}));
148+
}
149+
150+
it('returns an empty connection for an empty result set', () => {
151+
const params = buildCursorParams({});
152+
const connection = buildConnection([], params, 0);
153+
154+
expect(connection.edges).toHaveLength(0);
155+
expect(connection.pageInfo.hasNextPage).toBe(false);
156+
expect(connection.pageInfo.hasPreviousPage).toBe(false);
157+
expect(connection.pageInfo.startCursor).toBeNull();
158+
expect(connection.pageInfo.endCursor).toBeNull();
159+
expect(connection.totalCount).toBe(0);
160+
});
161+
162+
it('returns a single page when items fit exactly (no extra boundary item)', () => {
163+
const params = buildCursorParams({ first: 5 });
164+
// 5 items returned means no extra item => no next page
165+
const items = makeItems(5);
166+
const connection = buildConnection(items, params, 5);
167+
168+
expect(connection.edges).toHaveLength(5);
169+
expect(connection.pageInfo.hasNextPage).toBe(false);
170+
expect(connection.pageInfo.hasPreviousPage).toBe(false);
171+
expect(connection.totalCount).toBe(5);
172+
});
173+
174+
it('detects hasNextPage when forward pagination returns an extra item', () => {
175+
const params = buildCursorParams({ first: 3 });
176+
// 4 items returned (3 requested + 1 extra) => has next page
177+
const items = makeItems(4);
178+
const connection = buildConnection(items, params, 10);
179+
180+
expect(connection.edges).toHaveLength(3);
181+
expect(connection.pageInfo.hasNextPage).toBe(true);
182+
expect(connection.pageInfo.hasPreviousPage).toBe(false);
183+
expect(connection.totalCount).toBe(10);
184+
});
185+
186+
it('sets hasPreviousPage when forward pagination uses a cursor', () => {
187+
const afterCursor = encodeCursor('item-0');
188+
const params = buildCursorParams({ first: 3, after: afterCursor });
189+
const items = makeItems(3);
190+
const connection = buildConnection(items, params, 10);
191+
192+
expect(connection.pageInfo.hasPreviousPage).toBe(true);
193+
expect(connection.pageInfo.hasNextPage).toBe(false);
194+
});
195+
196+
it('detects hasPreviousPage for backward pagination with extra item', () => {
197+
const beforeCursor = encodeCursor('item-10');
198+
const params = buildCursorParams({ last: 3, before: beforeCursor });
199+
// 4 items returned => extra boundary item at index 0
200+
const items = makeItems(4);
201+
const connection = buildConnection(items, params, 10);
202+
203+
expect(connection.edges).toHaveLength(3);
204+
expect(connection.pageInfo.hasPreviousPage).toBe(true);
205+
// hasNextPage is true because we have a cursor (backward from item-10)
206+
expect(connection.pageInfo.hasNextPage).toBe(true);
207+
});
208+
209+
it('sets correct startCursor and endCursor', () => {
210+
const params = buildCursorParams({ first: 3 });
211+
const items = makeItems(3);
212+
const connection = buildConnection(items, params, 3);
213+
214+
expect(connection.pageInfo.startCursor).toBe(encodeCursor('item-1'));
215+
expect(connection.pageInfo.endCursor).toBe(encodeCursor('item-3'));
216+
});
217+
218+
it('passes totalCount through', () => {
219+
const params = buildCursorParams({ first: 2 });
220+
const items = makeItems(2);
221+
const connection = buildConnection(items, params, 999);
222+
223+
expect(connection.totalCount).toBe(999);
224+
});
225+
226+
it('builds edges with correct node and cursor pairs', () => {
227+
const params = buildCursorParams({ first: 2 });
228+
const items = makeItems(2);
229+
const connection = buildConnection(items, params, 2);
230+
231+
expect(connection.edges[0]).toEqual({
232+
node: { id: 'item-1', title: 'Item 1' },
233+
cursor: encodeCursor('item-1'),
234+
});
235+
expect(connection.edges[1]).toEqual({
236+
node: { id: 'item-2', title: 'Item 2' },
237+
cursor: encodeCursor('item-2'),
238+
});
239+
});
240+
241+
it('trims the extra boundary item from the end in forward pagination', () => {
242+
const params = buildCursorParams({ first: 2 });
243+
// 3 items => the third is the boundary item
244+
const items = makeItems(3);
245+
const connection = buildConnection(items, params, 5);
246+
247+
const ids = connection.edges.map((e) => e.node.id);
248+
expect(ids).toEqual(['item-1', 'item-2']);
249+
});
250+
251+
it('trims the extra boundary item from the start in backward pagination', () => {
252+
const params = buildCursorParams({ last: 2 });
253+
// 3 items in backward => the first is the boundary item
254+
const items = makeItems(3);
255+
const connection = buildConnection(items, params, 5);
256+
257+
const ids = connection.edges.map((e) => e.node.id);
258+
// Boundary item (item-1) trimmed from start
259+
expect(ids).toEqual(['item-2', 'item-3']);
260+
});
261+
});

0 commit comments

Comments
 (0)