Skip to content

Commit 5c48a02

Browse files
committed
test: fill remaining coverage gaps — large payloads, CORS, OAuth negatives, WS reconnect, cross-graph REST
1 parent 3224b2d commit 5c48a02

5 files changed

Lines changed: 362 additions & 0 deletions

File tree

demo-projects/test-sandbox/tests/08-edge-cases.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,73 @@ test('Search with unicode query → no crash', async () => {
290290
assert(res.status < 500, 'should not 500');
291291
});
292292

293+
// ─── 8.7 Large payload ─────────────────────────────────────────
294+
295+
group('8.7 Large payload');
296+
297+
test('POST body near limit (5MB) → accepted', async () => {
298+
const bigContent = 'x'.repeat(5 * 1024 * 1024);
299+
const res = await post('/knowledge/notes', {
300+
title: 'Big Note',
301+
content: bigContent,
302+
});
303+
// Should be accepted (limit is 10mb)
304+
if (res.ok) {
305+
await del(`/knowledge/notes/${res.data.id}`);
306+
}
307+
assert(res.status < 500, `should not 500, got ${res.status}`);
308+
});
309+
310+
test('POST body exceeding limit (15MB) → 413', async () => {
311+
const hugeContent = 'x'.repeat(15 * 1024 * 1024);
312+
const res = await post('/knowledge/notes', {
313+
title: 'Huge Note',
314+
content: hugeContent,
315+
});
316+
assert(res.status === 413 || res.status === 400, `expected 413 or 400, got ${res.status}`);
317+
});
318+
319+
// ─── 8.8 CORS headers ──────────────────────────────────────────
320+
321+
group('8.8 CORS headers');
322+
323+
test('Response includes CORS headers', async () => {
324+
const res = await get('/api/auth/status');
325+
assertOk(res);
326+
// CORS is enabled (app.use(cors(...))), should have Access-Control-Allow-Origin
327+
const origin = res.headers.get('access-control-allow-origin');
328+
assertExists(origin, 'Access-Control-Allow-Origin header');
329+
});
330+
331+
test('OPTIONS preflight returns CORS headers', async () => {
332+
const url = `http://127.0.0.1:3737/api/auth/status`;
333+
const res = await fetch(url, {
334+
method: 'OPTIONS',
335+
headers: {
336+
'Origin': 'http://localhost:3000',
337+
'Access-Control-Request-Method': 'GET',
338+
},
339+
});
340+
assert(res.status < 500, `OPTIONS should not 500, got ${res.status}`);
341+
const allow = res.headers.get('access-control-allow-origin')
342+
?? res.headers.get('access-control-allow-methods');
343+
assertExists(allow, 'CORS preflight headers');
344+
});
345+
346+
// ─── 8.9 Invalid project ────────────────────────────────────────
347+
348+
group('8.9 Invalid project');
349+
350+
test('GET /api/projects/nonexistent/stats → 404', async () => {
351+
const res = await get('http://127.0.0.1:3737/api/projects/nonexistent/stats');
352+
assertStatus(res, 404);
353+
});
354+
355+
test('GET /api/projects/nonexistent/knowledge/notes → 404', async () => {
356+
const res = await get('http://127.0.0.1:3737/api/projects/nonexistent/knowledge/notes');
357+
assertStatus(res, 404);
358+
});
359+
293360
// ─── Run ─────────────────────────────────────────────────────────
294361

