A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
42
fork

Configure Feed

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

at main 317 lines 8.9 kB view raw
1// @pds/core/loader tests - Repository loader from CAR files 2import { describe, it, expect, beforeEach } from 'vitest'; 3import { 4 loadRepositoryFromCar, 5 getCarRepoInfo, 6 validateCarFile, 7} from '../packages/core/src/loader.js'; 8import { 9 buildCarFile, 10 CID, 11 cborEncodeDagCbor, 12 createCid, 13 cidToString, 14 cidToBytes, 15 createTid, 16} from '../packages/core/src/repo.js'; 17import { buildMst } from '../packages/core/src/mst.js'; 18 19// In-memory storage for tests 20function createMockStorage() { 21 /** @type {Map<string, Uint8Array>} */ 22 const blocks = new Map(); 23 /** @type {Map<string, {cid: string, value: Uint8Array}>} */ 24 const records = new Map(); 25 /** @type {Array<{seq: number, cid: string, rev: string, prev: string|null}>} */ 26 const commits = []; 27 /** @type {{did: string|null, handle: string|null, privateKey: Uint8Array|null}} */ 28 const metadata = { did: null, handle: null, privateKey: null }; 29 30 return { 31 // Block operations 32 async getBlock(cid) { 33 return blocks.get(cid) || null; 34 }, 35 async putBlock(cid, data) { 36 blocks.set(cid, data); 37 }, 38 39 // Record operations 40 async getRecord(uri) { 41 const rec = records.get(uri); 42 return rec || null; 43 }, 44 async putRecord(uri, cid, collection, rkey, value) { 45 records.set(uri, { cid, value }); 46 }, 47 async listRecords(collection, cursor, limit) { 48 const all = []; 49 for (const [uri, rec] of records) { 50 if (uri.includes(`/${collection}/`)) { 51 const parts = uri.split('/'); 52 all.push({ uri, cid: rec.cid, value: rec.value, rkey: parts[parts.length - 1] }); 53 } 54 } 55 return { records: all.slice(0, limit), cursor: null }; 56 }, 57 async listAllRecords() { 58 const all = []; 59 for (const [uri, rec] of records) { 60 const parts = uri.replace('at://', '').split('/'); 61 const key = `${parts[1]}/${parts[2]}`; 62 all.push({ key, cid: rec.cid }); 63 } 64 return all; 65 }, 66 async deleteRecord(uri) { 67 records.delete(uri); 68 }, 69 70 // Commit operations 71 async getLatestCommit() { 72 return commits.length > 0 ? commits[commits.length - 1] : null; 73 }, 74 async putCommit(seq, cid, rev, prev) { 75 commits.push({ seq, cid, rev, prev }); 76 }, 77 78 // Metadata operations 79 async getDid() { 80 return metadata.did; 81 }, 82 async setDid(did) { 83 metadata.did = did; 84 }, 85 async getHandle() { 86 return metadata.handle; 87 }, 88 async setHandle(handle) { 89 metadata.handle = handle; 90 }, 91 async getPrivateKey() { 92 return metadata.privateKey; 93 }, 94 async setPrivateKey(key) { 95 metadata.privateKey = key; 96 }, 97 async getPreferences() { 98 return []; 99 }, 100 async setPreferences() {}, 101 102 // Events 103 async getEvents() { 104 return { events: [], cursor: 0 }; 105 }, 106 async putEvent() {}, 107 108 // Blobs 109 async getBlob() { 110 return null; 111 }, 112 async putBlob() {}, 113 async deleteBlob() {}, 114 async listBlobs() { 115 return { cids: [], cursor: null }; 116 }, 117 async getOrphanedBlobs() { 118 return []; 119 }, 120 async linkBlobToRecord() {}, 121 async unlinkBlobsFromRecord() {}, 122 123 // Test helpers 124 _getBlockCount() { 125 return blocks.size; 126 }, 127 _getRecordCount() { 128 return records.size; 129 }, 130 _getCommits() { 131 return commits; 132 }, 133 }; 134} 135 136/** 137 * Build a test CAR file with records 138 * @param {string} did 139 * @param {Array<{collection: string, rkey: string, record: object}>} recordsToInclude 140 * @returns {Promise<Uint8Array>} 141 */ 142async function buildTestCar(did, recordsToInclude) { 143 /** @type {Array<{cid: string, data: Uint8Array}>} */ 144 const blocks = []; 145 /** @type {Map<string, Uint8Array>} */ 146 const blockMap = new Map(); 147 148 // Encode records and build MST entries 149 /** @type {Array<{key: string, cid: string}>} */ 150 const mstEntries = []; 151 152 for (const { collection, rkey, record } of recordsToInclude) { 153 const recordBytes = cborEncodeDagCbor(record); 154 const recordCid = cidToString(await createCid(recordBytes)); 155 blocks.push({ cid: recordCid, data: recordBytes }); 156 blockMap.set(recordCid, recordBytes); 157 mstEntries.push({ key: `${collection}/${rkey}`, cid: recordCid }); 158 } 159 160 // Sort entries for MST 161 mstEntries.sort((a, b) => a.key.localeCompare(b.key)); 162 163 // Build MST 164 const mstRoot = await buildMst(mstEntries, async (cid, data) => { 165 blocks.push({ cid, data }); 166 blockMap.set(cid, data); 167 }); 168 169 // Build commit 170 const rev = createTid(); 171 const commit = { 172 did, 173 version: 3, 174 rev, 175 prev: null, 176 data: mstRoot ? new CID(cidToBytes(mstRoot)) : null, 177 }; 178 179 // Add signature field (empty for test) 180 const signedCommit = { ...commit, sig: new Uint8Array(64) }; 181 const commitBytes = cborEncodeDagCbor(signedCommit); 182 const commitCid = cidToString(await createCid(commitBytes)); 183 blocks.push({ cid: commitCid, data: commitBytes }); 184 185 return buildCarFile(commitCid, blocks); 186} 187 188describe('loadRepositoryFromCar', () => { 189 let storage; 190 191 beforeEach(() => { 192 storage = createMockStorage(); 193 }); 194 195 it('should load empty repository', async () => { 196 const did = 'did:plc:test123'; 197 const car = await buildTestCar(did, []); 198 199 const result = await loadRepositoryFromCar(car, storage); 200 201 expect(result.did).toBe(did); 202 expect(result.recordCount).toBe(0); 203 expect(result.blockCount).toBeGreaterThan(0); 204 expect(await storage.getDid()).toBe(did); 205 }); 206 207 it('should load repository with single record', async () => { 208 const did = 'did:plc:test123'; 209 const car = await buildTestCar(did, [ 210 { 211 collection: 'app.bsky.feed.post', 212 rkey: '3abc123', 213 record: { $type: 'app.bsky.feed.post', text: 'Hello World', createdAt: '2024-01-01T00:00:00Z' }, 214 }, 215 ]); 216 217 const result = await loadRepositoryFromCar(car, storage); 218 219 expect(result.did).toBe(did); 220 expect(result.recordCount).toBe(1); 221 222 const record = await storage.getRecord(`at://${did}/app.bsky.feed.post/3abc123`); 223 expect(record).not.toBeNull(); 224 expect(record?.cid).toBeDefined(); 225 }); 226 227 it('should load repository with multiple records', async () => { 228 const did = 'did:plc:test456'; 229 const car = await buildTestCar(did, [ 230 { 231 collection: 'app.bsky.feed.post', 232 rkey: '3post1', 233 record: { $type: 'app.bsky.feed.post', text: 'Post 1', createdAt: '2024-01-01T00:00:00Z' }, 234 }, 235 { 236 collection: 'app.bsky.feed.post', 237 rkey: '3post2', 238 record: { $type: 'app.bsky.feed.post', text: 'Post 2', createdAt: '2024-01-02T00:00:00Z' }, 239 }, 240 { 241 collection: 'app.bsky.actor.profile', 242 rkey: 'self', 243 record: { $type: 'app.bsky.actor.profile', displayName: 'Test User' }, 244 }, 245 ]); 246 247 const result = await loadRepositoryFromCar(car, storage); 248 249 expect(result.did).toBe(did); 250 expect(result.recordCount).toBe(3); 251 252 // Verify all records exist 253 expect(await storage.getRecord(`at://${did}/app.bsky.feed.post/3post1`)).not.toBeNull(); 254 expect(await storage.getRecord(`at://${did}/app.bsky.feed.post/3post2`)).not.toBeNull(); 255 expect(await storage.getRecord(`at://${did}/app.bsky.actor.profile/self`)).not.toBeNull(); 256 }); 257 258 it('should create commit in storage', async () => { 259 const did = 'did:plc:test789'; 260 const car = await buildTestCar(did, [ 261 { 262 collection: 'app.bsky.feed.post', 263 rkey: '3test', 264 record: { $type: 'app.bsky.feed.post', text: 'Test', createdAt: '2024-01-01T00:00:00Z' }, 265 }, 266 ]); 267 268 const result = await loadRepositoryFromCar(car, storage); 269 const commit = await storage.getLatestCommit(); 270 271 expect(commit).not.toBeNull(); 272 expect(commit?.cid).toBe(result.commitCid); 273 expect(commit?.rev).toBe(result.rev); 274 expect(commit?.seq).toBe(1); 275 }); 276}); 277 278describe('getCarRepoInfo', () => { 279 it('should extract DID and commit info', async () => { 280 const did = 'did:plc:infotest'; 281 const car = await buildTestCar(did, [ 282 { 283 collection: 'app.bsky.feed.post', 284 rkey: '3test', 285 record: { $type: 'app.bsky.feed.post', text: 'Test', createdAt: '2024-01-01T00:00:00Z' }, 286 }, 287 ]); 288 289 const info = getCarRepoInfo(car); 290 291 expect(info.did).toBe(did); 292 expect(info.commitCid).toBeDefined(); 293 expect(info.rev).toBeDefined(); 294 }); 295}); 296 297describe('validateCarFile', () => { 298 it('should validate correct CAR file', async () => { 299 const did = 'did:plc:validtest'; 300 const car = await buildTestCar(did, []); 301 302 const result = validateCarFile(car); 303 304 expect(result.valid).toBe(true); 305 expect(result.did).toBe(did); 306 expect(result.error).toBeUndefined(); 307 }); 308 309 it('should reject malformed CAR', () => { 310 const garbage = new Uint8Array([0xff, 0xff, 0xff]); 311 312 const result = validateCarFile(garbage); 313 314 expect(result.valid).toBe(false); 315 expect(result.error).toBeDefined(); 316 }); 317});