learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 319 lines 9.5 kB view raw
1import "fake-indexeddb/auto"; 2import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 3import { db, type LocalDeck } from "../db"; 4 5vi.mock( 6 "../api", 7 () => ({ 8 api: { 9 pushDeck: vi.fn().mockResolvedValue({ 10 ok: true, 11 json: async () => ({ pds_cid: "cid123", pds_uri: "at://test" }), 12 }), 13 pushNote: vi.fn().mockResolvedValue({ 14 ok: true, 15 json: async () => ({ pds_cid: "cid456", pds_uri: "at://test" }), 16 }), 17 getSyncStatus: vi.fn().mockResolvedValue({ 18 ok: true, 19 json: async () => ({ pending_count: 0, conflict_count: 0 }), 20 }), 21 resolveConflict: vi.fn().mockResolvedValue({ ok: true }), 22 }, 23 }), 24); 25 26vi.mock("../store", () => ({ authStore: { isAuthenticated: () => true, accessJwt: () => "test-token" } })); 27 28import { syncStore } from "../sync-store"; 29 30describe("syncStore", () => { 31 beforeEach(async () => { 32 await db.decks.clear(); 33 await db.cards.clear(); 34 await db.notes.clear(); 35 await db.syncQueue.clear(); 36 }); 37 38 afterEach(async () => { 39 await db.decks.clear(); 40 await db.cards.clear(); 41 await db.notes.clear(); 42 await db.syncQueue.clear(); 43 }); 44 45 describe("state signals", () => { 46 it("should have initial idle sync state", () => { 47 expect(["idle", "offline"]).toContain(syncStore.syncState()); 48 }); 49 50 it("should have online status signal", () => { 51 expect(typeof syncStore.isOnline()).toBe("boolean"); 52 }); 53 }); 54 55 describe("saveDeckLocally", () => { 56 it("should save a new deck with local_only status", async () => { 57 const deck = await syncStore.saveDeckLocally({ 58 ownerDid: "did:plc:test", 59 title: "New Deck", 60 description: "Test description", 61 tags: ["test"], 62 visibility: { type: "Private" }, 63 }); 64 65 expect(deck.id).toMatch(/^local_/); 66 expect(deck.syncStatus).toBe("local_only"); 67 expect(deck.localVersion).toBe(1); 68 69 const stored = await db.decks.get(deck.id); 70 expect(stored?.title).toBe("New Deck"); 71 }); 72 73 it("should update existing deck with pending_push status", async () => { 74 const deck = await syncStore.saveDeckLocally({ 75 ownerDid: "did:plc:test", 76 title: "Original Title", 77 description: "Test", 78 tags: [], 79 visibility: { type: "Private" }, 80 }); 81 82 const updated = await syncStore.saveDeckLocally({ 83 id: deck.id, 84 ownerDid: "did:plc:test", 85 title: "Updated Title", 86 description: "Test", 87 tags: [], 88 visibility: { type: "Private" }, 89 }); 90 91 expect(updated.id).toBe(deck.id); 92 expect(updated.title).toBe("Updated Title"); 93 expect(updated.syncStatus).toBe("pending_push"); 94 expect(updated.localVersion).toBe(2); 95 }); 96 }); 97 98 describe("saveNoteLocally", () => { 99 it("should save a new note with local_only status", async () => { 100 const note = await syncStore.saveNoteLocally({ 101 ownerDid: "did:plc:test", 102 title: "New Note", 103 body: "Note content", 104 tags: ["test"], 105 visibility: { type: "Private" }, 106 links: [], 107 }); 108 109 expect(note.id).toMatch(/^local_/); 110 expect(note.syncStatus).toBe("local_only"); 111 expect(note.localVersion).toBe(1); 112 }); 113 }); 114 115 describe("getLocalDecks", () => { 116 it("should return decks for a specific owner", async () => { 117 await db.decks.bulkPut([ 118 { 119 id: "deck-1", 120 ownerDid: "did:alice", 121 title: "Alice Deck", 122 description: "", 123 tags: [], 124 visibility: { type: "Private" }, 125 syncStatus: "synced", 126 localVersion: 1, 127 updatedAt: new Date().toISOString(), 128 } satisfies LocalDeck, 129 { 130 id: "deck-2", 131 ownerDid: "did:bob", 132 title: "Bob Deck", 133 description: "", 134 tags: [], 135 visibility: { type: "Private" }, 136 syncStatus: "synced", 137 localVersion: 1, 138 updatedAt: new Date().toISOString(), 139 } satisfies LocalDeck, 140 ]); 141 142 const aliceDecks = await syncStore.getLocalDecks("did:alice"); 143 expect(aliceDecks).toHaveLength(1); 144 expect(aliceDecks[0].title).toBe("Alice Deck"); 145 }); 146 }); 147 148 describe("queueForSync", () => { 149 it("should add item to sync queue", async () => { 150 await syncStore.queueForSync("deck", "deck-123", "push"); 151 152 const queue = await db.syncQueue.toArray(); 153 expect(queue).toHaveLength(1); 154 expect(queue[0].entityType).toBe("deck"); 155 expect(queue[0].entityId).toBe("deck-123"); 156 expect(queue[0].operation).toBe("push"); 157 }); 158 159 it("should not duplicate queue entries", async () => { 160 await syncStore.queueForSync("deck", "deck-123", "push"); 161 await syncStore.queueForSync("deck", "deck-123", "push"); 162 163 const queue = await db.syncQueue.toArray(); 164 expect(queue).toHaveLength(1); 165 }); 166 }); 167 168 describe("refreshCounts", () => { 169 it("should update pending and conflict counts", async () => { 170 await db.syncQueue.add({ 171 entityType: "deck", 172 entityId: "deck-1", 173 operation: "push", 174 createdAt: new Date().toISOString(), 175 retryCount: 0, 176 }); 177 178 await db.decks.put({ 179 id: "deck-2", 180 ownerDid: "did:test", 181 title: "Conflict Deck", 182 description: "", 183 tags: [], 184 visibility: { type: "Private" }, 185 syncStatus: "conflict", 186 localVersion: 1, 187 updatedAt: new Date().toISOString(), 188 }); 189 190 await syncStore.refreshCounts(); 191 192 expect(syncStore.pendingCount()).toBe(1); 193 expect(syncStore.conflictCount()).toBe(1); 194 }); 195 }); 196 197 describe("clearAll", () => { 198 it("should clear all local data", async () => { 199 await db.decks.put({ 200 id: "deck-1", 201 ownerDid: "did:test", 202 title: "Test", 203 description: "", 204 tags: [], 205 visibility: { type: "Private" }, 206 syncStatus: "synced", 207 localVersion: 1, 208 updatedAt: new Date().toISOString(), 209 }); 210 211 await syncStore.clearAll(); 212 213 expect(await db.decks.count()).toBe(0); 214 expect(await db.notes.count()).toBe(0); 215 expect(await db.syncQueue.count()).toBe(0); 216 }); 217 }); 218 219 describe("saveCardLocally", () => { 220 it("should save a new card with local_only status", async () => { 221 const card = await syncStore.saveCardLocally({ 222 deckId: "deck-1", 223 front: "Question", 224 back: "Answer", 225 cardType: "basic", 226 hints: [], 227 }); 228 229 expect(card.id).toMatch(/^local_/); 230 expect(card.syncStatus).toBe("local_only"); 231 expect(card.localVersion).toBe(1); 232 233 const stored = await db.cards.get(card.id); 234 expect(stored?.front).toBe("Question"); 235 }); 236 237 it("should update existing card with pending_push status", async () => { 238 const card = await syncStore.saveCardLocally({ 239 deckId: "deck-1", 240 front: "Original", 241 back: "Answer", 242 cardType: "basic", 243 hints: [], 244 }); 245 246 const updated = await syncStore.saveCardLocally({ 247 id: card.id, 248 deckId: "deck-1", 249 front: "Updated", 250 back: "Answer", 251 cardType: "basic", 252 hints: [], 253 }); 254 255 expect(updated.id).toBe(card.id); 256 expect(updated.front).toBe("Updated"); 257 expect(updated.syncStatus).toBe("pending_push"); 258 expect(updated.localVersion).toBe(2); 259 }); 260 }); 261 262 describe("getLocalCards", () => { 263 it("should return cards for a specific deck", async () => { 264 await syncStore.saveCardLocally({ deckId: "deck-1", front: "Q1", back: "A1", cardType: "basic", hints: [] }); 265 await syncStore.saveCardLocally({ deckId: "deck-1", front: "Q2", back: "A2", cardType: "basic", hints: [] }); 266 await syncStore.saveCardLocally({ deckId: "deck-2", front: "Q3", back: "A3", cardType: "basic", hints: [] }); 267 268 const cards = await syncStore.getLocalCards("deck-1"); 269 expect(cards).toHaveLength(2); 270 expect(cards.map((c) => c.front)).toContain("Q1"); 271 expect(cards.map((c) => c.front)).toContain("Q2"); 272 }); 273 }); 274 275 describe("deleteLocalCard", () => { 276 it("should delete a card by id", async () => { 277 const card = await syncStore.saveCardLocally({ 278 deckId: "deck-1", 279 front: "To Delete", 280 back: "Answer", 281 cardType: "basic", 282 hints: [], 283 }); 284 285 await syncStore.deleteLocalCard(card.id); 286 287 const stored = await db.cards.get(card.id); 288 expect(stored).toBeUndefined(); 289 }); 290 }); 291 292 describe("getAllLocalData", () => { 293 it("should return all local decks, notes, cards, and queue items", async () => { 294 await syncStore.saveDeckLocally({ 295 ownerDid: "did:test", 296 title: "Deck", 297 description: "", 298 tags: [], 299 visibility: { type: "Private" }, 300 }); 301 await syncStore.saveNoteLocally({ 302 ownerDid: "did:test", 303 title: "Note", 304 body: "Body", 305 tags: [], 306 visibility: { type: "Private" }, 307 links: [], 308 }); 309 await syncStore.saveCardLocally({ deckId: "deck-1", front: "Q", back: "A", cardType: "basic", hints: [] }); 310 311 const data = await syncStore.getAllLocalData(); 312 313 expect(data.decks).toHaveLength(1); 314 expect(data.notes).toHaveLength(1); 315 expect(data.cards).toHaveLength(1); 316 expect(data.queue.length).toBeGreaterThanOrEqual(0); 317 }); 318 }); 319});