Selaa lähdekoodia

fix: validate team background downloads

Developer 3 päivää sitten
vanhempi
commit
a4a20d9705
3 muutettua tiedostoa jossa 142 lisäystä ja 43 poistoa
  1. 29 2
      http/teamBackgroundFile.js
  2. 1 1
      package.json
  3. 112 40
      tests/teamBackgroundFile.test.js

+ 29 - 2
http/teamBackgroundFile.js

@@ -24,6 +24,25 @@ function unwrap(response) {
 	return result.data
 }
 
+function getHeader(headers, name) {
+	if (!headers || typeof headers !== 'object') return ''
+	const expectedName = name.toLowerCase()
+	for (const key of Object.keys(headers)) {
+		if (key.toLowerCase() === expectedName) return String(headers[key] || '')
+	}
+	return ''
+}
+
+function isDocumentDownload(headers) {
+	const contentType = getHeader(headers, 'content-type').split(';')[0].trim().toLowerCase()
+	const disposition = getHeader(headers, 'content-disposition').trim()
+	if (!contentType || !/^attachment(?:\s*;|$)/i.test(disposition)) return false
+	if (contentType === 'application/json' || contentType === 'text/json'
+		|| contentType.endsWith('+json') || contentType === 'text/html'
+		|| contentType === 'application/xhtml+xml') return false
+	return true
+}
+
 export function uploadTeamBackgroundFile(teamId, filePath, onProgress = () => {}) {
 	return new Promise((resolve, reject) => {
 		const task = uni.uploadFile({
@@ -58,11 +77,14 @@ export const createTeamBackgroundSession = teamId => api
 
 export function downloadTeamBackgroundFile(teamId, fileId) {
 	return new Promise((resolve, reject) => {
-		uni.downloadFile({
+		let responseHeaders
+		const task = uni.downloadFile({
 			url: `${BaseApi}/core/user/team/${teamId}/background-files/${fileId}/download`,
 			header: { token: uni.getStorageSync('token') },
 			success: result => {
-				if (result.statusCode !== 200) {
+				const resultHeaders = result && (result.header || result.headers)
+				if (!result || result.statusCode !== 200
+					|| !isDocumentDownload(responseHeaders || resultHeaders)) {
 					reject(new Error('下载失败,请重试'))
 					return
 				}
@@ -75,5 +97,10 @@ export function downloadTeamBackgroundFile(teamId, fileId) {
 			},
 			fail: reject
 		})
+		if (task && typeof task.onHeadersReceived === 'function') {
+			task.onHeadersReceived(result => {
+				responseHeaders = result && (result.header || result.headers)
+			})
+		}
 	})
 }

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "scripts": {
-    "test:team-background": "node tests/teamBackgroundFile.test.js"
+    "test:team-background": "node --disable-warning=ExperimentalWarning --experimental-vm-modules tests/teamBackgroundFile.test.js"
   },
   "dependencies": {
     "qrcode": "^1.5.4",

+ 112 - 40
tests/teamBackgroundFile.test.js

@@ -1,6 +1,7 @@
 const assert = require('node:assert/strict')
 const fs = require('node:fs')
 const path = require('node:path')
+const { pathToFileURL } = require('node:url')
 const vm = require('node:vm')
 const rules = require('../utils/teamBackgroundFile')
 
@@ -10,38 +11,37 @@ assert.match(rules.validateEnterpriseWebsite('ftp://example.com'), /http/)
 assert.equal(rules.validateBackgroundFile({ name: 'brief.PDF', size: 1 }), '')
 assert.match(rules.validateBackgroundFile({ name: 'brief.zip', size: 1 }), /类型/)
 assert.match(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 + 1 }), /50MB/)
+assert.equal(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 }), '')
+assert.equal(rules.validateBackgroundFile({ name: `${'a'.repeat(251)}.pdf`, size: 1 }), '')
+assert.match(rules.validateBackgroundFile({ name: `${'a'.repeat(252)}.pdf`, size: 1 }), /255/)
+assert.match(rules.validateBackgroundFile({ name: 'empty.pdf', size: 0 }), /非空/)
 
 console.log('team background rules: PASS')
 
 const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js')
 
-function loadTransport({ api = {}, uni = {}, wx = {} } = {}) {
+async function loadTransport({ api = {}, uni = {}, wx = {} } = {}) {
 	const source = fs.readFileSync(transportPath, 'utf8')
-		.replace(/import\s+\{\s*BaseApi\s*\}\s+from\s+['"]\.\/baseApi\.js['"]\s*/, '')
-		.replace(/import\s+api\s+from\s+['"]\.\/index\.js['"]\s*/, '')
-		.replace(/export\s+function\s+/g, 'function ')
-		.replace(/export\s+const\s+/g, 'const ')
-		.concat(`
-			module.exports = {
-				uploadTeamBackgroundFile,
-				listTeamBackgroundFiles,
-				disableTeamBackgroundFile,
-				createTeamBackgroundSession,
-				downloadTeamBackgroundFile
-			}
-		`)
-	const module = { exports: {} }
-
-	vm.runInNewContext(source, {
-		module,
-		exports: module.exports,
-		BaseApi: 'https://api.example.test/app',
-		api,
-		uni,
-		wx
-	}, { filename: transportPath })
-
-	return module.exports
+	const context = vm.createContext({ uni, wx })
+	const baseApiModule = new vm.SyntheticModule(['BaseApi'], function () {
+		this.setExport('BaseApi', 'https://api.example.test/app')
+	}, { context, identifier: 'test:baseApi' })
+	const apiModule = new vm.SyntheticModule(['default'], function () {
+		this.setExport('default', api)
+	}, { context, identifier: 'test:api' })
+	const transportModule = new vm.SourceTextModule(source, {
+		context,
+		identifier: pathToFileURL(transportPath).href
+	})
+
+	await transportModule.link(specifier => {
+		if (specifier === './baseApi.js') return baseApiModule
+		if (specifier === './index.js') return apiModule
+		throw new Error(`unexpected transport import: ${specifier}`)
+	})
+	await transportModule.evaluate()
+
+	return transportModule.namespace
 }
 
 function assertNoIdentityFields(value) {
@@ -52,7 +52,7 @@ function assertNoIdentityFields(value) {
 	}
 }
 
-function createUploadHarness(response, progress = 37) {
+async function createUploadHarness(response, progress = 37) {
 	let request
 	const progressValues = []
 	const uni = {
@@ -70,7 +70,7 @@ function createUploadHarness(response, progress = 37) {
 			}
 		}
 	}
-	const transport = loadTransport({ uni })
+	const transport = await loadTransport({ uni })
 
 	return {
 		request: () => request,
@@ -80,7 +80,7 @@ function createUploadHarness(response, progress = 37) {
 }
 
 async function testUploadTransport() {
-	const stringHarness = createUploadHarness({
+	const stringHarness = await createUploadHarness({
 		statusCode: 200,
 		data: JSON.stringify({ code: 0, data: { id: 7 } })
 	})
@@ -92,14 +92,17 @@ async function testUploadTransport() {
 	assert.deepEqual(stringHarness.progressValues, [37])
 	assertNoIdentityFields(stringHarness.request())
 
-	const objectHarness = createUploadHarness({ statusCode: 200, data: { code: 0, data: { id: 8 } } })
+	const objectHarness = await createUploadHarness({ statusCode: 200, data: { code: 0, data: { id: 8 } } })
 	assert.equal((await objectHarness.promise).id, 8)
 
-	const statusHarness = createUploadHarness({ statusCode: 500, data: { code: 0 } })
+	const statusHarness = await createUploadHarness({ statusCode: 500, data: { code: 0 } })
 	await assert.rejects(statusHarness.promise, /上传失败,请重试/)
 
-	const codeHarness = createUploadHarness({ statusCode: 200, data: { code: 9, msg: '上传业务失败' } })
+	const codeHarness = await createUploadHarness({ statusCode: 200, data: { code: 9, msg: '上传业务失败' } })
 	await assert.rejects(codeHarness.promise, /上传业务失败/)
+
+	const invalidJsonHarness = await createUploadHarness({ statusCode: 200, data: '<invalid-json>' })
+	await assert.rejects(invalidJsonHarness.promise, /上传失败,请重试/)
 }
 
 async function testApiTransport() {
@@ -118,7 +121,7 @@ async function testApiTransport() {
 			return Promise.resolve({ data: { code: 0, data: { sessionId: 'session-1' } } })
 		}
 	}
-	const transport = loadTransport({ api })
+	const transport = await loadTransport({ api })
 
 	assert.deepEqual(await transport.listTeamBackgroundFiles(12), [{ id: 1 }])
 	assert.equal(await transport.disableTeamBackgroundFile(12, 34), true)
@@ -136,7 +139,7 @@ async function testApiTransport() {
 		assertNoIdentityFields(call)
 	}
 
-	const failingTransport = loadTransport({
+	const failingTransport = await loadTransport({
 		api: {
 			get: () => Promise.resolve({ data: { code: 3, msg: '列表业务失败' } })
 		}
@@ -150,7 +153,8 @@ async function testApiTransport() {
 	assert.equal(transport.downloadTeamBackgroundFile.length, 2)
 }
 
-function createDownloadHarness({ result, downloadError, openError } = {}) {
+async function createDownloadHarness({ result, responseHeaders, downloadError, openError,
+	withHeadersListener = true } = {}) {
 	let request
 	let openRequest
 	const uni = {
@@ -164,6 +168,12 @@ function createDownloadHarness({ result, downloadError, openError } = {}) {
 				if (downloadError) options.fail(downloadError)
 				else options.success(result)
 			})
+			if (!withHeadersListener) return {}
+			return {
+				onHeadersReceived(callback) {
+					if (responseHeaders) callback({ header: responseHeaders })
+				}
+			}
 		}
 	}
 	const wx = {
@@ -175,7 +185,7 @@ function createDownloadHarness({ result, downloadError, openError } = {}) {
 			})
 		}
 	}
-	const transport = loadTransport({ uni, wx })
+	const transport = await loadTransport({ uni, wx })
 
 	return {
 		request: () => request,
@@ -185,7 +195,13 @@ function createDownloadHarness({ result, downloadError, openError } = {}) {
 }
 
 async function testDownloadTransport() {
-	const successHarness = createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' } })
+	const successHarness = await createDownloadHarness({
+		result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
+		responseHeaders: {
+			'cOnTeNt-TyPe': 'application/pdf',
+			'CONTENT-disPOSITION': 'attachment; filename="download.pdf"'
+		}
+	})
 	assert.equal(await successHarness.promise, 'opened')
 	assert.equal(successHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files/34/download')
 	assert.equal(successHarness.request().header.token, 'token-456')
@@ -193,23 +209,79 @@ async function testDownloadTransport() {
 	assert.equal(successHarness.openRequest().showMenu, true)
 	assertNoIdentityFields(successHarness.request())
 
-	const statusHarness = createDownloadHarness({ result: { statusCode: 500 } })
+	const resultHeaderHarness = await createDownloadHarness({
+		result: {
+			statusCode: 200,
+			tempFilePath: '/tmp/result-header.docx',
+			header: {
+				'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+				'content-disposition': 'attachment; filename="result-header.docx"'
+			}
+		},
+		withHeadersListener: false
+	})
+	assert.equal(await resultHeaderHarness.promise, 'opened')
+
+	const resultHeadersHarness = await createDownloadHarness({
+		result: {
+			statusCode: 200,
+			tempFilePath: '/tmp/result-headers.txt',
+			headers: {
+				'Content-Type': 'text/plain; charset=UTF-8',
+				'Content-Disposition': 'attachment; filename="result-headers.txt"'
+			}
+		},
+		withHeadersListener: false
+	})
+	assert.equal(await resultHeadersHarness.promise, 'opened')
+
+	const jsonHarness = await createDownloadHarness({
+		result: { statusCode: 200, tempFilePath: '/tmp/error.json' },
+		responseHeaders: {
+			'Content-Type': 'application/json;charset=UTF-8',
+			'Content-Disposition': 'attachment; filename="error.json"'
+		}
+	})
+	await assert.rejects(jsonHarness.promise, /下载失败,请重试/)
+	assert.equal(jsonHarness.openRequest(), undefined)
+
+	const htmlHarness = await createDownloadHarness({
+		result: { statusCode: 200, tempFilePath: '/tmp/error.html' },
+		responseHeaders: { 'content-type': 'text/html; charset=UTF-8' }
+	})
+	await assert.rejects(htmlHarness.promise, /下载失败,请重试/)
+	assert.equal(htmlHarness.openRequest(), undefined)
+
+	const missingHeadersHarness = await createDownloadHarness({
+		result: { statusCode: 200, tempFilePath: '/tmp/unknown.pdf' },
+		withHeadersListener: false
+	})
+	await assert.rejects(missingHeadersHarness.promise, /下载失败,请重试/)
+	assert.equal(missingHeadersHarness.openRequest(), undefined)
+
+	const statusHarness = await createDownloadHarness({ result: { statusCode: 500 } })
 	await assert.rejects(statusHarness.promise, /下载失败,请重试/)
 	assert.equal(statusHarness.openRequest(), undefined)
 
 	const downloadError = new Error('download network error')
-	const downloadFailureHarness = createDownloadHarness({ downloadError })
+	const downloadFailureHarness = await createDownloadHarness({ downloadError })
 	await assert.rejects(downloadFailureHarness.promise, /download network error/)
 
 	const openError = new Error('open document error')
-	const openFailureHarness = createDownloadHarness({
+	const openFailureHarness = await createDownloadHarness({
 		result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
+		responseHeaders: {
+			'Content-Type': 'application/pdf',
+			'Content-Disposition': 'attachment; filename="download.pdf"'
+		},
 		openError
 	})
 	await assert.rejects(openFailureHarness.promise, /open document error/)
 }
 
 async function main() {
+	const loadedTransport = await loadTransport()
+	assert.equal(typeof loadedTransport.downloadTeamBackgroundFile, 'function')
 	await testUploadTransport()
 	await testApiTransport()
 	await testDownloadTransport()