teamBackgroundFile.test.js 17 KB

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