an app to share curated trails sidetrail.app
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 15 kB View raw
1/** 2 * Tests for account takedown/suspension handling 3 * 4 * Tests that: 5 * - Takendown/deleted accounts have their content removed 6 * - Suspended/deactivated accounts keep their content but are filtered from listings 7 * - Commits from takendown/deleted accounts are ignored 8 * - Reactivation restores visibility 9 */ 10 11import { describe, it, expect } from "vitest"; 12import { emit, emitAccount, ALICE, BOB, setCurrentUser, generateTid } from "./helpers"; 13import { getTestDb, trails, walks, completions, accounts } from "./helpers/test-db"; 14import { eq, count } from "drizzle-orm"; 15import { 16 loadTrails, 17 loadTrailActiveWalkers, 18 loadTrailDetail, 19 loadUserPublishedTrails, 20} from "../queries"; 21 22// ============================================================================ 23// Test Data Helpers 24// ============================================================================ 25 26function createTrailRecord() { 27 return { 28 $type: "app.sidetrail.trail", 29 title: "Test Trail", 30 description: "A test trail", 31 stops: [ 32 { tid: generateTid(), title: "Stop 1", content: "Content 1" }, 33 { tid: generateTid(), title: "Stop 2", content: "Content 2" }, 34 ], 35 accentColor: "#FF0000", 36 backgroundColor: "#FFFFFF", 37 createdAt: new Date().toISOString(), 38 }; 39} 40 41function createWalkRecord(trailUri: string, trailCid: string) { 42 return { 43 $type: "app.sidetrail.walk", 44 trail: { uri: trailUri, cid: trailCid }, 45 visitedStops: [], 46 createdAt: new Date().toISOString(), 47 }; 48} 49 50function createCompletionRecord(trailUri: string, trailCid: string) { 51 return { 52 $type: "app.sidetrail.completion", 53 trail: { uri: trailUri, cid: trailCid }, 54 createdAt: new Date().toISOString(), 55 }; 56} 57 58// ============================================================================ 59// Account Event Tests 60// ============================================================================ 61 62describe("Account Events", () => { 63 describe("account takedown", () => { 64 it("deletes all content from takendown account", async () => { 65 // Alice creates a trail 66 const trail = await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 67 68 // Alice creates a walk 69 await emit("app.sidetrail.walk", ALICE.did, createWalkRecord(trail.uri, trail.cid)); 70 71 // Alice creates a completion 72 await emit( 73 "app.sidetrail.completion", 74 ALICE.did, 75 createCompletionRecord(trail.uri, trail.cid), 76 ); 77 78 // Verify content exists 79 const db = getTestDb(); 80 let [trailCount] = await db.select({ count: count() }).from(trails); 81 let [walkCount] = await db.select({ count: count() }).from(walks); 82 let [completionCount] = await db.select({ count: count() }).from(completions); 83 84 expect(trailCount.count).toBe(1); 85 expect(walkCount.count).toBe(1); 86 expect(completionCount.count).toBe(1); 87 88 // Alice's account is taken down 89 await emitAccount(ALICE.did, false, "takendown"); 90 91 // Verify all content deleted 92 [trailCount] = await db.select({ count: count() }).from(trails); 93 [walkCount] = await db.select({ count: count() }).from(walks); 94 [completionCount] = await db.select({ count: count() }).from(completions); 95 96 expect(trailCount.count).toBe(0); 97 expect(walkCount.count).toBe(0); 98 expect(completionCount.count).toBe(0); 99 100 // Verify account status recorded 101 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 102 103 expect(accountStatus.active).toBe(0); 104 expect(accountStatus.status).toBe("takendown"); 105 }); 106 107 it("deletes all content from deleted account", async () => { 108 // Alice creates a trail 109 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 110 111 // Alice's account is deleted 112 await emitAccount(ALICE.did, false, "deleted"); 113 114 // Verify content deleted 115 const db = getTestDb(); 116 const [trailCount] = await db.select({ count: count() }).from(trails); 117 expect(trailCount.count).toBe(0); 118 119 // Verify account status 120 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 121 122 expect(accountStatus.active).toBe(0); 123 expect(accountStatus.status).toBe("deleted"); 124 }); 125 }); 126 127 describe("account suspension/deactivation", () => { 128 it("keeps content for suspended account", async () => { 129 // Alice creates a trail 130 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 131 132 // Alice's account is suspended 133 await emitAccount(ALICE.did, false, "suspended"); 134 135 // Verify content still exists 136 const db = getTestDb(); 137 const [trailCount] = await db.select({ count: count() }).from(trails); 138 expect(trailCount.count).toBe(1); 139 140 // Verify account marked inactive 141 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 142 143 expect(accountStatus.active).toBe(0); 144 expect(accountStatus.status).toBe("suspended"); 145 }); 146 147 it("keeps content for deactivated account", async () => { 148 // Alice creates a trail 149 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 150 151 // Alice deactivates her account 152 await emitAccount(ALICE.did, false, "deactivated"); 153 154 // Verify content still exists 155 const db = getTestDb(); 156 const [trailCount] = await db.select({ count: count() }).from(trails); 157 expect(trailCount.count).toBe(1); 158 159 // Verify account marked inactive 160 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 161 162 expect(accountStatus.active).toBe(0); 163 expect(accountStatus.status).toBe("deactivated"); 164 }); 165 }); 166 167 describe("account reactivation", () => { 168 it("marks account active again on reactivation", async () => { 169 // Alice creates a trail 170 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 171 172 // Alice deactivates her account 173 await emitAccount(ALICE.did, false, "deactivated"); 174 175 // Alice reactivates her account 176 await emitAccount(ALICE.did, true); 177 178 // Verify account is active 179 const db = getTestDb(); 180 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 181 182 expect(accountStatus.active).toBe(1); 183 expect(accountStatus.status).toBeNull(); 184 }); 185 }); 186 187 describe("stale event handling", () => { 188 it("ignores stale account events with lower seq", async () => { 189 // Alice creates content first (accounts are only tracked if they have content) 190 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 191 192 // First event: account suspended (seq 1) 193 await emitAccount(ALICE.did, false, "suspended"); 194 195 // Second event: account active (seq 2) 196 await emitAccount(ALICE.did, true); 197 198 // Verify account is active with seq 2 199 const db = getTestDb(); 200 const [accountStatus] = await db.select().from(accounts).where(eq(accounts.did, ALICE.did)); 201 202 expect(accountStatus.active).toBe(1); 203 expect(accountStatus.seq).toBe(2); 204 }); 205 }); 206}); 207 208// ============================================================================ 209// Query Filtering Tests 210// ============================================================================ 211 212describe("Query Filtering", () => { 213 describe("loadTrails", () => { 214 it("filters out trails from inactive accounts", async () => { 215 setCurrentUser(BOB); 216 217 // Alice creates a trail 218 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 219 220 // Bob creates a trail 221 await emit("app.sidetrail.trail", BOB.did, createTrailRecord()); 222 223 // Verify both trails visible 224 let trailsList = await loadTrails(); 225 expect(trailsList.length).toBe(2); 226 227 // Alice's account is suspended 228 await emitAccount(ALICE.did, false, "suspended"); 229 230 // Verify only Bob's trail visible 231 trailsList = await loadTrails(); 232 expect(trailsList.length).toBe(1); 233 expect(trailsList[0].creator.did).toBe(BOB.did); 234 }); 235 236 it("shows trails again after account reactivation", async () => { 237 setCurrentUser(BOB); 238 239 // Alice creates a trail 240 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 241 242 // Alice's account is deactivated 243 await emitAccount(ALICE.did, false, "deactivated"); 244 245 // Verify trail hidden 246 let trailsList = await loadTrails(); 247 expect(trailsList.length).toBe(0); 248 249 // Alice reactivates 250 await emitAccount(ALICE.did, true); 251 252 // Verify trail visible again 253 trailsList = await loadTrails(); 254 expect(trailsList.length).toBe(1); 255 }); 256 257 it("shows trails from accounts with no status record (assume active)", async () => { 258 // Alice creates a trail but has no account status record 259 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 260 261 // Verify trail is visible (no account record = assume active) 262 const trailsList = await loadTrails(); 263 expect(trailsList.length).toBe(1); 264 }); 265 }); 266 267 describe("loadTrailActiveWalkers", () => { 268 it("filters out walkers from inactive accounts", async () => { 269 // Alice creates a trail 270 const trail = await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 271 272 // Bob starts walking 273 await emit("app.sidetrail.walk", BOB.did, createWalkRecord(trail.uri, trail.cid)); 274 275 // Verify Bob appears as walker 276 let walkers = await loadTrailActiveWalkers(trail.uri); 277 expect(walkers.length).toBe(1); 278 expect(walkers[0].did).toBe(BOB.did); 279 280 // Bob's account is suspended 281 await emitAccount(BOB.did, false, "suspended"); 282 283 // Verify Bob no longer appears 284 walkers = await loadTrailActiveWalkers(trail.uri); 285 expect(walkers.length).toBe(0); 286 }); 287 288 it("shows walkers again after reactivation", async () => { 289 // Alice creates a trail 290 const trail = await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 291 292 // Bob starts walking 293 await emit("app.sidetrail.walk", BOB.did, createWalkRecord(trail.uri, trail.cid)); 294 295 // Bob deactivates 296 await emitAccount(BOB.did, false, "deactivated"); 297 298 // Bob reactivates 299 await emitAccount(BOB.did, true); 300 301 // Verify Bob appears again 302 const walkers = await loadTrailActiveWalkers(trail.uri); 303 expect(walkers.length).toBe(1); 304 expect(walkers[0].did).toBe(BOB.did); 305 }); 306 }); 307 308 describe("direct link access", () => { 309 it("trail detail returns 404 for suspended account", async () => { 310 // Alice creates a trail 311 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 312 313 // Alice's account is suspended 314 await emitAccount(ALICE.did, false, "suspended"); 315 316 // Direct link to trail should 404 317 await expect(loadTrailDetail("alice.test", "test_rkey")).rejects.toThrow("NEXT_NOT_FOUND"); 318 }); 319 320 it("trail detail returns 404 for deactivated account", async () => { 321 // Alice creates a trail 322 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 323 324 // Alice deactivates 325 await emitAccount(ALICE.did, false, "deactivated"); 326 327 // Direct link to trail should 404 328 await expect(loadTrailDetail("alice.test", "test_rkey")).rejects.toThrow("NEXT_NOT_FOUND"); 329 }); 330 331 it("profile returns 404 for inactive account", async () => { 332 // Alice creates a trail 333 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 334 335 // Alice's account is suspended 336 await emitAccount(ALICE.did, false, "suspended"); 337 338 // Direct link to profile should 404 339 await expect(loadUserPublishedTrails("alice.test")).rejects.toThrow("NEXT_NOT_FOUND"); 340 }); 341 342 it("trail detail works after reactivation", async () => { 343 // Alice creates a trail 344 const trail = await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 345 346 // Alice deactivates then reactivates 347 await emitAccount(ALICE.did, false, "deactivated"); 348 await emitAccount(ALICE.did, true); 349 350 // Direct link should work again 351 const detail = await loadTrailDetail("alice.test", trail.rkey); 352 expect(detail.header.title).toBe("Test Trail"); 353 }); 354 }); 355}); 356 357// ============================================================================ 358// Commit Blocking Tests 359// ============================================================================ 360 361describe("Commit Blocking", () => { 362 it("ignores commits from takendown accounts", async () => { 363 // Alice creates content first (which creates her account record) 364 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 365 366 // Alice's account is taken down (deletes content, marks account takendown) 367 await emitAccount(ALICE.did, false, "takendown"); 368 369 // Try to create another trail - should be ignored 370 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 371 372 // Verify no trails exist (original deleted, new one blocked) 373 const db = getTestDb(); 374 const [trailCount] = await db.select({ count: count() }).from(trails); 375 expect(trailCount.count).toBe(0); 376 }); 377 378 it("ignores commits from deleted accounts", async () => { 379 // Alice creates content first (which creates her account record) 380 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 381 382 // Alice's account is deleted 383 await emitAccount(ALICE.did, false, "deleted"); 384 385 // Try to create another trail - should be ignored 386 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 387 388 // Verify no trails exist 389 const db = getTestDb(); 390 const [trailCount] = await db.select({ count: count() }).from(trails); 391 expect(trailCount.count).toBe(0); 392 }); 393 394 it("ignores commits from suspended accounts", async () => { 395 // Alice creates content first (which creates her account record) 396 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 397 398 // Alice's account is suspended 399 await emitAccount(ALICE.did, false, "suspended"); 400 401 // Try to create another trail - should be ignored 402 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 403 404 // Verify only original trail exists (content kept but new commits blocked) 405 const db = getTestDb(); 406 const [trailCount] = await db.select({ count: count() }).from(trails); 407 expect(trailCount.count).toBe(1); 408 }); 409 410 it("ignores commits from deactivated accounts", async () => { 411 // Alice creates content first (which creates her account record) 412 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 413 414 // Alice's account is deactivated 415 await emitAccount(ALICE.did, false, "deactivated"); 416 417 // Try to create another trail - should be ignored 418 await emit("app.sidetrail.trail", ALICE.did, createTrailRecord()); 419 420 // Verify only original trail exists (content kept but new commits blocked) 421 const db = getTestDb(); 422 const [trailCount] = await db.select({ count: count() }).from(trails); 423 expect(trailCount.count).toBe(1); 424 }); 425});