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 { processUploadedFiles, type UploadedFile } from './tree'
3
4describe('processUploadedFiles', () => {
5 test('should preserve nested directory structure', () => {
6 const files: UploadedFile[] = [
7 {
8 name: 'mysite/index.html',
9 content: Buffer.from('<html>'),
10 mimeType: 'text/html',
11 size: 6
12 },
13 {
14 name: 'mysite/_astro/main.js',
15 content: Buffer.from('console.log()'),
16 mimeType: 'application/javascript',
17 size: 13
18 },
19 {
20 name: 'mysite/_astro/styles.css',
21 content: Buffer.from('body {}'),
22 mimeType: 'text/css',
23 size: 7
24 },
25 {
26 name: 'mysite/images/logo.png',
27 content: Buffer.from([0x89, 0x50, 0x4e, 0x47]),
28 mimeType: 'image/png',
29 size: 4
30 }
31 ]
32
33 const result = processUploadedFiles(files)
34
35 expect(result.fileCount).toBe(4)
36 expect(result.directory.entries).toHaveLength(3) // index.html, _astro/, images/
37
38 // Check _astro directory exists
39 const astroEntry = result.directory.entries.find(e => e.name === '_astro')
40 expect(astroEntry).toBeTruthy()
41 expect('type' in astroEntry!.node && astroEntry!.node.type).toBe('directory')
42
43 if ('entries' in astroEntry!.node) {
44 const astroDir = astroEntry!.node
45 expect(astroDir.entries).toHaveLength(2) // main.js, styles.css
46 expect(astroDir.entries.find(e => e.name === 'main.js')).toBeTruthy()
47 expect(astroDir.entries.find(e => e.name === 'styles.css')).toBeTruthy()
48 }
49
50 // Check images directory exists
51 const imagesEntry = result.directory.entries.find(e => e.name === 'images')
52 expect(imagesEntry).toBeTruthy()
53 expect('type' in imagesEntry!.node && imagesEntry!.node.type).toBe('directory')
54
55 if ('entries' in imagesEntry!.node) {
56 const imagesDir = imagesEntry!.node
57 expect(imagesDir.entries).toHaveLength(1) // logo.png
58 expect(imagesDir.entries.find(e => e.name === 'logo.png')).toBeTruthy()
59 }
60 })
61
62 test('should handle deeply nested directories', () => {
63 const files: UploadedFile[] = [
64 {
65 name: 'site/a/b/c/d/deep.txt',
66 content: Buffer.from('deep'),
67 mimeType: 'text/plain',
68 size: 4
69 }
70 ]
71
72 const result = processUploadedFiles(files)
73
74 expect(result.fileCount).toBe(1)
75
76 // Navigate through nested structure
77 const aEntry = result.directory.entries.find(e => e.name === 'a')
78 expect(aEntry).toBeTruthy()
79 expect('type' in aEntry!.node && aEntry!.node.type).toBe('directory')
80
81 if ('entries' in aEntry!.node) {
82 const bEntry = aEntry!.node.entries.find(e => e.name === 'b')
83 expect(bEntry).toBeTruthy()
84 expect('type' in bEntry!.node && bEntry!.node.type).toBe('directory')
85
86 if ('entries' in bEntry!.node) {
87 const cEntry = bEntry!.node.entries.find(e => e.name === 'c')
88 expect(cEntry).toBeTruthy()
89 expect('type' in cEntry!.node && cEntry!.node.type).toBe('directory')
90
91 if ('entries' in cEntry!.node) {
92 const dEntry = cEntry!.node.entries.find(e => e.name === 'd')
93 expect(dEntry).toBeTruthy()
94 expect('type' in dEntry!.node && dEntry!.node.type).toBe('directory')
95
96 if ('entries' in dEntry!.node) {
97 const fileEntry = dEntry!.node.entries.find(e => e.name === 'deep.txt')
98 expect(fileEntry).toBeTruthy()
99 expect('type' in fileEntry!.node && fileEntry!.node.type).toBe('file')
100 }
101 }
102 }
103 }
104 })
105
106 test('should handle files at root level', () => {
107 const files: UploadedFile[] = [
108 {
109 name: 'mysite/index.html',
110 content: Buffer.from('<html>'),
111 mimeType: 'text/html',
112 size: 6
113 },
114 {
115 name: 'mysite/robots.txt',
116 content: Buffer.from('User-agent: *'),
117 mimeType: 'text/plain',
118 size: 13
119 }
120 ]
121
122 const result = processUploadedFiles(files)
123
124 expect(result.fileCount).toBe(2)
125 expect(result.directory.entries).toHaveLength(2)
126 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
127 expect(result.directory.entries.find(e => e.name === 'robots.txt')).toBeTruthy()
128 })
129
130 test('should skip .git directories', () => {
131 const files: UploadedFile[] = [
132 {
133 name: 'mysite/index.html',
134 content: Buffer.from('<html>'),
135 mimeType: 'text/html',
136 size: 6
137 },
138 {
139 name: 'mysite/.git/config',
140 content: Buffer.from('[core]'),
141 mimeType: 'text/plain',
142 size: 6
143 },
144 {
145 name: 'mysite/.gitignore',
146 content: Buffer.from('node_modules'),
147 mimeType: 'text/plain',
148 size: 12
149 }
150 ]
151
152 const result = processUploadedFiles(files)
153
154 expect(result.fileCount).toBe(2) // Only index.html and .gitignore
155 expect(result.directory.entries).toHaveLength(2)
156 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
157 expect(result.directory.entries.find(e => e.name === '.gitignore')).toBeTruthy()
158 expect(result.directory.entries.find(e => e.name === '.git')).toBeFalsy()
159 })
160
161 test('should handle mixed root and nested files', () => {
162 const files: UploadedFile[] = [
163 {
164 name: 'mysite/index.html',
165 content: Buffer.from('<html>'),
166 mimeType: 'text/html',
167 size: 6
168 },
169 {
170 name: 'mysite/about/index.html',
171 content: Buffer.from('<html>'),
172 mimeType: 'text/html',
173 size: 6
174 },
175 {
176 name: 'mysite/about/team.html',
177 content: Buffer.from('<html>'),
178 mimeType: 'text/html',
179 size: 6
180 },
181 {
182 name: 'mysite/robots.txt',
183 content: Buffer.from('User-agent: *'),
184 mimeType: 'text/plain',
185 size: 13
186 }
187 ]
188
189 const result = processUploadedFiles(files)
190
191 expect(result.fileCount).toBe(4)
192 expect(result.directory.entries).toHaveLength(3) // index.html, about/, robots.txt
193
194 const aboutEntry = result.directory.entries.find(e => e.name === 'about')
195 expect(aboutEntry).toBeTruthy()
196 expect('type' in aboutEntry!.node && aboutEntry!.node.type).toBe('directory')
197
198 if ('entries' in aboutEntry!.node) {
199 const aboutDir = aboutEntry!.node
200 expect(aboutDir.entries).toHaveLength(2) // index.html, team.html
201 expect(aboutDir.entries.find(e => e.name === 'index.html')).toBeTruthy()
202 expect(aboutDir.entries.find(e => e.name === 'team.html')).toBeTruthy()
203 }
204 })
205
206 test('should handle empty file array', () => {
207 const files: UploadedFile[] = []
208
209 const result = processUploadedFiles(files)
210
211 expect(result.fileCount).toBe(0)
212 expect(result.directory.entries).toHaveLength(0)
213 })
214
215 test('should strip base folder name from paths', () => {
216 // This tests the behavior where file.name includes the base folder
217 // e.g., "mysite/index.html" should become "index.html" at root
218 const files: UploadedFile[] = [
219 {
220 name: 'build-output/index.html',
221 content: Buffer.from('<html>'),
222 mimeType: 'text/html',
223 size: 6
224 },
225 {
226 name: 'build-output/assets/main.js',
227 content: Buffer.from('console.log()'),
228 mimeType: 'application/javascript',
229 size: 13
230 }
231 ]
232
233 const result = processUploadedFiles(files)
234
235 expect(result.fileCount).toBe(2)
236
237 // Should have index.html at root and assets/ directory
238 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
239 expect(result.directory.entries.find(e => e.name === 'assets')).toBeTruthy()
240
241 // Should NOT have 'build-output' directory
242 expect(result.directory.entries.find(e => e.name === 'build-output')).toBeFalsy()
243 })
244
245 test('should preserve full paths with skipNormalization option (CLI use case)', () => {
246 // CLI passes paths already relative to site directory, without a folder prefix
247 const files: UploadedFile[] = [
248 {
249 name: 'index.html',
250 content: Buffer.from('<html>'),
251 mimeType: 'text/html',
252 size: 6
253 },
254 {
255 name: 'assets/readme.txt',
256 content: Buffer.from('readme'),
257 mimeType: 'text/plain',
258 size: 6
259 },
260 {
261 name: 'assets/images/logo.txt',
262 content: Buffer.from('logo'),
263 mimeType: 'text/plain',
264 size: 4
265 },
266 {
267 name: 'assets/scripts/main.js',
268 content: Buffer.from('console.log()'),
269 mimeType: 'application/javascript',
270 size: 13
271 }
272 ]
273
274 const result = processUploadedFiles(files, { skipNormalization: true })
275
276 expect(result.fileCount).toBe(4)
277
278 // index.html at root
279 expect(result.directory.entries.find(e => e.name === 'index.html')).toBeTruthy()
280
281 // assets directory should exist (NOT be stripped)
282 const assetsEntry = result.directory.entries.find(e => e.name === 'assets')
283 expect(assetsEntry).toBeTruthy()
284 expect('type' in assetsEntry!.node && assetsEntry!.node.type).toBe('directory')
285
286 if ('entries' in assetsEntry!.node) {
287 const assetsDir = assetsEntry!.node
288 // Should have readme.txt, images/, scripts/
289 expect(assetsDir.entries.find(e => e.name === 'readme.txt')).toBeTruthy()
290 expect(assetsDir.entries.find(e => e.name === 'images')).toBeTruthy()
291 expect(assetsDir.entries.find(e => e.name === 'scripts')).toBeTruthy()
292
293 // Check nested directories have files
294 const imagesEntry = assetsDir.entries.find(e => e.name === 'images')
295 if (imagesEntry && 'entries' in imagesEntry.node) {
296 expect(imagesEntry.node.entries.find(e => e.name === 'logo.txt')).toBeTruthy()
297 }
298
299 const scriptsEntry = assetsDir.entries.find(e => e.name === 'scripts')
300 if (scriptsEntry && 'entries' in scriptsEntry.node) {
301 expect(scriptsEntry.node.entries.find(e => e.name === 'main.js')).toBeTruthy()
302 }
303 }
304 })
305})