feat(contract): 接入移动端合同打印与小程序文档交付以支持导出分享。
This commit is contained in:
1 parent
0cd120ef5e
commit
13630045df
16 files changed
+3666
-4
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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/)
|
||||
})
|
||||
Reference in new issue
Block a user