99 lines
4.8 KiB
JavaScript
99 lines
4.8 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { existsSync } from 'node:fs'
|
|
const file = new URL('../utils/contract-document.js', import.meta.url)
|
|
test('document links only accept configured storage, PDF/DOCX and safe boundaries', async () => {
|
|
assert.ok(existsSync(file), 'document validator exists')
|
|
const { validateContractDocument } = await import(file)
|
|
const base = 'https://res.example.com/ruoyi'
|
|
assert.equal(validateContractDocument({url: base+'/a.pdf?signature=x', type:'pdf'}, base).type, 'pdf')
|
|
for (const url of ['https://evil.com/a.pdf', base+'evil/a.pdf', 'https://res.example.com/ruoyi/../secret', 'javascript:alert(1)', 'https://res.example.com@evil.com/ruoyi/a.pdf']) {
|
|
assert.throws(() => validateContractDocument({url,type:'pdf'},base))
|
|
}
|
|
assert.throws(() => validateContractDocument({url:base+'/a.exe',type:'exe'},base))
|
|
})
|
|
|
|
import vm from 'node:vm'
|
|
import { readFileSync } from 'node:fs'
|
|
async function pageHarness() {
|
|
const { validateContractDocument, contractDownloadErrorMessage } = await import(file)
|
|
const source = readFileSync(new URL('../pages/common/contract-document.vue', import.meta.url),'utf8')
|
|
.split('<script setup>')[1].split('</script>')[0].replace(/^import .*$/gm,'')
|
|
const callbacks = {}
|
|
const calls = {downloads: [], opens: [], removed: [], aborted:0}
|
|
const context = vm.createContext({
|
|
ref: value => ({value}),
|
|
onLoad: fn => callbacks.load = fn,
|
|
onUnload: fn => callbacks.unload = fn,
|
|
MINIO_BASE_URL: 'https://res.example.com/ruoyi',
|
|
validateContractDocument,
|
|
contractDownloadErrorMessage,
|
|
uni: {
|
|
downloadFile: options => {calls.downloads.push(options); return {abort() {calls.aborted++}}},
|
|
openDocument: options => calls.opens.push(options),
|
|
getFileSystemManager: () => ({unlink: options => calls.removed.push(options.filePath)}),
|
|
navigateBack() {}
|
|
}
|
|
})
|
|
vm.runInContext(source+'\nglobalThis.state = { busy, message, fileName, documentInfo, openDocument };',context)
|
|
return {callbacks,calls,state: context.state}
|
|
}
|
|
const tick = () => new Promise(resolve=>setImmediate(resolve))
|
|
test('download, preview/share, retry and cleanup use local files without credentials', async () => {
|
|
const h = await pageHarness()
|
|
h.callbacks.load({url:'https://res.example.com/ruoyi/a.pdf',type:'pdf',name:'100%合同.pdf'})
|
|
assert.equal(h.state.fileName.value,'100%合同.pdf')
|
|
assert.equal(h.calls.downloads[0].header,undefined)
|
|
h.calls.downloads[0].fail()
|
|
await tick()
|
|
assert.equal(h.state.busy.value,false)
|
|
void h.state.openDocument()
|
|
h.calls.downloads[1].success({statusCode:200,tempFilePath:'/tmp/contract.pdf'})
|
|
await tick()
|
|
assert.equal(h.calls.opens[0].filePath,'/tmp/contract.pdf')
|
|
assert.equal(h.calls.opens[0].showMenu,true)
|
|
h.calls.opens[0].success()
|
|
await tick()
|
|
h.callbacks.unload()
|
|
assert.deepEqual(h.calls.removed,['/tmp/contract.pdf'])
|
|
})
|
|
test('unload during download prevents opening and removes late temp files', async () => {
|
|
const h=await pageHarness()
|
|
h.callbacks.load({url:'https://res.example.com/ruoyi/a.pdf',type:'pdf'})
|
|
h.callbacks.unload()
|
|
h.calls.downloads[0].success({statusCode:200,tempFilePath:'/tmp/late.pdf'})
|
|
await tick()
|
|
assert.equal(h.calls.opens.length,0)
|
|
assert.deepEqual(h.calls.removed,['/tmp/late.pdf'])
|
|
})
|
|
test('invalid links never start download', async () => {
|
|
const h=await pageHarness()
|
|
h.callbacks.load({url:'https://evil.com/a.pdf',type:'pdf'})
|
|
assert.equal(h.calls.downloads.length,0)
|
|
assert.equal(h.state.documentInfo.value,null)
|
|
})
|
|
test('registering contract documents preserves the login launch page', () => {
|
|
const pages = JSON.parse(readFileSync(new URL('../pages.json',import.meta.url),'utf8').replace(/^\s*\/\/.*$/gm, '')).pages
|
|
assert.equal(pages[0].path,'pages/login/login')
|
|
assert.equal(pages.filter(p => p.path === 'pages/common/contract-document').length,1)
|
|
})
|
|
|
|
test('native domain rejection identifies the required downloadFile domain', async () => {
|
|
const h = await pageHarness()
|
|
h.callbacks.load({url:'https://res.example.com/ruoyi/a.pdf',type:'pdf'})
|
|
h.calls.downloads[0].fail({errMsg:'downloadFile:fail url not in domain list',errno:600002})
|
|
await tick()
|
|
assert.match(h.state.message.value, /downloadFile 合法域名/)
|
|
assert.match(h.state.message.value, /https:\/\/res\.example\.com/)
|
|
assert.equal(h.calls.opens.length, 0)
|
|
})
|
|
test('native download failures retain diagnostic details without signed URLs', async () => {
|
|
const h = await pageHarness()
|
|
h.callbacks.load({url:'https://res.example.com/ruoyi/a.pdf',type:'pdf'})
|
|
h.calls.downloads[0].fail({errMsg:'downloadFile:fail connection reset https://res.example.com/a.pdf?X-Amz-Signature=secret',errno:600001})
|
|
await tick()
|
|
assert.match(h.state.message.value, /connection reset/)
|
|
assert.match(h.state.message.value, /600001/)
|
|
assert.doesNotMatch(h.state.message.value, /secret|X-Amz/)
|
|
})
|