import { describe, expect, it } from 'bun:test'
import { MAX_PRIVATE_SITE_FILE_COUNT } from '@wispplace/constants'
import { readPrivateSiteUpload } from './private-site-upload'
const requestFor = (form: FormData) =>
new Request('http://localhost/api/user/private-sites', {
method: 'POST',
body: form,
})
describe('readPrivateSiteUpload', () => {
it('preserves names, relative paths, and omitted expiry', async () => {
const form = new FormData()
form.append('name', ' my private site ')
form.append('files', new File(['hello'], 'index.html', { type: 'text/html' }), 'nested/index.html')
const upload = await readPrivateSiteUpload(requestFor(form))
expect(upload.name).toBe('my private site')
expect(upload.expiryMinutes).toBeUndefined()
expect(upload.files).toHaveLength(1)
expect(upload.files[0]?.path).toBe('nested/index.html')
expect(new TextDecoder().decode(upload.files[0]?.bytes)).toBe('hello')
expect(upload.files[0]?.mimeType).toStartWith('text/html')
})
it('preserves explicit zero expiry', async () => {
const form = new FormData()
form.append('name', 'forever')
form.append('expiryMinutes', '0')
form.append('files', new File(['hello'], 'index.html'))
const upload = await readPrivateSiteUpload(requestFor(form))
expect(upload.expiryMinutes).toBe(0)
})
it('strips the directory-picker root for editor uploads', async () => {
const form = new FormData()
form.append('name', 'folder upload')
form.append('files', new File(['home'], 'index.html'), 'dist/index.html')
form.append('files', new File(['css'], 'app.css'), 'dist/assets/app.css')
const upload = await readPrivateSiteUpload(requestFor(form), { stripSharedRoot: true })
expect(upload.files.map((file) => file.path)).toEqual(['index.html', 'assets/app.css'])
})
it('does not strip mixed roots or service-authenticated relative paths', async () => {
const form = new FormData()
form.append('files', new File(['home'], 'index.html'), 'index.html')
form.append('files', new File(['css'], 'app.css'), 'assets/app.css')
const upload = await readPrivateSiteUpload(requestFor(form), { stripSharedRoot: true })
expect(upload.files.map((file) => file.path)).toEqual(['index.html', 'assets/app.css'])
})
it('rejects a non-numeric expiry', async () => {
const form = new FormData()
form.append('expiryMinutes', 'later')
await expect(readPrivateSiteUpload(requestFor(form))).rejects.toEqual(
expect.objectContaining({
message: 'expiryMinutes must be a number',
status: 400,
}),
)
})
it('enforces the private file-count limit', async () => {
const form = new FormData()
for (let index = 0; index <= MAX_PRIVATE_SITE_FILE_COUNT; index++) {
form.append('files', new File([], `${index}.txt`))
}
await expect(readPrivateSiteUpload(requestFor(form))).rejects.toEqual(
expect.objectContaining({
message: `at most ${MAX_PRIVATE_SITE_FILE_COUNT} files are allowed`,
status: 413,
}),
)
})
it('rejects unsafe file paths instead of normalizing them into collisions', async () => {
for (const path of ['nested/../index.html', '..\\index.html', 'nested//index.html', 'C:/Windows/index.html']) {
const form = new FormData()
form.append('files', new File(['hello'], 'index.html'), path)
await expect(readPrivateSiteUpload(requestFor(form))).rejects.toEqual(
expect.objectContaining({ message: 'invalid file path', status: 400 }),
)
}
})
})