more tests
Massimo Melina committed
Jan 22, 2026 at 01:09 UTC
b225612e587f8b013b3dfcf5862dbcfc325a31b4
2 files changed
+48
-31
tests/config.yaml
+2
@@ -64,8 +64,10 @@ vfs:
64
"**/!*.png|files|":
65
can_see: false
66
- name: renameChild
67
+ can_upload: true
68
children:
69
- source: ..
70
+ can_upload: false
71
rename:
72
alfa.txt: renamed1
73
page/gpl.png: renamed2
tests/test.ts
+46
-31
@@ -26,6 +26,7 @@ const ROOT = 'tests/'
26
const BASE_URL = 'http://[::1]:81'
27
const BASE_URL_127 = 'http://127.0.0.1:81'
28
const UPLOAD_ROOT = '/for-admins/upload/'
29
+const VIRTUAL_UPLOAD_ROOT = '/renameChild/'
30
const UPLOAD_DIR = 'temp'
31
const CANT_OVERWRITE_NAME = 'cant-overwrite'
32
const CANT_OVERWRITE_URI = `/for-admins/${CANT_OVERWRITE_NAME}/`
@@ -35,6 +36,7 @@ const BIG_CONTENT = _.repeat(randomId(10), 300_000) // 3MB, big enough to satura
36
const throttle = BIG_CONTENT.length /1000 /0.8 // KB, finish in 0.8s, quick but still overlapping downloads
37
const SAMPLE_FILE_PATH = resolve(__dirname, 'page/gpl.png')
38
let defaultBaseUrl = BASE_URL
39
+
40
const execP = (cmd: string) => promisify(exec)(cmd).then(x => x.stdout)
41
42
describe('basics', () => {
@@ -157,42 +159,45 @@ describe('basics', () => {
159
}))
160
161
test('upload.need account', reqUpload( UPLOAD_DEST, 401))
160
- test('upload.post', () => // this is also testing basic-auth
161
- execP(`curl -u ${auth} -F upload=@${SAMPLE_FILE_PATH} ${BASE_URL}${UPLOAD_ROOT}`).then(x => {
162
- const uri = tryJson(x)?.uris?.[0]
163
- if (!uri) throw "unexpected output " + x
164
- const fn = resolve(__dirname, basename(decodeURI(uri)))
165
- const stats = statSync(fn)
166
- rm(fn).catch(() => {}) // clear
167
- if (stats?.size !== statSync(SAMPLE_FILE_PATH).size)
168
- throw "unexpected size for " + fn
169
- }))
170
- test('upload.post.empty filename', () => {
162
+ test('upload.post', async () => { // this is also testing basic-auth
163
+ const output = await execP(`curl -u ${auth} -F upload=@${SAMPLE_FILE_PATH} ${BASE_URL}${UPLOAD_ROOT}`)
164
+ const uri = tryJson(output)?.uris?.[0]
165
+ if (!uri) throw "unexpected output " + output
166
+ const fn = resolve(__dirname, basename(decodeURI(uri)))
167
+ const stats = statSync(fn)
168
+ rm(fn).catch(() => {}) // clear
169
+ if (stats?.size !== statSync(SAMPLE_FILE_PATH).size)
170
+ throw "unexpected size for " + fn
171
+ })
172
+ test('upload.post.virtual folder', async () => {
173
+ const { status } = await curlWithStatus(`curl -s -u ${auth} -F upload=@${SAMPLE_FILE_PATH} ${BASE_URL}${VIRTUAL_UPLOAD_ROOT}`)
174
+ if (status !== 403)
175
+ throw "unexpected status " + status
176
+ })
177
+ test('upload.put.virtual folder', reqUpload(`${VIRTUAL_UPLOAD_ROOT}gpl.png`, 403))
178
+ test('upload.post.empty filename', async () => {
179
const boundary = '----hfs-boundary'
180
const body = `--${boundary}\\r\\nContent-Disposition: form-data; name="upload"; filename=""\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nX\\r\\n--${boundary}--\\r\\n`
173
- return execP(`printf '%b' "${body}" | curl -s -u ${auth} -H "Content-Type: multipart/form-data; boundary=${boundary}" --data-binary @- ${BASE_URL}${UPLOAD_ROOT} -w "\\nSTATUS:%{http_code}"`).then(x => {
174
- const out = x.trimEnd()
175
- const idx = out.lastIndexOf('\nSTATUS:')
176
- const status = out.slice(idx + 8)
177
- if (status !== '400')
178
- throw "unexpected status " + status
179
- const errMsg = tryJson(out.slice(0, idx))?.errors?.[0]
180
- if (!['empty filename', 'no files'].includes(errMsg))
181
- throw 'missing error'
182
- })
181
+ const { status, body: responseBody } = await curlWithStatus(`printf '%b' "${body}" | curl -s -u ${auth} -H "Content-Type: multipart/form-data; boundary=${boundary}" --data-binary @- ${BASE_URL}${UPLOAD_ROOT}`)
182
+ if (status !== 400)
183
+ throw "unexpected status " + status
184
+ const errMsg = tryJson(responseBody)?.errors?.[0]
185
+ if (!['empty filename', 'no files'].includes(errMsg))
186
+ throw 'missing error'
187
})
184
- test('upload.post.absolute filename', () => {
188
+ test('upload.post.absolute filename', async () => {
189
const absPath = resolve(__dirname, `abs-${randomId(6)}.txt`)
190
const absForBody = absPath.replace(/\\\\/g, '/')
191
const storedPath = resolve(__dirname, 'tmp', basename(absPath))
188
- return execP(`curl -s -u ${auth} -H "x-hfs-wait: 1" -F "upload=@${SAMPLE_FILE_PATH};filename=${absForBody}" ${BASE_URL}${UPLOAD_ROOT} -w "\\nSTATUS:%{http_code}"`).then(async x => {
189
- const out = x.trimEnd()
190
- const idx = out.lastIndexOf('\nSTATUS:')
191
- const status = out.slice(idx + 8)
192
- throwIf(status !== '418' ? "unexpected status " + status
192
+ try {
193
+ const { status } = await curlWithStatus(`curl -s -u ${auth} -H "x-hfs-wait: 1" -F "upload=@${SAMPLE_FILE_PATH};filename=${absForBody}" ${BASE_URL}${UPLOAD_ROOT}`)
194
+ throwIf(status !== 418 ? "unexpected status " + status
195
: existsSync(absPath) ? "absolute path accepted"
196
: existsSync(storedPath) ? "stored file escaped" : '')
195
- }).finally(() => Promise.all([rmAny(absPath), rmAny(storedPath)]))
197
+ }
198
+ finally {
199
+ await Promise.all([rmAny(absPath), rmAny(storedPath)])
200
+ }
201
})
202
test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
203
test('create_folder.bad type', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 123 }, { status: 400, re: /name/ }))
@@ -211,10 +216,11 @@ describe('basics', () => {
216
test('folder size.cant', reqApi('get_folder_size', { uri: 'for-admins' }, 401))
217
218
test('get_accounts', reqApi('get_accounts', {}, 401)) // admin api requires login
214
- test('url login', () => execP(`curl -s -D - -o /dev/null "${BASE_URL}/for-admins/?login=${auth}"`).then(x => {
215
- if (!/^(location|set-cookie):/im.test(x))
219
+ test('url login', async () => {
220
+ const output = await execP(`curl -s -D - -o /dev/null "${BASE_URL}/for-admins/?login=${auth}"`)
221
+ if (!/^(location|set-cookie):/im.test(output))
222
throw "failed"
217
- }))
223
+ })
224
})
225
226
// do this before login, or max_dl_accounts config will override max_dl
@@ -237,6 +243,7 @@ describe('accounts', () => {
243
describe('after-login', () => {
244
before(() => login(username))
245
test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 200))
246
+ test('create_folder.empty name', reqApi('create_folder', { uri: UPLOAD_ROOT, name: '' }, 409))
247
test('inherit.perm', reqList('/for-admins/', { inList:['alfa.txt'] }))
248
test('inherit.disabled', reqList('/for-disabled/', 401))
249
test('rename.to existing folder', async () => {
@@ -607,3 +614,11 @@ function makeReadableThatTakes(ms: number) {
614
return Object.assign(Readable.from(BIG_CONTENT).pipe(new ThrottledStream(new ThrottleGroup(BIG_CONTENT.length / ms))),
615
{ length: BIG_CONTENT.length })
616
}
617
+
618
+async function curlWithStatus(cmd: string) {
619
+ const out = (await execP(`${cmd} -w "\\nSTATUS:%{http_code}"`)).trimEnd()
620
+ const idx = out.lastIndexOf('\nSTATUS:')
621
+ if (idx < 0)
622
+ throw "missing status in curl output"
623
+ return { status: Number(out.slice(idx + 8)), body: out.slice(0, idx) }
624
+}