Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
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})