295362
export async function run() {

demo-projects/test-sandbox/tests/11-oauth.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,66 @@ test('POST /api/oauth/end-session — returns 200', async () => {
210210
assertOk(res);
211211
});
212212

213+
// ─── 11.8 Negative OAuth cases ─────────────────────────────────
214+
215+
group('11.8 Negative OAuth cases');
216+
217+
test('POST /api/oauth/token — invalid grant_type → error', async () => {
218+
const res = await restWith(BASE, 'POST', '/api/oauth/token', {
219+
grant_type: 'client_credentials',
220+
client_id: 'test-client',
221+
});
222+
assert(res.status >= 400, 'unsupported grant_type should fail');
223+
});
224+
225+
test('POST /api/oauth/token — missing code → error', async () => {
226+
const res = await restWith(BASE, 'POST', '/api/oauth/token', {
227+
grant_type: 'authorization_code',
228+
client_id: 'test-client',
229+
redirect_uri: 'http://localhost:9999/callback',
230+
code_verifier: codeVerifier,
231+
});
232+
assert(res.status >= 400, 'missing code should fail');
233+
});
234+
235+
test('POST /api/oauth/token — expired/fake code → error', async () => {
236+
const res = await restWith(BASE, 'POST', '/api/oauth/token', {
237+
grant_type: 'authorization_code',
238+
code: 'fake-expired-code-12345',
239+
client_id: 'test-client',
240+
redirect_uri: 'http://localhost:9999/callback',
241+
code_verifier: codeVerifier,
242+
});
243+
assert(res.status >= 400, 'fake code should fail');
244+
});
245+
246+
test('POST /api/oauth/token — refresh with invalid token → error', async () => {
247+
const res = await restWith(BASE, 'POST', '/api/oauth/token', {
248+
grant_type: 'refresh_token',
249+
refresh_token: 'invalid-refresh-token',
250+
client_id: 'test-client',
251+
});
252+
assert(res.status >= 400, 'invalid refresh token should fail');
253+
});
254+
255+
test('POST /api/oauth/authorize — missing code_challenge → error', async () => {
256+
const res = await restWith(BASE, 'POST', '/api/oauth/authorize', {
257+
response_type: 'code',
258+
client_id: 'test-client',
259+
redirect_uri: 'http://localhost:9999/callback',
260+
// no code_challenge
261+
}, { cookie: cookieHeader(adminCookies) });
262+
assert(res.status >= 400, 'missing code_challenge should fail');
263+
});
264+
265+
test('GET /api/oauth/userinfo with revoked token — still works (revoke is no-op)', async () => {
266+
// OAuth revoke endpoint returns 200 but does not actually invalidate JWTs
267+
// (stateless tokens can't be revoked without a blocklist)
268+
const res = await restWith(BASE, 'GET', '/api/oauth/userinfo',
269+
undefined, { bearer: accessToken });
270+
assertOk(res);
271+
});
272+
213273
// ─── Teardown ────────────────────────────────────────────────────
214274

215275
group('Teardown');

demo-projects/test-sandbox/tests/13-websocket.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,70 @@ test('Add attachment → receives event', async () => {
236236
try { require('fs').unlinkSync(tmpFile); } catch {}
237237
});
238238

239+
// ─── 13.7 WebSocket reconnection ───────────────────────────────
240+
241+
group('13.7 WebSocket reconnection');
242+
243+
test('Reconnect after close — receives events', async () => {
244+
// Close current connection
245+
if (ws && ws.readyState === WebSocket.OPEN) ws.close();
246+
await wait(500);
247+
248+
// Reconnect
249+
await connectWs();
250+
assert(ws.readyState === WebSocket.OPEN, 'ws should reconnect');
251+
252+
// Verify events still flow
253+
clearReceived();
254+
await restWith(BASE, 'POST', '/api/projects/sandbox/knowledge/notes',
255+
{ title: 'Reconnect Test', content: 'test' });
256+
const evt = await findEvent('note:created');
257+
assertExists(evt, 'note:created after reconnect');
258+
259+
// Cleanup
260+
const listRes = await restWith(BASE, 'GET', '/api/projects/sandbox/knowledge/notes');
261+
for (const n of (listRes.data.results ?? listRes.data)) {
262+
if (n.title === 'Reconnect Test') {
263+
await restWith(BASE, 'DELETE', `/api/projects/sandbox/knowledge/notes/${n.id}`);
264+
}
265+
}
266+
});
267+
268+
test('Multiple WS clients receive same event', async () => {
269+
const received2: any[] = [];
270+
const ws2 = new WebSocket(`ws://127.0.0.1:${PORT}/api/ws`);
271+
await new Promise<void>((resolve, reject) => {
272+
ws2.on('open', resolve);
273+
ws2.on('error', reject);
274+
setTimeout(() => reject(new Error('ws2 timeout')), 5000);
275+
});
276+
ws2.on('message', (data) => {
277+
try { received2.push(JSON.parse(data.toString())); } catch {}
278+
});
279+
280+
clearReceived();
281+
await restWith(BASE, 'POST', '/api/projects/sandbox/knowledge/notes',
282+
{ title: 'Multi WS Test', content: 'test' });
283+
284+
// Both clients should receive the event
285+
const evt1 = await findEvent('note:created');
286+
assertExists(evt1, 'client 1 received');
287+
288+
await wait(500);
289+
const evt2 = received2.find(e => e.type === 'note:created');
290+
assertExists(evt2, 'client 2 received');
291+
292+
ws2.close();
293+
294+
// Cleanup
295+
const listRes = await restWith(BASE, 'GET', '/api/projects/sandbox/knowledge/notes');
296+
for (const n of (listRes.data.results ?? listRes.data)) {
297+
if (n.title === 'Multi WS Test') {
298+
await restWith(BASE, 'DELETE', `/api/projects/sandbox/knowledge/notes/${n.id}`);
299+
}
300+
}
301+
});
302+
239303
// ─── Teardown ────────────────────────────────────────────────────
240304

241305
group('Teardown');

demo-projects/test-sandbox/tests/15-workspace.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,38 @@ test('Shared graphs have same counts from both projects', async () => {
224224
assertEqual(resA.data.tasks.nodes, resB.data.tasks.nodes, 'tasks nodes should be same');
225225
});
226226

227+
// ─── 15.9 Concurrent workspace writes ──────────────────────────
228+
229+
group('15.9 Concurrent workspace writes');
230+
231+
test('Parallel note creates from both projects — all succeed', async () => {
232+
const promises = [
233+
...Array.from({ length: 5 }, (_, i) =>
234+
restWith(BASE, 'POST', `${API_A()}/knowledge/notes`, {
235+
title: `WS Concurrent A-${i}`, content: `from A #${i}`,
236+
}),
237+
),
238+
...Array.from({ length: 5 }, (_, i) =>
239+
restWith(BASE, 'POST', `${API_B()}/knowledge/notes`, {
240+
title: `WS Concurrent B-${i}`, content: `from B #${i}`,
241+
}),
242+
),
243+
];
244+
const results = await Promise.all(promises);
245+
const ok = results.filter(r => r.ok);
246+
assert(ok.length === 10, `all 10 should succeed, got ${ok.length}`);
247+
248+
// Verify all visible from both projects
249+
const notesA = await restWith(BASE, 'GET', `${API_A()}/knowledge/notes`);
250+
const notesB = await restWith(BASE, 'GET', `${API_B()}/knowledge/notes`);
251+
assertOk(notesA);
252+
assertOk(notesB);
253+
const listA = notesA.data.results ?? notesA.data;
254+
const listB = notesB.data.results ?? notesB.data;
255+
assert(listA.length >= 10, `project A should see >= 10 notes, got ${listA.length}`);
256+
assertEqual(listA.length, listB.length, 'both projects see same count');
257+
});
258+
227259
// ─── Cleanup ─────────────────────────────────────────────────────
228260

229261
group('Cleanup');

0 commit comments

Comments
 (0)