The Appview for the kipclip.com atproto bookmarking service
at main 300 lines 8.6 kB view raw
1/** 2 * Tests for /api/initial-data endpoint. 3 * Verifies that bookmarks include tags, pagination works correctly, 4 * and all data is returned for each page. 5 * 6 * First-page requests trigger background PDS migrations (fire-and-forget), 7 * so those tests disable resource/op sanitizers. 8 */ 9 10import "./test-setup.ts"; 11 12import { assertEquals, assertExists } from "@std/assert"; 13import { app } from "../main.ts"; 14import { initOAuth } from "../lib/oauth-config.ts"; 15import { setTestSessionProvider } from "../lib/session.ts"; 16import type { SessionResult } from "../lib/session.ts"; 17 18initOAuth(new Request("https://kipclip.com")); 19const handler = app.handler(); 20 21const TEST_DID = "did:plc:test123"; 22const BOOKMARK_COLLECTION = "community.lexicon.bookmarks.bookmark"; 23const ANNOTATION_COLLECTION = "com.kipclip.annotation"; 24const TAG_COLLECTION = "com.kipclip.tag"; 25const PREFERENCES_COLLECTION = "com.kipclip.preferences"; 26 27function createSession( 28 onRequest: (method: string, url: string, options?: any) => Response, 29): SessionResult { 30 return { 31 session: { 32 did: TEST_DID, 33 pdsUrl: "https://test.pds.example", 34 handle: "test.handle", 35 makeRequest: ( 36 method: string, 37 url: string, 38 options?: any, 39 ): Promise<Response> => { 40 return Promise.resolve(onRequest(method, url, options)); 41 }, 42 } as any, 43 setCookieHeader: "sid=mock; Path=/; HttpOnly", 44 }; 45} 46 47function bookmarkRecord( 48 rkey: string, 49 tags: string[], 50 createdAt = "2026-01-01T00:00:00.000Z", 51) { 52 return { 53 uri: `at://${TEST_DID}/${BOOKMARK_COLLECTION}/${rkey}`, 54 cid: `cid-${rkey}`, 55 value: { 56 subject: `https://example.com/${rkey}`, 57 createdAt, 58 tags, 59 }, 60 }; 61} 62 63function annotationRecord(rkey: string) { 64 return { 65 uri: `at://${TEST_DID}/${ANNOTATION_COLLECTION}/${rkey}`, 66 cid: `ann-cid-${rkey}`, 67 value: { 68 subject: `at://${TEST_DID}/${BOOKMARK_COLLECTION}/${rkey}`, 69 title: `Title for ${rkey}`, 70 createdAt: "2026-01-01T00:00:00.000Z", 71 }, 72 }; 73} 74 75function tagRecord(rkey: string, value: string) { 76 return { 77 uri: `at://${TEST_DID}/${TAG_COLLECTION}/${rkey}`, 78 cid: `cid-${rkey}`, 79 value: { value, createdAt: "2026-01-01T00:00:00.000Z" }, 80 }; 81} 82 83// ---------- First page returns bookmarks with tags ---------- 84 85Deno.test({ 86 name: "GET /api/initial-data - first page returns bookmarks with their tags", 87 sanitizeResources: false, 88 sanitizeOps: false, 89 async fn() { 90 const bookmarks = [ 91 bookmarkRecord("bm1", ["swift", "ios"]), 92 bookmarkRecord("bm2", ["react", "web"]), 93 bookmarkRecord("bm3", []), 94 ]; 95 const annotations = [annotationRecord("bm1"), annotationRecord("bm2")]; 96 const tags = [ 97 tagRecord("t1", "swift"), 98 tagRecord("t2", "ios"), 99 tagRecord("t3", "react"), 100 tagRecord("t4", "web"), 101 ]; 102 103 setTestSessionProvider(() => 104 Promise.resolve( 105 createSession((_method, url) => { 106 if ( 107 url.includes("listRecords") && 108 url.includes(BOOKMARK_COLLECTION) 109 ) { 110 return Response.json({ records: bookmarks }); 111 } 112 if ( 113 url.includes("listRecords") && 114 url.includes(ANNOTATION_COLLECTION) 115 ) { 116 return Response.json({ records: annotations }); 117 } 118 if (url.includes("listRecords") && url.includes(TAG_COLLECTION)) { 119 return Response.json({ records: tags }); 120 } 121 if ( 122 url.includes("getRecord") && 123 url.includes(PREFERENCES_COLLECTION) 124 ) { 125 return Response.json({}, { status: 400 }); 126 } 127 return Response.json({}); 128 }), 129 ) 130 ); 131 132 try { 133 const req = new Request("https://kipclip.com/api/initial-data"); 134 const res = await handler(req); 135 assertEquals(res.status, 200); 136 137 const body = await res.json(); 138 139 // Verify bookmarks include tags 140 assertEquals(body.bookmarks.length, 3); 141 assertEquals(body.bookmarks[0].tags, ["swift", "ios"]); 142 assertEquals(body.bookmarks[1].tags, ["react", "web"]); 143 assertEquals(body.bookmarks[2].tags, []); 144 145 // Verify tags are returned 146 assertEquals(body.tags.length, 4); 147 assertEquals(body.tags[0].value, "swift"); 148 149 // Verify enrichment from annotations 150 assertEquals(body.bookmarks[0].title, "Title for bm1"); 151 } finally { 152 setTestSessionProvider(null); 153 } 154 }, 155}); 156 157// ---------- Bookmarks with undefined tags get empty array ---------- 158 159Deno.test({ 160 name: "GET /api/initial-data - bookmarks without tags field get empty array", 161 sanitizeResources: false, 162 sanitizeOps: false, 163 async fn() { 164 // Simulate old bookmark records that don't have a tags field 165 const bookmarks = [{ 166 uri: `at://${TEST_DID}/${BOOKMARK_COLLECTION}/old1`, 167 cid: "cid-old1", 168 value: { 169 subject: "https://example.com/old-page", 170 createdAt: "2024-01-01T00:00:00.000Z", 171 // No tags field — simulates pre-tag bookmarks 172 }, 173 }]; 174 175 setTestSessionProvider(() => 176 Promise.resolve( 177 createSession((_method, url) => { 178 if ( 179 url.includes("listRecords") && 180 url.includes(BOOKMARK_COLLECTION) 181 ) { 182 return Response.json({ records: bookmarks }); 183 } 184 if (url.includes("listRecords")) { 185 return Response.json({ records: [] }); 186 } 187 if (url.includes("getRecord")) { 188 return Response.json({}, { status: 400 }); 189 } 190 return Response.json({}); 191 }), 192 ) 193 ); 194 195 try { 196 const req = new Request("https://kipclip.com/api/initial-data"); 197 const res = await handler(req); 198 assertEquals(res.status, 200); 199 200 const body = await res.json(); 201 assertEquals(body.bookmarks.length, 1); 202 // Should default to empty array, not undefined 203 assertEquals(body.bookmarks[0].tags, []); 204 } finally { 205 setTestSessionProvider(null); 206 } 207 }, 208}); 209 210// ---------- Subsequent pages also include tags ---------- 211 212Deno.test( 213 "GET /api/initial-data - subsequent pages include tags on bookmarks", 214 async () => { 215 const page2Bookmarks = [ 216 bookmarkRecord("bm101", ["2d", "art"]), 217 bookmarkRecord("bm102", ["gaming"]), 218 ]; 219 220 setTestSessionProvider(() => 221 Promise.resolve( 222 createSession((_method, url) => { 223 if ( 224 url.includes("listRecords") && 225 url.includes(BOOKMARK_COLLECTION) 226 ) { 227 return Response.json({ records: page2Bookmarks }); 228 } 229 if (url.includes("listRecords")) { 230 return Response.json({ records: [] }); 231 } 232 return Response.json({}); 233 }), 234 ) 235 ); 236 237 try { 238 // Request a subsequent page (with cursor) 239 const req = new Request( 240 "https://kipclip.com/api/initial-data?bookmarkCursor=page2cursor", 241 ); 242 const res = await handler(req); 243 assertEquals(res.status, 200); 244 245 const body = await res.json(); 246 assertEquals(body.bookmarks.length, 2); 247 assertEquals(body.bookmarks[0].tags, ["2d", "art"]); 248 assertEquals(body.bookmarks[1].tags, ["gaming"]); 249 } finally { 250 setTestSessionProvider(null); 251 } 252 }, 253); 254 255// ---------- First page returns cursor when more pages available ---------- 256 257Deno.test({ 258 name: "GET /api/initial-data - returns cursor when more bookmarks exist", 259 sanitizeResources: false, 260 sanitizeOps: false, 261 async fn() { 262 const bookmarks = [bookmarkRecord("bm1", ["tag1"])]; 263 264 setTestSessionProvider(() => 265 Promise.resolve( 266 createSession((_method, url) => { 267 if ( 268 url.includes("listRecords") && 269 url.includes(BOOKMARK_COLLECTION) 270 ) { 271 return Response.json({ 272 records: bookmarks, 273 cursor: "next-page-cursor", 274 }); 275 } 276 if (url.includes("listRecords")) { 277 return Response.json({ records: [] }); 278 } 279 if (url.includes("getRecord")) { 280 return Response.json({}, { status: 400 }); 281 } 282 return Response.json({}); 283 }), 284 ) 285 ); 286 287 try { 288 const req = new Request("https://kipclip.com/api/initial-data"); 289 const res = await handler(req); 290 assertEquals(res.status, 200); 291 292 const body = await res.json(); 293 assertExists(body.bookmarkCursor); 294 assertEquals(body.bookmarkCursor, "next-page-cursor"); 295 assertEquals(body.bookmarks[0].tags, ["tag1"]); 296 } finally { 297 setTestSessionProvider(null); 298 } 299 }, 300});