@samitouri / QOSami-HFS / commits / b225612e

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 +}