forked from
nekomimi.pet/wisp.place-monorepo
Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { describe, test, expect } from 'bun:test'
2import { sanitizePath, extractBlobCid } from './utils'
3import { CID } from 'multiformats'
4
5describe('sanitizePath', () => {
6 test('allows normal file paths', () => {
7 expect(sanitizePath('index.html')).toBe('index.html')
8 expect(sanitizePath('css/styles.css')).toBe('css/styles.css')
9 expect(sanitizePath('images/logo.png')).toBe('images/logo.png')
10 expect(sanitizePath('js/app.js')).toBe('js/app.js')
11 })
12
13 test('allows deeply nested paths', () => {
14 expect(sanitizePath('assets/images/icons/favicon.ico')).toBe('assets/images/icons/favicon.ico')
15 expect(sanitizePath('a/b/c/d/e/f.txt')).toBe('a/b/c/d/e/f.txt')
16 })
17
18 test('removes leading slashes', () => {
19 expect(sanitizePath('/index.html')).toBe('index.html')
20 expect(sanitizePath('//index.html')).toBe('index.html')
21 expect(sanitizePath('///index.html')).toBe('index.html')
22 expect(sanitizePath('/css/styles.css')).toBe('css/styles.css')
23 })
24
25 test('blocks parent directory traversal', () => {
26 expect(sanitizePath('../etc/passwd')).toBe('etc/passwd')
27 expect(sanitizePath('../../etc/passwd')).toBe('etc/passwd')
28 expect(sanitizePath('../../../etc/passwd')).toBe('etc/passwd')
29 expect(sanitizePath('css/../../../etc/passwd')).toBe('css/etc/passwd')
30 })
31
32 test('blocks directory traversal in middle of path', () => {
33 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd')
34 // Note: sanitizePath only filters out ".." segments, doesn't resolve paths
35 expect(sanitizePath('a/b/../c')).toBe('a/b/c')
36 expect(sanitizePath('a/../b/../c')).toBe('a/b/c')
37 })
38
39 test('removes current directory references', () => {
40 expect(sanitizePath('./index.html')).toBe('index.html')
41 expect(sanitizePath('././index.html')).toBe('index.html')
42 expect(sanitizePath('css/./styles.css')).toBe('css/styles.css')
43 expect(sanitizePath('./css/./styles.css')).toBe('css/styles.css')
44 })
45
46 test('removes empty path segments', () => {
47 expect(sanitizePath('css//styles.css')).toBe('css/styles.css')
48 expect(sanitizePath('css///styles.css')).toBe('css/styles.css')
49 expect(sanitizePath('a//b//c')).toBe('a/b/c')
50 })
51
52 test('blocks null bytes', () => {
53 // Null bytes cause the entire segment to be filtered out
54 expect(sanitizePath('index.html\0.txt')).toBe('')
55 expect(sanitizePath('test\0')).toBe('')
56 // Null byte in middle segment
57 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css')
58 })
59
60 test('handles mixed attacks', () => {
61 expect(sanitizePath('/../../../etc/passwd')).toBe('etc/passwd')
62 expect(sanitizePath('/./././../etc/passwd')).toBe('etc/passwd')
63 expect(sanitizePath('//../../.\0./etc/passwd')).toBe('etc/passwd')
64 })
65
66 test('handles edge cases', () => {
67 expect(sanitizePath('')).toBe('')
68 expect(sanitizePath('/')).toBe('')
69 expect(sanitizePath('//')).toBe('')
70 expect(sanitizePath('.')).toBe('')
71 expect(sanitizePath('..')).toBe('')
72 expect(sanitizePath('../..')).toBe('')
73 })
74
75 test('preserves valid special characters in filenames', () => {
76 expect(sanitizePath('file-name.html')).toBe('file-name.html')
77 expect(sanitizePath('file_name.html')).toBe('file_name.html')
78 expect(sanitizePath('file.name.html')).toBe('file.name.html')
79 expect(sanitizePath('file (1).html')).toBe('file (1).html')
80 expect(sanitizePath('file@2x.png')).toBe('file@2x.png')
81 })
82
83 test('handles Unicode characters', () => {
84 expect(sanitizePath('文件.html')).toBe('文件.html')
85 expect(sanitizePath('файл.html')).toBe('файл.html')
86 expect(sanitizePath('ファイル.html')).toBe('ファイル.html')
87 })
88})
89
90describe('extractBlobCid', () => {
91 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y'
92
93 test('extracts CID from IPLD link', () => {
94 const blobRef = { $link: TEST_CID }
95 expect(extractBlobCid(blobRef)).toBe(TEST_CID)
96 })
97
98 test('extracts CID from typed BlobRef with CID object', () => {
99 const cid = CID.parse(TEST_CID)
100 const blobRef = { ref: cid }
101 const result = extractBlobCid(blobRef)
102 expect(result).toBe(TEST_CID)
103 })
104
105 test('extracts CID from typed BlobRef with IPLD link', () => {
106 const blobRef = {
107 ref: { $link: TEST_CID }
108 }
109 expect(extractBlobCid(blobRef)).toBe(TEST_CID)
110 })
111
112 test('extracts CID from untyped BlobRef', () => {
113 const blobRef = { cid: TEST_CID }
114 expect(extractBlobCid(blobRef)).toBe(TEST_CID)
115 })
116
117 test('returns null for invalid blob ref', () => {
118 expect(extractBlobCid(null)).toBe(null)
119 expect(extractBlobCid(undefined)).toBe(null)
120 expect(extractBlobCid({})).toBe(null)
121 expect(extractBlobCid('not-an-object')).toBe(null)
122 expect(extractBlobCid(123)).toBe(null)
123 })
124
125 test('returns null for malformed objects', () => {
126 expect(extractBlobCid({ wrongKey: 'value' })).toBe(null)
127 expect(extractBlobCid({ ref: 'not-a-cid' })).toBe(null)
128 expect(extractBlobCid({ ref: {} })).toBe(null)
129 })
130
131 test('handles nested structures from AT Proto API', () => {
132 // Real structure from AT Proto
133 const blobRef = {
134 $type: 'blob',
135 ref: CID.parse(TEST_CID),
136 mimeType: 'text/html',
137 size: 1234
138 }
139 expect(extractBlobCid(blobRef)).toBe(TEST_CID)
140 })
141
142 test('handles BlobRef with additional properties', () => {
143 const blobRef = {
144 ref: { $link: TEST_CID },
145 mimeType: 'image/png',
146 size: 5678,
147 someOtherField: 'value'
148 }
149 expect(extractBlobCid(blobRef)).toBe(TEST_CID)
150 })
151
152 test('prioritizes checking IPLD link first', () => {
153 // Direct $link takes precedence
154 const directLink = { $link: TEST_CID }
155 expect(extractBlobCid(directLink)).toBe(TEST_CID)
156 })
157
158 test('handles CID v0 format', () => {
159 const cidV0 = 'QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx'
160 const blobRef = { $link: cidV0 }
161 expect(extractBlobCid(blobRef)).toBe(cidV0)
162 })
163
164 test('handles CID v1 format', () => {
165 const cidV1 = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi'
166 const blobRef = { $link: cidV1 }
167 expect(extractBlobCid(blobRef)).toBe(cidV1)
168 })
169})