teamBackgroundFile.test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. const assert = require('node:assert/strict')
  2. const fs = require('node:fs')
  3. const path = require('node:path')
  4. const { pathToFileURL } = require('node:url')
  5. const vm = require('node:vm')
  6. const rules = require('../utils/teamBackgroundFile')
  7. assert.equal(rules.validateEnterpriseWebsite(' 无 '), '')
  8. assert.equal(rules.validateEnterpriseWebsite('https://example.com'), '')
  9. assert.match(rules.validateEnterpriseWebsite('ftp://example.com'), /http/)
  10. assert.equal(rules.validateBackgroundFile({ name: 'brief.PDF', size: 1 }), '')
  11. assert.match(rules.validateBackgroundFile({ name: 'brief.zip', size: 1 }), /类型/)
  12. assert.match(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 + 1 }), /50MB/)
  13. assert.equal(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 }), '')
  14. assert.equal(rules.validateBackgroundFile({ name: `${'a'.repeat(251)}.pdf`, size: 1 }), '')
  15. assert.match(rules.validateBackgroundFile({ name: `${'a'.repeat(252)}.pdf`, size: 1 }), /255/)
  16. assert.match(rules.validateBackgroundFile({ name: 'empty.pdf', size: 0 }), /非空/)
  17. console.log('team background rules: PASS')
  18. const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js')
  19. async function loadTransport({ api = {}, uni = {}, wx = {} } = {}) {
  20. const source = fs.readFileSync(transportPath, 'utf8')
  21. const context = vm.createContext({ uni, wx })
  22. const baseApiModule = new vm.SyntheticModule(['BaseApi'], function () {
  23. this.setExport('BaseApi', 'https://api.example.test/app')
  24. }, { context, identifier: 'test:baseApi' })
  25. const apiModule = new vm.SyntheticModule(['default'], function () {
  26. this.setExport('default', api)
  27. }, { context, identifier: 'test:api' })
  28. const transportModule = new vm.SourceTextModule(source, {
  29. context,
  30. identifier: pathToFileURL(transportPath).href
  31. })
  32. await transportModule.link(specifier => {
  33. if (specifier === './baseApi.js') return baseApiModule
  34. if (specifier === './index.js') return apiModule
  35. throw new Error(`unexpected transport import: ${specifier}`)
  36. })
  37. await transportModule.evaluate()
  38. return transportModule.namespace
  39. }
  40. function assertNoIdentityFields(value) {
  41. if (!value || typeof value !== 'object') return
  42. for (const [key, child] of Object.entries(value)) {
  43. assert.ok(!['source', 'coachId', 'uploaderId'].includes(key), `unexpected identity field: ${key}`)
  44. assertNoIdentityFields(child)
  45. }
  46. }
  47. async function createUploadHarness(response, progress = 37) {
  48. let request
  49. const progressValues = []
  50. const uni = {
  51. getStorageSync(key) {
  52. assert.equal(key, 'token')
  53. return 'token-123'
  54. },
  55. uploadFile(options) {
  56. request = options
  57. return {
  58. onProgressUpdate(callback) {
  59. callback({ progress })
  60. setImmediate(() => options.success(response))
  61. }
  62. }
  63. }
  64. }
  65. const transport = await loadTransport({ uni })
  66. return {
  67. request: () => request,
  68. progressValues,
  69. promise: transport.uploadTeamBackgroundFile(12, '/tmp/brief.pdf', value => progressValues.push(value))
  70. }
  71. }
  72. async function testUploadTransport() {
  73. const stringHarness = await createUploadHarness({
  74. statusCode: 200,
  75. data: JSON.stringify({ code: 0, data: { id: 7 } })
  76. })
  77. assert.equal((await stringHarness.promise).id, 7)
  78. assert.equal(stringHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files')
  79. assert.equal(stringHarness.request().filePath, '/tmp/brief.pdf')
  80. assert.equal(stringHarness.request().name, 'file')
  81. assert.equal(stringHarness.request().header.token, 'token-123')
  82. assert.deepEqual(stringHarness.progressValues, [37])
  83. assertNoIdentityFields(stringHarness.request())
  84. const objectHarness = await createUploadHarness({ statusCode: 200, data: { code: 0, data: { id: 8 } } })
  85. assert.equal((await objectHarness.promise).id, 8)
  86. const statusHarness = await createUploadHarness({ statusCode: 500, data: { code: 0 } })
  87. await assert.rejects(statusHarness.promise, /上传失败,请重试/)
  88. const codeHarness = await createUploadHarness({ statusCode: 200, data: { code: 9, msg: '上传业务失败' } })
  89. await assert.rejects(codeHarness.promise, /上传业务失败/)
  90. const invalidJsonHarness = await createUploadHarness({ statusCode: 200, data: '<invalid-json>' })
  91. await assert.rejects(invalidJsonHarness.promise, /上传失败,请重试/)
  92. }
  93. async function testApiTransport() {
  94. const calls = []
  95. const api = {
  96. get(...args) {
  97. calls.push(['get', ...args])
  98. return Promise.resolve({ data: { code: 0, data: [{ id: 1 }] } })
  99. },
  100. del(...args) {
  101. calls.push(['del', ...args])
  102. return Promise.resolve({ data: { code: 0, data: true } })
  103. },
  104. post(...args) {
  105. calls.push(['post', ...args])
  106. return Promise.resolve({ data: { code: 0, data: { sessionId: 'session-1' } } })
  107. }
  108. }
  109. const transport = await loadTransport({ api })
  110. assert.deepEqual(await transport.listTeamBackgroundFiles(12), [{ id: 1 }])
  111. assert.equal(await transport.disableTeamBackgroundFile(12, 34), true)
  112. assert.deepEqual(await transport.createTeamBackgroundSession(12), { sessionId: 'session-1' })
  113. assert.equal(calls[0][0], 'get')
  114. assert.equal(calls[0][1], '/core/user/team/12/background-files')
  115. assert.equal(calls[1][0], 'del')
  116. assert.equal(calls[1][1], '/core/user/team/12/background-files/34')
  117. assert.equal(calls[2][0], 'post')
  118. assert.equal(calls[2][1], '/core/user/team/12/background-upload-session')
  119. for (const call of calls) {
  120. assert.equal(Object.keys(call[2]).length, 0)
  121. assert.equal(call[3], false)
  122. assertNoIdentityFields(call)
  123. }
  124. const failingTransport = await loadTransport({
  125. api: {
  126. get: () => Promise.resolve({ data: { code: 3, msg: '列表业务失败' } })
  127. }
  128. })
  129. await assert.rejects(failingTransport.listTeamBackgroundFiles(12), /列表业务失败/)
  130. assert.equal(transport.uploadTeamBackgroundFile.length, 2)
  131. assert.equal(transport.listTeamBackgroundFiles.length, 1)
  132. assert.equal(transport.disableTeamBackgroundFile.length, 2)
  133. assert.equal(transport.createTeamBackgroundSession.length, 1)
  134. assert.equal(transport.downloadTeamBackgroundFile.length, 2)
  135. }
  136. const TEXT_PREVIEW_LIMIT = 256 * 1024
  137. async function createDownloadHarness({ result, responseHeaders, downloadError, openError,
  138. fileSize, fileContent = '', fileInfoError, readError, withHeadersListener = true } = {}) {
  139. let request
  140. let openRequest
  141. let fileInfoRequest
  142. let readRequest
  143. let fileSystemManagerCalls = 0
  144. const resolvedFileSize = fileSize === undefined
  145. ? Buffer.byteLength(fileContent, 'utf8')
  146. : fileSize
  147. const uni = {
  148. getStorageSync(key) {
  149. assert.equal(key, 'token')
  150. return 'token-456'
  151. },
  152. downloadFile(options) {
  153. request = options
  154. setImmediate(() => {
  155. if (downloadError) options.fail(downloadError)
  156. else options.success(result)
  157. })
  158. if (!withHeadersListener) return {}
  159. return {
  160. onHeadersReceived(callback) {
  161. if (responseHeaders) callback({ header: responseHeaders })
  162. }
  163. }
  164. }
  165. }
  166. const wx = {
  167. openDocument(options) {
  168. openRequest = options
  169. setImmediate(() => {
  170. if (openError) options.fail(openError)
  171. else options.success('opened')
  172. })
  173. },
  174. getFileSystemManager() {
  175. fileSystemManagerCalls += 1
  176. return {
  177. getFileInfo(options) {
  178. fileInfoRequest = options
  179. setImmediate(() => {
  180. if (fileInfoError) options.fail(fileInfoError)
  181. else options.success({ size: resolvedFileSize })
  182. })
  183. },
  184. readFile(options) {
  185. readRequest = options
  186. setImmediate(() => {
  187. if (readError) options.fail(readError)
  188. else options.success({ data: fileContent })
  189. })
  190. }
  191. }
  192. }
  193. }
  194. const transport = await loadTransport({ uni, wx })
  195. return {
  196. request: () => request,
  197. openRequest: () => openRequest,
  198. fileInfoRequest: () => fileInfoRequest,
  199. readRequest: () => readRequest,
  200. fileSystemManagerCalls: () => fileSystemManagerCalls,
  201. promise: transport.downloadTeamBackgroundFile(12, 34)
  202. }
  203. }
  204. async function testDownloadTransport() {
  205. const successHarness = await createDownloadHarness({
  206. result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
  207. responseHeaders: {
  208. 'cOnTeNt-TyPe': 'application/pdf',
  209. 'CONTENT-disPOSITION': 'attachment; filename="download.pdf"'
  210. }
  211. })
  212. assert.equal(await successHarness.promise, 'opened')
  213. assert.equal(successHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files/34/download')
  214. assert.equal(successHarness.request().header.token, 'token-456')
  215. assert.equal(successHarness.openRequest().filePath, '/tmp/download.pdf')
  216. assert.equal(successHarness.openRequest().showMenu, true)
  217. assertNoIdentityFields(successHarness.request())
  218. const resultHeaderHarness = await createDownloadHarness({
  219. result: {
  220. statusCode: 200,
  221. tempFilePath: '/tmp/result-header.docx',
  222. header: {
  223. 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  224. 'content-disposition': 'attachment; filename="result-header.docx"'
  225. }
  226. },
  227. withHeadersListener: false
  228. })
  229. assert.equal(await resultHeaderHarness.promise, 'opened')
  230. assert.equal(resultHeaderHarness.fileSystemManagerCalls(), 0)
  231. const txtHarness = await createDownloadHarness({
  232. result: {
  233. statusCode: 200,
  234. tempFilePath: '/tmp/text-preview.txt',
  235. headers: {
  236. 'Content-Type': 'text/plain; charset=UTF-8',
  237. 'Content-Disposition': 'attachment; filename="fallback.txt"; '
  238. + "filename*=UTF-8''%E8%83%8C%E6%99%AF%20%E8%B5%84%E6%96%99.txt"
  239. }
  240. },
  241. fileSize: 5,
  242. fileContent: 'hello',
  243. withHeadersListener: false
  244. })
  245. const txtPreview = await txtHarness.promise
  246. assert.equal(txtHarness.openRequest(), undefined, 'txt must not call openDocument')
  247. assert.equal(txtPreview.mode, 'text')
  248. assert.equal(txtPreview.fileName, '背景 资料.txt')
  249. assert.equal(txtPreview.content, 'hello')
  250. assert.equal(txtPreview.truncated, false)
  251. assert.equal(txtHarness.fileInfoRequest().filePath, '/tmp/text-preview.txt')
  252. assert.equal(txtHarness.readRequest().filePath, '/tmp/text-preview.txt')
  253. assert.equal(txtHarness.readRequest().encoding, 'utf8')
  254. assert.equal(txtHarness.readRequest().position, 0)
  255. assert.equal(txtHarness.readRequest().length, 5)
  256. const mdContent = 'm'.repeat(TEXT_PREVIEW_LIMIT)
  257. const mdHarness = await createDownloadHarness({
  258. result: { statusCode: 200, tempFilePath: '/tmp/guide.md' },
  259. responseHeaders: {
  260. 'Content-Type': 'text/markdown; charset=UTF-8',
  261. 'Content-Disposition': 'attachment; filename="guide.md"'
  262. },
  263. fileSize: 50 * 1024 * 1024,
  264. fileContent: mdContent
  265. })
  266. const mdPreview = await mdHarness.promise
  267. assert.equal(mdHarness.openRequest(), undefined)
  268. assert.equal(mdPreview.mode, 'text')
  269. assert.equal(mdPreview.fileName, 'guide.md')
  270. assert.equal(mdPreview.content.length, TEXT_PREVIEW_LIMIT)
  271. assert.equal(mdPreview.truncated, true)
  272. assert.equal(mdHarness.readRequest().length, TEXT_PREVIEW_LIMIT)
  273. const csvContent = 'c'.repeat(TEXT_PREVIEW_LIMIT)
  274. const csvHarness = await createDownloadHarness({
  275. result: { statusCode: 200, tempFilePath: '/tmp/data.csv' },
  276. responseHeaders: {
  277. 'Content-Type': 'text/csv',
  278. 'Content-Disposition': 'attachment; filename=data.csv'
  279. },
  280. fileSize: TEXT_PREVIEW_LIMIT,
  281. fileContent: csvContent
  282. })
  283. const csvPreview = await csvHarness.promise
  284. assert.equal(csvHarness.openRequest(), undefined)
  285. assert.equal(csvPreview.mode, 'text')
  286. assert.equal(csvPreview.fileName, 'data.csv')
  287. assert.equal(csvPreview.content.length, TEXT_PREVIEW_LIMIT)
  288. assert.equal(csvPreview.truncated, false)
  289. assert.equal(csvHarness.readRequest().length, TEXT_PREVIEW_LIMIT)
  290. const fileInfoFailureHarness = await createDownloadHarness({
  291. result: { statusCode: 200, tempFilePath: '/tmp/file-info-failure.txt' },
  292. responseHeaders: {
  293. 'Content-Type': 'text/plain',
  294. 'Content-Disposition': 'attachment; filename="file-info-failure.txt"'
  295. },
  296. fileInfoError: new Error('getFileInfo failed')
  297. })
  298. await assert.rejects(fileInfoFailureHarness.promise, /文件读取失败,请重试/)
  299. assert.equal(fileInfoFailureHarness.openRequest(), undefined)
  300. assert.equal(fileInfoFailureHarness.readRequest(), undefined)
  301. const readFailureHarness = await createDownloadHarness({
  302. result: { statusCode: 200, tempFilePath: '/tmp/read-failure.md' },
  303. responseHeaders: {
  304. 'Content-Type': 'text/markdown',
  305. 'Content-Disposition': 'attachment; filename="read-failure.md"'
  306. },
  307. fileSize: 10,
  308. readError: new Error('readFile failed')
  309. })
  310. await assert.rejects(readFailureHarness.promise, /文件读取失败,请重试/)
  311. assert.equal(readFailureHarness.openRequest(), undefined)
  312. const unknownExtensionHarness = await createDownloadHarness({
  313. result: { statusCode: 200, tempFilePath: '/tmp/unknown-file' },
  314. responseHeaders: {
  315. 'Content-Type': 'application/octet-stream',
  316. 'Content-Disposition': 'attachment; filename="archive.zip"'
  317. }
  318. })
  319. await assert.rejects(unknownExtensionHarness.promise, /文件类型不支持预览/)
  320. assert.equal(unknownExtensionHarness.openRequest(), undefined)
  321. assert.equal(unknownExtensionHarness.fileSystemManagerCalls(), 0)
  322. const jsonHarness = await createDownloadHarness({
  323. result: { statusCode: 200, tempFilePath: '/tmp/error.json' },
  324. responseHeaders: {
  325. 'Content-Type': 'application/json;charset=UTF-8',
  326. 'Content-Disposition': 'attachment; filename="error.json"'
  327. }
  328. })
  329. await assert.rejects(jsonHarness.promise, /下载失败,请重试/)
  330. assert.equal(jsonHarness.openRequest(), undefined)
  331. const htmlHarness = await createDownloadHarness({
  332. result: { statusCode: 200, tempFilePath: '/tmp/error.html' },
  333. responseHeaders: {
  334. 'content-type': 'text/html; charset=UTF-8',
  335. 'content-disposition': 'attachment; filename="error.html"'
  336. }
  337. })
  338. await assert.rejects(htmlHarness.promise, /下载失败,请重试/)
  339. assert.equal(htmlHarness.openRequest(), undefined)
  340. const missingHeadersHarness = await createDownloadHarness({
  341. result: { statusCode: 200, tempFilePath: '/tmp/unknown.pdf' },
  342. withHeadersListener: false
  343. })
  344. await assert.rejects(missingHeadersHarness.promise, /下载失败,请重试/)
  345. assert.equal(missingHeadersHarness.openRequest(), undefined)
  346. const statusHarness = await createDownloadHarness({ result: { statusCode: 500 } })
  347. await assert.rejects(statusHarness.promise, /下载失败,请重试/)
  348. assert.equal(statusHarness.openRequest(), undefined)
  349. const downloadError = new Error('download network error')
  350. const downloadFailureHarness = await createDownloadHarness({ downloadError })
  351. await assert.rejects(downloadFailureHarness.promise, /download network error/)
  352. const openError = new Error('open document error')
  353. const openFailureHarness = await createDownloadHarness({
  354. result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
  355. responseHeaders: {
  356. 'Content-Type': 'application/pdf',
  357. 'Content-Disposition': 'attachment; filename="download.pdf"'
  358. },
  359. openError
  360. })
  361. await assert.rejects(openFailureHarness.promise, /open document error/)
  362. }
  363. async function main() {
  364. const loadedTransport = await loadTransport()
  365. assert.equal(typeof loadedTransport.downloadTeamBackgroundFile, 'function')
  366. await testUploadTransport()
  367. await testApiTransport()
  368. await testDownloadTransport()
  369. console.log('team background transport: PASS')
  370. }
  371. main().catch(error => {
  372. console.error(error)
  373. process.exitCode = 1
  374. })