Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 20 kB view raw
1import { describe, test, expect } from 'bun:test' 2import { sanitizePath, extractBlobCid, extractSubfsUris, expandSubfsNodes } from './utils' 3import { CID } from 'multiformats' 4import { BlobRef } from '@atproto/lexicon' 5import type { 6 Record as WispFsRecord, 7 Directory as FsDirectory, 8 Entry as FsEntry, 9 File as FsFile, 10 Subfs as FsSubfs, 11} from '@wisp/lexicons/types/place/wisp/fs' 12import type { 13 Record as SubfsRecord, 14 Directory as SubfsDirectory, 15 Entry as SubfsEntry, 16 File as SubfsFile, 17 Subfs as SubfsSubfs, 18} from '@wisp/lexicons/types/place/wisp/subfs' 19import type { $Typed } from '@wisp/lexicons/util' 20 21describe('sanitizePath', () => { 22 test('allows normal file paths', () => { 23 expect(sanitizePath('index.html')).toBe('index.html') 24 expect(sanitizePath('css/styles.css')).toBe('css/styles.css') 25 expect(sanitizePath('images/logo.png')).toBe('images/logo.png') 26 expect(sanitizePath('js/app.js')).toBe('js/app.js') 27 }) 28 29 test('allows deeply nested paths', () => { 30 expect(sanitizePath('assets/images/icons/favicon.ico')).toBe('assets/images/icons/favicon.ico') 31 expect(sanitizePath('a/b/c/d/e/f.txt')).toBe('a/b/c/d/e/f.txt') 32 }) 33 34 test('removes leading slashes', () => { 35 expect(sanitizePath('/index.html')).toBe('index.html') 36 expect(sanitizePath('//index.html')).toBe('index.html') 37 expect(sanitizePath('///index.html')).toBe('index.html') 38 expect(sanitizePath('/css/styles.css')).toBe('css/styles.css') 39 }) 40 41 test('blocks parent directory traversal', () => { 42 expect(sanitizePath('../etc/passwd')).toBe('etc/passwd') 43 expect(sanitizePath('../../etc/passwd')).toBe('etc/passwd') 44 expect(sanitizePath('../../../etc/passwd')).toBe('etc/passwd') 45 expect(sanitizePath('css/../../../etc/passwd')).toBe('css/etc/passwd') 46 }) 47 48 test('blocks directory traversal in middle of path', () => { 49 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd') 50 expect(sanitizePath('a/b/../c')).toBe('a/b/c') 51 expect(sanitizePath('a/../b/../c')).toBe('a/b/c') 52 }) 53 54 test('removes current directory references', () => { 55 expect(sanitizePath('./index.html')).toBe('index.html') 56 expect(sanitizePath('././index.html')).toBe('index.html') 57 expect(sanitizePath('css/./styles.css')).toBe('css/styles.css') 58 expect(sanitizePath('./css/./styles.css')).toBe('css/styles.css') 59 }) 60 61 test('removes empty path segments', () => { 62 expect(sanitizePath('css//styles.css')).toBe('css/styles.css') 63 expect(sanitizePath('css///styles.css')).toBe('css/styles.css') 64 expect(sanitizePath('a//b//c')).toBe('a/b/c') 65 }) 66 67 test('blocks null bytes', () => { 68 expect(sanitizePath('index.html\0.txt')).toBe('') 69 expect(sanitizePath('test\0')).toBe('') 70 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css') 71 }) 72 73 test('handles mixed attacks', () => { 74 expect(sanitizePath('/../../../etc/passwd')).toBe('etc/passwd') 75 expect(sanitizePath('/./././../etc/passwd')).toBe('etc/passwd') 76 expect(sanitizePath('//../../.\0./etc/passwd')).toBe('etc/passwd') 77 }) 78 79 test('handles edge cases', () => { 80 expect(sanitizePath('')).toBe('') 81 expect(sanitizePath('/')).toBe('') 82 expect(sanitizePath('//')).toBe('') 83 expect(sanitizePath('.')).toBe('') 84 expect(sanitizePath('..')).toBe('') 85 expect(sanitizePath('../..')).toBe('') 86 }) 87 88 test('preserves valid special characters in filenames', () => { 89 expect(sanitizePath('file-name.html')).toBe('file-name.html') 90 expect(sanitizePath('file_name.html')).toBe('file_name.html') 91 expect(sanitizePath('file.name.html')).toBe('file.name.html') 92 expect(sanitizePath('file (1).html')).toBe('file (1).html') 93 expect(sanitizePath('file@2x.png')).toBe('file@2x.png') 94 }) 95 96 test('handles Unicode characters', () => { 97 expect(sanitizePath('文件.html')).toBe('文件.html') 98 expect(sanitizePath('файл.html')).toBe('файл.html') 99 expect(sanitizePath('ファイル.html')).toBe('ファイル.html') 100 }) 101}) 102 103describe('extractBlobCid', () => { 104 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 105 106 test('extracts CID from IPLD link', () => { 107 const blobRef = { $link: TEST_CID } 108 expect(extractBlobCid(blobRef)).toBe(TEST_CID) 109 }) 110 111 test('extracts CID from typed BlobRef with CID object', () => { 112 const cid = CID.parse(TEST_CID) 113 const blobRef = { ref: cid } 114 const result = extractBlobCid(blobRef) 115 expect(result).toBe(TEST_CID) 116 }) 117 118 test('extracts CID from typed BlobRef with IPLD link', () => { 119 const blobRef = { 120 ref: { $link: TEST_CID } 121 } 122 expect(extractBlobCid(blobRef)).toBe(TEST_CID) 123 }) 124 125 test('extracts CID from untyped BlobRef', () => { 126 const blobRef = { cid: TEST_CID } 127 expect(extractBlobCid(blobRef)).toBe(TEST_CID) 128 }) 129 130 test('returns null for invalid blob ref', () => { 131 expect(extractBlobCid(null)).toBe(null) 132 expect(extractBlobCid(undefined)).toBe(null) 133 expect(extractBlobCid({})).toBe(null) 134 expect(extractBlobCid('not-an-object')).toBe(null) 135 expect(extractBlobCid(123)).toBe(null) 136 }) 137 138 test('returns null for malformed objects', () => { 139 expect(extractBlobCid({ wrongKey: 'value' })).toBe(null) 140 expect(extractBlobCid({ ref: 'not-a-cid' })).toBe(null) 141 expect(extractBlobCid({ ref: {} })).toBe(null) 142 }) 143 144 test('handles nested structures from AT Proto API', () => { 145 const blobRef = { 146 $type: 'blob', 147 ref: CID.parse(TEST_CID), 148 mimeType: 'text/html', 149 size: 1234 150 } 151 expect(extractBlobCid(blobRef)).toBe(TEST_CID) 152 }) 153 154 test('handles BlobRef with additional properties', () => { 155 const blobRef = { 156 ref: { $link: TEST_CID }, 157 mimeType: 'image/png', 158 size: 5678, 159 someOtherField: 'value' 160 } 161 expect(extractBlobCid(blobRef)).toBe(TEST_CID) 162 }) 163 164 test('prioritizes checking IPLD link first', () => { 165 const directLink = { $link: TEST_CID } 166 expect(extractBlobCid(directLink)).toBe(TEST_CID) 167 }) 168 169 test('handles CID v0 format', () => { 170 const cidV0 = 'QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx' 171 const blobRef = { $link: cidV0 } 172 expect(extractBlobCid(blobRef)).toBe(cidV0) 173 }) 174 175 test('handles CID v1 format', () => { 176 const cidV1 = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi' 177 const blobRef = { $link: cidV1 } 178 expect(extractBlobCid(blobRef)).toBe(cidV1) 179 }) 180}) 181 182const TEST_CID_BASE = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 183 184function createMockBlobRef(cidSuffix: string = '', size: number = 100, mimeType: string = 'text/plain'): BlobRef { 185 const cidString = TEST_CID_BASE 186 return new BlobRef(CID.parse(cidString), mimeType, size) 187} 188 189function createFsFile( 190 name: string, 191 options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 192): FsEntry { 193 const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 194 const file: $Typed<FsFile, 'place.wisp.fs#file'> = { 195 $type: 'place.wisp.fs#file', 196 type: 'file', 197 blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 198 ...(encoding && { encoding }), 199 ...(mimeType && { mimeType }), 200 ...(base64 && { base64 }), 201 } 202 return { name, node: file } 203} 204 205function createFsDirectory(name: string, entries: FsEntry[]): FsEntry { 206 const dir: $Typed<FsDirectory, 'place.wisp.fs#directory'> = { 207 $type: 'place.wisp.fs#directory', 208 type: 'directory', 209 entries, 210 } 211 return { name, node: dir } 212} 213 214function createFsSubfs(name: string, subject: string, flat: boolean = true): FsEntry { 215 const subfs: $Typed<FsSubfs, 'place.wisp.fs#subfs'> = { 216 $type: 'place.wisp.fs#subfs', 217 type: 'subfs', 218 subject, 219 flat, 220 } 221 return { name, node: subfs } 222} 223 224function createFsRootDirectory(entries: FsEntry[]): FsDirectory { 225 return { 226 $type: 'place.wisp.fs#directory', 227 type: 'directory', 228 entries, 229 } 230} 231 232function createFsRecord(site: string, entries: FsEntry[], fileCount?: number): WispFsRecord { 233 return { 234 $type: 'place.wisp.fs', 235 site, 236 root: createFsRootDirectory(entries), 237 ...(fileCount !== undefined && { fileCount }), 238 createdAt: new Date().toISOString(), 239 } 240} 241 242function createSubfsFile( 243 name: string, 244 options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 245): SubfsEntry { 246 const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 247 const file: $Typed<SubfsFile, 'place.wisp.subfs#file'> = { 248 $type: 'place.wisp.subfs#file', 249 type: 'file', 250 blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 251 ...(encoding && { encoding }), 252 ...(mimeType && { mimeType }), 253 ...(base64 && { base64 }), 254 } 255 return { name, node: file } 256} 257 258function createSubfsDirectory(name: string, entries: SubfsEntry[]): SubfsEntry { 259 const dir: $Typed<SubfsDirectory, 'place.wisp.subfs#directory'> = { 260 $type: 'place.wisp.subfs#directory', 261 type: 'directory', 262 entries, 263 } 264 return { name, node: dir } 265} 266 267function createSubfsSubfs(name: string, subject: string): SubfsEntry { 268 const subfs: $Typed<SubfsSubfs, 'place.wisp.subfs#subfs'> = { 269 $type: 'place.wisp.subfs#subfs', 270 type: 'subfs', 271 subject, 272 } 273 return { name, node: subfs } 274} 275 276function createSubfsRootDirectory(entries: SubfsEntry[]): SubfsDirectory { 277 return { 278 $type: 'place.wisp.subfs#directory', 279 type: 'directory', 280 entries, 281 } 282} 283 284function createSubfsRecord(entries: SubfsEntry[], fileCount?: number): SubfsRecord { 285 return { 286 $type: 'place.wisp.subfs', 287 root: createSubfsRootDirectory(entries), 288 ...(fileCount !== undefined && { fileCount }), 289 createdAt: new Date().toISOString(), 290 } 291} 292 293describe('extractSubfsUris', () => { 294 test('extracts subfs URIs from flat directory structure', () => { 295 const subfsUri = 'at://did:plc:test/place.wisp.subfs/a' 296 const dir = createFsRootDirectory([ 297 createFsSubfs('a', subfsUri), 298 createFsFile('file.txt'), 299 ]) 300 301 const uris = extractSubfsUris(dir) 302 303 expect(uris).toHaveLength(1) 304 expect(uris[0]).toEqual({ uri: subfsUri, path: 'a' }) 305 }) 306 307 test('extracts subfs URIs from nested directory structure', () => { 308 const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 309 const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 310 311 const dir = createFsRootDirectory([ 312 createFsSubfs('a', subfsAUri), 313 createFsDirectory('nested', [ 314 createFsSubfs('b', subfsBUri), 315 createFsFile('file.txt'), 316 ]), 317 ]) 318 319 const uris = extractSubfsUris(dir) 320 321 expect(uris).toHaveLength(2) 322 expect(uris).toContainEqual({ uri: subfsAUri, path: 'a' }) 323 expect(uris).toContainEqual({ uri: subfsBUri, path: 'nested/b' }) 324 }) 325 326 test('returns empty array when no subfs nodes exist', () => { 327 const dir = createFsRootDirectory([ 328 createFsFile('file1.txt'), 329 createFsDirectory('dir', [createFsFile('file2.txt')]), 330 ]) 331 332 const uris = extractSubfsUris(dir) 333 expect(uris).toHaveLength(0) 334 }) 335 336 test('handles deeply nested subfs', () => { 337 const subfsUri = 'at://did:plc:test/place.wisp.subfs/deep' 338 const dir = createFsRootDirectory([ 339 createFsDirectory('a', [ 340 createFsDirectory('b', [ 341 createFsDirectory('c', [ 342 createFsSubfs('deep', subfsUri), 343 ]), 344 ]), 345 ]), 346 ]) 347 348 const uris = extractSubfsUris(dir) 349 350 expect(uris).toHaveLength(1) 351 expect(uris[0]).toEqual({ uri: subfsUri, path: 'a/b/c/deep' }) 352 }) 353}) 354 355describe('expandSubfsNodes caching', () => { 356 test('cache map is populated after expansion', async () => { 357 const subfsCache = new Map<string, SubfsRecord | null>() 358 const dir = createFsRootDirectory([createFsFile('file.txt')]) 359 360 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 361 362 expect(subfsCache.size).toBe(0) 363 expect(result.entries).toHaveLength(1) 364 expect(result.entries[0]?.name).toBe('file.txt') 365 }) 366 367 test('cache is passed through recursion depths', async () => { 368 const subfsCache = new Map<string, SubfsRecord | null>() 369 const mockSubfsUri = 'at://did:plc:test/place.wisp.subfs/cached' 370 const mockRecord = createSubfsRecord([createSubfsFile('cached-file.txt')]) 371 subfsCache.set(mockSubfsUri, mockRecord) 372 373 const dir = createFsRootDirectory([createFsSubfs('cached', mockSubfsUri)]) 374 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 375 376 expect(subfsCache.has(mockSubfsUri)).toBe(true) 377 expect(result.entries).toHaveLength(1) 378 expect(result.entries[0]?.name).toBe('cached-file.txt') 379 }) 380 381 test('pre-populated cache prevents re-fetching', async () => { 382 const subfsCache = new Map<string, SubfsRecord | null>() 383 const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 384 const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 385 386 subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('b', subfsBUri)])) 387 subfsCache.set(subfsBUri, createSubfsRecord([createSubfsFile('final.txt')])) 388 389 const dir = createFsRootDirectory([createFsSubfs('a', subfsAUri)]) 390 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 391 392 expect(result.entries).toHaveLength(1) 393 expect(result.entries[0]?.name).toBe('final.txt') 394 }) 395 396 test('diamond dependency uses cache for shared reference', async () => { 397 const subfsCache = new Map<string, SubfsRecord | null>() 398 const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 399 const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 400 const subfsCUri = 'at://did:plc:test/place.wisp.subfs/c' 401 402 subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 403 subfsCache.set(subfsBUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 404 subfsCache.set(subfsCUri, createSubfsRecord([createSubfsFile('shared.txt')])) 405 406 const dir = createFsRootDirectory([ 407 createFsSubfs('a', subfsAUri), 408 createFsSubfs('b', subfsBUri), 409 ]) 410 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 411 412 expect(result.entries.filter(e => e.name === 'shared.txt')).toHaveLength(2) 413 }) 414 415 test('handles null records in cache gracefully', async () => { 416 const subfsCache = new Map<string, SubfsRecord | null>() 417 const subfsUri = 'at://did:plc:test/place.wisp.subfs/missing' 418 subfsCache.set(subfsUri, null) 419 420 const dir = createFsRootDirectory([ 421 createFsFile('file.txt'), 422 createFsSubfs('missing', subfsUri), 423 ]) 424 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 425 426 expect(result.entries.some(e => e.name === 'file.txt')).toBe(true) 427 expect(result.entries.some(e => e.name === 'missing')).toBe(true) 428 }) 429 430 test('non-flat subfs merge creates directory instead of hoisting', async () => { 431 const subfsCache = new Map<string, SubfsRecord | null>() 432 const subfsUri = 'at://did:plc:test/place.wisp.subfs/nested' 433 subfsCache.set(subfsUri, createSubfsRecord([createSubfsFile('nested-file.txt')])) 434 435 const dir = createFsRootDirectory([ 436 createFsFile('root.txt'), 437 createFsSubfs('subdir', subfsUri, false), 438 ]) 439 const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 440 441 expect(result.entries).toHaveLength(2) 442 443 const rootFile = result.entries.find(e => e.name === 'root.txt') 444 expect(rootFile).toBeDefined() 445 446 const subdir = result.entries.find(e => e.name === 'subdir') 447 expect(subdir).toBeDefined() 448 449 if (subdir && 'entries' in subdir.node) { 450 expect(subdir.node.type).toBe('directory') 451 expect(subdir.node.entries).toHaveLength(1) 452 expect(subdir.node.entries[0]?.name).toBe('nested-file.txt') 453 } 454 }) 455}) 456 457describe('WispFsRecord mock builders', () => { 458 test('createFsRecord creates valid record structure', () => { 459 const record = createFsRecord('my-site', [ 460 createFsFile('index.html', { mimeType: 'text/html' }), 461 createFsDirectory('assets', [ 462 createFsFile('style.css', { mimeType: 'text/css' }), 463 ]), 464 ]) 465 466 expect(record.$type).toBe('place.wisp.fs') 467 expect(record.site).toBe('my-site') 468 expect(record.root.type).toBe('directory') 469 expect(record.root.entries).toHaveLength(2) 470 expect(record.createdAt).toBeDefined() 471 }) 472 473 test('createFsFile creates valid file entry', () => { 474 const entry = createFsFile('test.html', { mimeType: 'text/html', size: 500 }) 475 476 expect(entry.name).toBe('test.html') 477 478 const file = entry.node 479 if ('blob' in file) { 480 expect(file.$type).toBe('place.wisp.fs#file') 481 expect(file.type).toBe('file') 482 expect(file.blob).toBeDefined() 483 expect(file.mimeType).toBe('text/html') 484 } 485 }) 486 487 test('createFsFile with gzip encoding', () => { 488 const entry = createFsFile('bundle.js', { mimeType: 'application/javascript', encoding: 'gzip' }) 489 490 const file = entry.node 491 if ('encoding' in file) { 492 expect(file.encoding).toBe('gzip') 493 } 494 }) 495 496 test('createFsFile with base64 flag', () => { 497 const entry = createFsFile('data.bin', { base64: true }) 498 499 const file = entry.node 500 if ('base64' in file) { 501 expect(file.base64).toBe(true) 502 } 503 }) 504 505 test('createFsDirectory creates valid directory entry', () => { 506 const entry = createFsDirectory('assets', [ 507 createFsFile('file1.txt'), 508 createFsFile('file2.txt'), 509 ]) 510 511 expect(entry.name).toBe('assets') 512 513 const dir = entry.node 514 if ('entries' in dir) { 515 expect(dir.$type).toBe('place.wisp.fs#directory') 516 expect(dir.type).toBe('directory') 517 expect(dir.entries).toHaveLength(2) 518 } 519 }) 520 521 test('createFsSubfs creates valid subfs entry with flat=true', () => { 522 const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext') 523 524 expect(entry.name).toBe('external') 525 526 const subfs = entry.node 527 if ('subject' in subfs) { 528 expect(subfs.$type).toBe('place.wisp.fs#subfs') 529 expect(subfs.type).toBe('subfs') 530 expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/ext') 531 expect(subfs.flat).toBe(true) 532 } 533 }) 534 535 test('createFsSubfs creates valid subfs entry with flat=false', () => { 536 const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext', false) 537 538 const subfs = entry.node 539 if ('subject' in subfs) { 540 expect(subfs.flat).toBe(false) 541 } 542 }) 543 544 test('createFsRecord with fileCount', () => { 545 const record = createFsRecord('my-site', [createFsFile('index.html')], 1) 546 expect(record.fileCount).toBe(1) 547 }) 548}) 549 550describe('SubfsRecord mock builders', () => { 551 test('createSubfsRecord creates valid record structure', () => { 552 const record = createSubfsRecord([ 553 createSubfsFile('file1.txt'), 554 createSubfsDirectory('nested', [ 555 createSubfsFile('file2.txt'), 556 ]), 557 ]) 558 559 expect(record.$type).toBe('place.wisp.subfs') 560 expect(record.root.type).toBe('directory') 561 expect(record.root.entries).toHaveLength(2) 562 expect(record.createdAt).toBeDefined() 563 }) 564 565 test('createSubfsFile creates valid file entry', () => { 566 const entry = createSubfsFile('data.json', { mimeType: 'application/json', size: 1024 }) 567 568 expect(entry.name).toBe('data.json') 569 570 const file = entry.node 571 if ('blob' in file) { 572 expect(file.$type).toBe('place.wisp.subfs#file') 573 expect(file.type).toBe('file') 574 expect(file.blob).toBeDefined() 575 expect(file.mimeType).toBe('application/json') 576 } 577 }) 578 579 test('createSubfsDirectory creates valid directory entry', () => { 580 const entry = createSubfsDirectory('subdir', [createSubfsFile('inner.txt')]) 581 582 expect(entry.name).toBe('subdir') 583 584 const dir = entry.node 585 if ('entries' in dir) { 586 expect(dir.$type).toBe('place.wisp.subfs#directory') 587 expect(dir.type).toBe('directory') 588 expect(dir.entries).toHaveLength(1) 589 } 590 }) 591 592 test('createSubfsSubfs creates valid nested subfs entry', () => { 593 const entry = createSubfsSubfs('deeper', 'at://did:plc:test/place.wisp.subfs/deeper') 594 595 expect(entry.name).toBe('deeper') 596 597 const subfs = entry.node 598 if ('subject' in subfs) { 599 expect(subfs.$type).toBe('place.wisp.subfs#subfs') 600 expect(subfs.type).toBe('subfs') 601 expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/deeper') 602 expect('flat' in subfs).toBe(false) 603 } 604 }) 605 606 test('createSubfsRecord with fileCount', () => { 607 const record = createSubfsRecord([createSubfsFile('file.txt')], 1) 608 expect(record.fileCount).toBe(1) 609 }) 610}) 611 612describe('extractBlobCid with typed mock data', () => { 613 test('extracts CID from FsFile blob', () => { 614 const entry = createFsFile('test.txt') 615 const file = entry.node 616 617 if ('blob' in file) { 618 const cid = extractBlobCid(file.blob) 619 expect(cid).toBeDefined() 620 expect(cid).toContain('bafkrei') 621 } 622 }) 623 624 test('extracts CID from SubfsFile blob', () => { 625 const entry = createSubfsFile('test.txt') 626 const file = entry.node 627 628 if ('blob' in file) { 629 const cid = extractBlobCid(file.blob) 630 expect(cid).toBeDefined() 631 expect(cid).toContain('bafkrei') 632 } 633 }) 634})