sumor 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc.yml +4 -0
  3. package/.gitattributes +1 -0
  4. package/.github/workflows/audit.yml +22 -0
  5. package/.github/workflows/available.yml +22 -0
  6. package/.github/workflows/ci.yml +45 -0
  7. package/.github/workflows/coverage.yml +23 -0
  8. package/.github/workflows/publish.yml +31 -0
  9. package/.github/workflows/ut.yml +24 -0
  10. package/.husky/pre-commit +10 -0
  11. package/.husky/pre-push +10 -0
  12. package/.prettierrc.yml +7 -0
  13. package/LICENSE +21 -0
  14. package/demo/api/error.js +3 -0
  15. package/demo/api/file.js +4 -0
  16. package/demo/api/file.yaml +11 -0
  17. package/demo/api/hello.js +4 -0
  18. package/demo/api/hello.yaml +13 -0
  19. package/demo/api/login.js +4 -0
  20. package/demo/api/logout.js +4 -0
  21. package/demo/api/long.js +9 -0
  22. package/demo/api/long.yaml +2 -0
  23. package/demo/api/user/admin.js +12 -0
  24. package/demo/api/user/info.js +8 -0
  25. package/demo/config/config.yaml +2 -0
  26. package/demo/i18n/demo.yaml +1 -0
  27. package/demo/package.json +6 -0
  28. package/i18n/README.md +32 -0
  29. package/i18n/sumor_api.yaml +16 -0
  30. package/i18n/sumor_app.yaml +3 -0
  31. package/i18n/sumor_internal.yaml +1 -0
  32. package/i18n/sumor_token.yaml +2 -0
  33. package/i18nExt/sumor_api.de.yaml +3 -0
  34. package/i18nExt/sumor_api.en.yaml +3 -0
  35. package/i18nExt/sumor_api.es.yaml +3 -0
  36. package/i18nExt/sumor_api.fr.yaml +3 -0
  37. package/i18nExt/sumor_api.it.yaml +3 -0
  38. package/i18nExt/sumor_api.ja.yaml +3 -0
  39. package/i18nExt/sumor_api.ko.yaml +3 -0
  40. package/i18nExt/sumor_api.pt.yaml +3 -0
  41. package/i18nExt/sumor_api.zh-TW.yaml +3 -0
  42. package/i18nExt/sumor_api.zh.yaml +3 -0
  43. package/i18nExt/sumor_app.de.yaml +3 -0
  44. package/i18nExt/sumor_app.en.yaml +3 -0
  45. package/i18nExt/sumor_app.es.yaml +3 -0
  46. package/i18nExt/sumor_app.zh-TW.yaml +3 -0
  47. package/i18nExt/sumor_app.zh.yaml +3 -0
  48. package/i18nExt/sumor_internal.de.yaml +1 -0
  49. package/i18nExt/sumor_internal.en.yaml +1 -0
  50. package/i18nExt/sumor_internal.es.yaml +1 -0
  51. package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
  52. package/i18nExt/sumor_internal.zh.yaml +1 -0
  53. package/i18nExt/sumor_token.de.yaml +2 -0
  54. package/i18nExt/sumor_token.es.yaml +2 -0
  55. package/i18nExt/sumor_token.fr.yaml +2 -0
  56. package/i18nExt/sumor_token.it.yaml +2 -0
  57. package/i18nExt/sumor_token.ja.yaml +2 -0
  58. package/i18nExt/sumor_token.ko.yaml +2 -0
  59. package/i18nExt/sumor_token.pt.yaml +2 -0
  60. package/i18nExt/sumor_token.ru.yaml +2 -0
  61. package/i18nExt/sumor_token.tr.yaml +2 -0
  62. package/i18nExt/sumor_token.zh-TW.yaml +2 -0
  63. package/i18nExt/sumor_token.zh.yaml +2 -0
  64. package/jest.config.json +26 -0
  65. package/modules/alertPage/html/template.html +162 -0
  66. package/modules/alertPage/html/undraw_access-denied.svg +52 -0
  67. package/modules/alertPage/html/undraw_alert.svg +1 -0
  68. package/modules/alertPage/html/undraw_celebration.svg +78 -0
  69. package/modules/alertPage/html/undraw_complete-form.svg +1 -0
  70. package/modules/alertPage/html/undraw_login.svg +1 -0
  71. package/modules/alertPage/index.js +46 -0
  72. package/modules/alertPage/index.test.js +12 -0
  73. package/modules/i18n/README.md +90 -0
  74. package/modules/i18n/convertI18nValue/README.md +31 -0
  75. package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
  76. package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
  77. package/modules/i18n/convertI18nValue/index.js +14 -0
  78. package/modules/i18n/convertI18nValue/index.test.js +55 -0
  79. package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
  80. package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
  81. package/modules/i18n/index.js +26 -0
  82. package/modules/i18n/index.test.js +13 -0
  83. package/modules/i18n/load/README.md +28 -0
  84. package/modules/i18n/load/load.js +31 -0
  85. package/modules/i18n/load/load.test.js +30 -0
  86. package/modules/i18n/registry.js +48 -0
  87. package/modules/i18n/registry.test.js +84 -0
  88. package/modules/logger/convert/parseFile.js +14 -0
  89. package/modules/logger/convert/parseFile.test.js +28 -0
  90. package/modules/logger/convert/stringifyCMD.js +48 -0
  91. package/modules/logger/convert/stringifyCMD.test.js +37 -0
  92. package/modules/logger/convert/stringifyFile.js +24 -0
  93. package/modules/logger/convert/stringifyFile.test.js +79 -0
  94. package/modules/logger/index.js +82 -0
  95. package/modules/logger/index.test.js +124 -0
  96. package/modules/logger/logFileOperator.js +50 -0
  97. package/modules/logger/logFileOperator.test.js +69 -0
  98. package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
  99. package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
  100. package/modules/middlewares/apiMiddleware/index.js +111 -0
  101. package/modules/middlewares/apiMiddleware/index.test.js +145 -0
  102. package/modules/middlewares/apiMiddleware/load/index.js +35 -0
  103. package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
  104. package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
  105. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
  106. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
  107. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
  108. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
  109. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
  110. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
  111. package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
  112. package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
  113. package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
  114. package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
  115. package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
  116. package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
  117. package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
  118. package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
  119. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
  120. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
  121. package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
  122. package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
  123. package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
  124. package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
  125. package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
  126. package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
  127. package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
  128. package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
  129. package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
  130. package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
  131. package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
  132. package/modules/middlewares/bodyMiddleware/index.js +12 -0
  133. package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
  134. package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
  135. package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
  136. package/modules/middlewares/i18nMiddleware/index.js +82 -0
  137. package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
  138. package/modules/middlewares/tokenMiddleware/Token.js +115 -0
  139. package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
  140. package/modules/middlewares/tokenMiddleware/index.js +32 -0
  141. package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
  142. package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
  143. package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
  144. package/modules/serve/formatConfig.js +7 -0
  145. package/modules/serve/formatConfig.test.js +28 -0
  146. package/modules/serve/index.js +30 -0
  147. package/modules/serve/index.test.js +69 -0
  148. package/modules/serve/listenApp.js +11 -0
  149. package/modules/system/getSystemLanguage.js +9 -0
  150. package/modules/system/getSystemLanguage.test.js +19 -0
  151. package/modules/utils/getError.js +56 -0
  152. package/modules/utils/getError.test.js +97 -0
  153. package/modules/utils/loadConfig.js +73 -0
  154. package/modules/utils/loadConfig.test.js +129 -0
  155. package/modules/utils/pathUtils.js +43 -0
  156. package/modules/utils/pathUtils.test.js +52 -0
  157. package/modules/utils/type.js +14 -0
  158. package/modules/utils/type.test.js +40 -0
  159. package/package.json +61 -1
  160. package/src/app.js +40 -0
  161. package/src/cli.js +28 -0
  162. package/src/index.js +3 -0
  163. package/test-utils/codeTest/README.md +4 -0
  164. package/test-utils/codeTest/custom.css +47 -0
  165. package/test-utils/codeTest/index.js +80 -0
  166. package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
  167. package/test-utils/codeTest/utils/getTestFiles.js +20 -0
  168. package/test-utils/codeTest/utils/runTests.js +38 -0
  169. package/test-utils/codeTest/utils/startServer.js +23 -0
  170. package/test-utils/tmp.js +15 -0
  171. package/cli.js +0 -3
  172. package/index.es.js +0 -578
  173. package/template/web/AppWithFrame.vue +0 -20
  174. package/template/web/index.html +0 -19
  175. package/template/web/src/App.vue +0 -15
  176. package/template/web/src/entry-client.js +0 -9
  177. package/template/web/src/entry-server.js +0 -69
  178. package/template/web/src/main.js +0 -41
  179. package/template/web/src/style.scss +0 -389
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import prepareData from './index.js'
3
+
4
+ describe('prepareData', () => {
5
+ it('应该转换类型并验证参数', () => {
6
+ const req = {
7
+ data: {
8
+ name: 'John',
9
+ age: '25'
10
+ },
11
+ namespace: namespace => {
12
+ if (namespace === 'SUMOR_API') {
13
+ return {
14
+ Error: class extends Error {
15
+ constructor(code, details) {
16
+ super(code)
17
+ this.details = details
18
+ }
19
+ }
20
+ }
21
+ }
22
+ if (namespace === 'API') {
23
+ return {
24
+ Error: class extends Error {
25
+ constructor(code, details) {
26
+ super(code)
27
+ this.details = details
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ const parameters = {
36
+ name: { type: 'string', required: true, length: 10 },
37
+ age: { type: 'number', required: true }
38
+ }
39
+
40
+ const result = prepareData(req, '/unit-test', parameters)
41
+
42
+ expect(result).toEqual({
43
+ name: 'John',
44
+ age: 25
45
+ })
46
+ })
47
+
48
+ it('应该抛出缺少必需参数的错误', () => {
49
+ const req = {
50
+ data: {},
51
+ namespace: namespace => {
52
+ if (namespace === 'SUMOR_API') {
53
+ return {
54
+ Error: class extends Error {
55
+ constructor(code, details) {
56
+ super(code)
57
+ this.details = details
58
+ }
59
+ }
60
+ }
61
+ }
62
+ if (namespace === 'API') {
63
+ return {
64
+ Error: class extends Error {
65
+ constructor(code, details) {
66
+ super(code)
67
+ this.details = details
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ const parameters = {
76
+ name: { type: 'string', required: true }
77
+ }
78
+
79
+ expect(() => prepareData(req, '/unit-test', parameters)).toThrowError(
80
+ 'API_PARAMETERS_NOT_VALID'
81
+ )
82
+ })
83
+
84
+ it('应该抛出字符串超出长度限制的错误', () => {
85
+ const req = {
86
+ data: {
87
+ name: 'This is a very long name'
88
+ },
89
+ namespace: namespace => {
90
+ if (namespace === 'SUMOR_API') {
91
+ return {
92
+ Error: class extends Error {
93
+ constructor(code, details) {
94
+ super(code)
95
+ this.details = details
96
+ }
97
+ }
98
+ }
99
+ }
100
+ if (namespace === 'API') {
101
+ return {
102
+ Error: class extends Error {
103
+ constructor(code, details) {
104
+ super(code)
105
+ this.details = details
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ const parameters = {
114
+ name: { type: 'string', required: true, length: 10 }
115
+ }
116
+
117
+ expect(() => prepareData(req, '/unit-test', parameters)).toThrowError(
118
+ 'API_PARAMETERS_NOT_VALID'
119
+ )
120
+ })
121
+ })
@@ -0,0 +1,26 @@
1
+ export default (parameter, value) => {
2
+ if (parameter.length && value !== null) {
3
+ switch (parameter.type) {
4
+ case 'string':
5
+ if (value.length > parameter.length) {
6
+ return true
7
+ }
8
+ break
9
+ case 'number':
10
+ if (value.toString().length > parameter.length) {
11
+ return true
12
+ }
13
+ break
14
+ case 'array':
15
+ case 'file':
16
+ if (Array.isArray(value) && value.length > parameter.length) {
17
+ return true
18
+ }
19
+ break
20
+ default:
21
+ break
22
+ }
23
+ }
24
+
25
+ return false
26
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import checkLength from './checkLength.js'
3
+
4
+ describe('数据长度检查', () => {
5
+ it('应该返回 true,当字符串长度超过限制', () => {
6
+ const parameter = { type: 'string', length: 5 }
7
+ const value = 'exceeds'
8
+ expect(checkLength(parameter, value)).toBe(true)
9
+ })
10
+
11
+ it('应该返回 false,当字符串长度在限制范围内', () => {
12
+ const parameter = { type: 'string', length: 10 }
13
+ const value = 'short'
14
+ expect(checkLength(parameter, value)).toBe(false)
15
+ })
16
+
17
+ it('应该返回 true,当数字长度超过限制', () => {
18
+ const parameter = { type: 'number', length: 3 }
19
+ const value = 12345
20
+ expect(checkLength(parameter, value)).toBe(true)
21
+ })
22
+
23
+ it('应该返回 false,当数字长度在限制范围内', () => {
24
+ const parameter = { type: 'number', length: 5 }
25
+ const value = 123
26
+ expect(checkLength(parameter, value)).toBe(false)
27
+ })
28
+
29
+ it('应该返回 true,当数组长度超过限制', () => {
30
+ const parameter = { type: 'array', length: 2 }
31
+ const value = [1, 2, 3]
32
+ expect(checkLength(parameter, value)).toBe(true)
33
+ })
34
+
35
+ it('应该返回 false,当数组长度在限制范围内', () => {
36
+ const parameter = { type: 'array', length: 5 }
37
+ const value = [1, 2, 3]
38
+ expect(checkLength(parameter, value)).toBe(false)
39
+ })
40
+
41
+ it('应该返回 false,当值为 null', () => {
42
+ const parameter = { type: 'string', length: 5 }
43
+ const value = null
44
+ expect(checkLength(parameter, value)).toBe(false)
45
+ })
46
+
47
+ it('应该返回 false,当类型不受支持', () => {
48
+ const parameter = { type: 'unsupported', length: 5 }
49
+ const value = 'test'
50
+ expect(checkLength(parameter, value)).toBe(false)
51
+ })
52
+ })
@@ -0,0 +1,30 @@
1
+ import checkLength from './checkLength.js'
2
+
3
+ export default (Error, APIError, apiKey, parameter, value) => {
4
+ const errors = []
5
+
6
+ // 检查必填项
7
+ if (parameter.required) {
8
+ if (value === null || (parameter.type === 'string' && value === '')) {
9
+ errors.push(new Error('API_PARAMETER_REQUIRED'))
10
+ }
11
+ }
12
+
13
+ // 检查长度
14
+ if (checkLength(parameter, value)) {
15
+ errors.push(new Error('API_PARAMETER_TOO_LONG', { length: parameter.length }))
16
+ }
17
+
18
+ // 检查规则
19
+ for (const rule in parameter.rules) {
20
+ const ruleInfo = parameter.rules[rule]
21
+ if (ruleInfo.expression) {
22
+ const regexp = new RegExp(ruleInfo.expression)
23
+ if (!regexp.test(value)) {
24
+ errors.push(new APIError(`parameter.${parameter.key}.rules.${rule}@${apiKey}`))
25
+ }
26
+ }
27
+ }
28
+
29
+ return errors
30
+ }
@@ -0,0 +1,57 @@
1
+ import sendResponse from './sendResponse.js'
2
+
3
+ export default (req, res, err) => {
4
+ const apiNamespace = req.namespace('SUMOR_API')
5
+
6
+ let error
7
+ if (err.json) {
8
+ error = err.json()
9
+ } else {
10
+ // 非可控错误
11
+ error = {
12
+ code: 'API_ERROR',
13
+ message: apiNamespace.i18n('API_ERROR_UNKNOWN_ERROR')
14
+ }
15
+ apiNamespace.logger.error('API_ERROR_UNKNOWN_MESSAGE', {
16
+ path: req.path
17
+ })
18
+ apiNamespace.logger.error(err)
19
+ }
20
+
21
+ let code
22
+ let svg
23
+ switch (error.code) {
24
+ case 'API_PARAMETERS_NOT_VALID':
25
+ code = 400
26
+ svg = 'undraw_complete-form'
27
+ break
28
+ case 'LOGIN_EXPIRED':
29
+ code = 401
30
+ svg = 'undraw_login'
31
+ break
32
+ case 'PERMISSION_DENIED':
33
+ code = 403
34
+ svg = 'undraw_access-denied'
35
+ break
36
+ default:
37
+ code = 500
38
+ svg = 'undraw_alert'
39
+ break
40
+ }
41
+
42
+ const data = {
43
+ svg,
44
+ status: code,
45
+ json: error,
46
+ html: {
47
+ title: error.message,
48
+ major: error.message,
49
+ minor: '',
50
+ showMore: true,
51
+ moreButton: apiNamespace.i18n('API_ERROR_SHOW_MORE'),
52
+ moreContent: JSON.stringify(error, null, 4)
53
+ }
54
+ }
55
+
56
+ sendResponse(req, res, data)
57
+ }
@@ -0,0 +1,251 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'
2
+ import axios from 'axios'
3
+ import express from 'express'
4
+ import i18nMiddleware from '../../i18nMiddleware/index.js'
5
+ import http from 'http'
6
+ import fse from 'fs-extra'
7
+ import sendError from './sendError.js'
8
+
9
+ const tmpTroubleshooting = './tmp/testTroubleshooting'
10
+
11
+ describe('Error Middleware', () => {
12
+ let app
13
+ let server
14
+ let baseURL
15
+
16
+ beforeAll(async () => {
17
+ app = express()
18
+ app.use(express.json())
19
+
20
+ process.env.LANGUAGE = 'en-US'
21
+ await i18nMiddleware(app)
22
+
23
+ app.get('/error', (req, res, next) => {
24
+ const error = new Error('Test Error')
25
+ error.code = 'TEST_ERROR'
26
+ throw error
27
+ })
28
+ app.get('/unknown_error', (req, res, next) => {
29
+ throw new Error('Unknown Error')
30
+ })
31
+
32
+ app.get('/json_error', (req, res, next) => {
33
+ const error = new Error('JSON Error')
34
+ error.code = 'JSON_ERROR'
35
+ error.json = () => {
36
+ return {
37
+ code: error.code,
38
+ message: error.message
39
+ }
40
+ }
41
+ throw error
42
+ })
43
+
44
+ app.get('/error_no_code', (req, res, next) => {
45
+ throw new Error('Unknown Error')
46
+ })
47
+
48
+ app.get('/no_error', (req, res, next) => {
49
+ res.status(200).send('No Error')
50
+ })
51
+
52
+ app.get('/no_login', (req, res, next) => {
53
+ sendError(req, res, {
54
+ json: () => {
55
+ return {
56
+ code: 'LOGIN_EXPIRED',
57
+ message: 'Login expired'
58
+ }
59
+ }
60
+ })
61
+ })
62
+
63
+ app.get('/no_permission', (req, res, next) => {
64
+ sendError(req, res, {
65
+ json: () => {
66
+ return {
67
+ code: 'PERMISSION_DENIED',
68
+ message: 'Permission denied'
69
+ }
70
+ }
71
+ })
72
+ })
73
+
74
+ app.use((err, req, res, next) => {
75
+ sendError(req, res, err)
76
+ })
77
+
78
+ app.get('*', (req, res, next) => {
79
+ res.status(404).send('Not Found')
80
+ })
81
+
82
+ await new Promise(resolve => {
83
+ server = http.createServer(app).listen(() => {
84
+ const { port } = server.address()
85
+ baseURL = `http://localhost:${port}`
86
+ resolve()
87
+ })
88
+ })
89
+ })
90
+
91
+ afterAll(done => {
92
+ server.close(done)
93
+ })
94
+
95
+ it('应该通过 HTML 返回错误信息', async () => {
96
+ let error
97
+ try {
98
+ await axios.get(`${baseURL}/error`, {
99
+ headers: {
100
+ Accept: 'text/html',
101
+ 'Accept-Language': 'en-US'
102
+ }
103
+ })
104
+ } catch (e) {
105
+ error = e
106
+ }
107
+ expect(error).toBeDefined()
108
+ expect(error.response.status).toBe(500)
109
+ expect(error.response.data).toContain(`<title>Interface error`)
110
+ expect(error.response.data).toContain(`Show more technical details`)
111
+
112
+ // 保存 HTML 文件到临时目录
113
+ const tempDir = `${tmpTroubleshooting}/errorMiddleware.html`
114
+ await fse.ensureDir(tmpTroubleshooting)
115
+ await fse.writeFile(tempDir, error.response.data)
116
+ })
117
+
118
+ it('应该通过 JSON 返回错误信息', async () => {
119
+ let error
120
+ try {
121
+ await axios.get(`${baseURL}/error`, {
122
+ headers: { Accept: 'application/json' }
123
+ })
124
+ } catch (e) {
125
+ error = e
126
+ }
127
+ expect(error).toBeDefined()
128
+ expect(error.response.status).toBe(500)
129
+ expect(error.response.data.code).toEqual('API_ERROR')
130
+
131
+ // 保存 HTML 文件到临时目录
132
+ const tempDir = `${tmpTroubleshooting}/errorMiddleware.json`
133
+ await fse.ensureDir(tmpTroubleshooting)
134
+ await fse.writeJson(tempDir, error.response.data)
135
+ })
136
+
137
+ it('国际化支持', async () => {
138
+ let error
139
+ try {
140
+ await axios.get(`${baseURL}/error`, {
141
+ headers: {
142
+ Accept: 'text/html',
143
+ 'Accept-Language': 'zh-CN'
144
+ }
145
+ })
146
+ } catch (e) {
147
+ error = e
148
+ }
149
+ expect(error).toBeDefined()
150
+ expect(error.response.status).toBe(500)
151
+ expect(error.response.data).toContain(`<title>接口异常`)
152
+ expect(error.response.data).toContain(`显示更多技术细节`)
153
+
154
+ // 保存 HTML 文件到临时目录
155
+ const tempDir = `${tmpTroubleshooting}/errorMiddleware_zh-CN.html`
156
+ await fse.ensureDir(tmpTroubleshooting)
157
+ await fse.writeFile(tempDir, error.response.data)
158
+ })
159
+
160
+ it('应该通过 JSON 返回自定义错误信息', async () => {
161
+ let error
162
+ try {
163
+ await axios.get(`${baseURL}/json_error`, {
164
+ headers: { Accept: 'application/json' }
165
+ })
166
+ } catch (e) {
167
+ error = e
168
+ }
169
+ expect(error).toBeDefined()
170
+ expect(error.response.status).toBe(500)
171
+ expect(error.response.data).toEqual({
172
+ code: 'JSON_ERROR',
173
+ message: 'JSON Error'
174
+ })
175
+
176
+ // 保存 JSON 文件到临时目录
177
+ const tempDir = `${tmpTroubleshooting}/errorMiddleware_json.json`
178
+ await fse.ensureDir(tmpTroubleshooting)
179
+ await fse.writeJson(tempDir, error.response.data)
180
+ })
181
+
182
+ it('应该返回 404 错误', async () => {
183
+ const response = await axios.get(`${baseURL}/not-found`, {
184
+ validateStatus: status => status === 404
185
+ })
186
+ expect(response.status).toBe(404)
187
+ expect(response.data).toBe('Not Found')
188
+ })
189
+
190
+ it('应该返回 200 状态码', async () => {
191
+ const response = await axios.get(`${baseURL}/no_error`)
192
+ expect(response.status).toBe(200)
193
+ expect(response.data).toBe('No Error')
194
+ })
195
+
196
+ it('未知错误', async () => {
197
+ let error
198
+ try {
199
+ await axios.get(`${baseURL}/error_no_code`, {
200
+ headers: {
201
+ Accept: 'application/json',
202
+ 'Accept-Language': 'zh-CN'
203
+ }
204
+ })
205
+ } catch (e) {
206
+ error = e
207
+ }
208
+ expect(error).toBeDefined()
209
+ expect(error.response.status).toBe(500)
210
+ expect(error.response.data.message).toContain('接口异常')
211
+
212
+ // 保存 JSON 文件到临时目录
213
+ const tempDir = `${tmpTroubleshooting}/errorMiddleware_no_code.json`
214
+ await fse.ensureDir(tmpTroubleshooting)
215
+ await fse.writeJson(tempDir, error.response.data)
216
+ })
217
+
218
+ it('应该返回 401 状态码,当错误代码为 LOGIN_EXPIRED', async () => {
219
+ let error
220
+ try {
221
+ await axios.get(`${baseURL}/no_login`, {
222
+ headers: {
223
+ Accept: 'application/json',
224
+ 'Accept-Language': 'en-US'
225
+ }
226
+ })
227
+ } catch (e) {
228
+ error = e
229
+ }
230
+ expect(error).toBeDefined()
231
+ expect(error.response.status).toBe(401)
232
+ expect(error.response.data.code).toEqual('LOGIN_EXPIRED')
233
+ })
234
+
235
+ it('应该返回 403 状态码,当错误代码为 PERMISSION_DENIED', async () => {
236
+ let error
237
+ try {
238
+ await axios.get(`${baseURL}/no_permission`, {
239
+ headers: {
240
+ Accept: 'application/json',
241
+ 'Accept-Language': 'en-US'
242
+ }
243
+ })
244
+ } catch (e) {
245
+ error = e
246
+ }
247
+ expect(error).toBeDefined()
248
+ expect(error.response.status).toBe(403)
249
+ expect(error.response.data.code).toEqual('PERMISSION_DENIED')
250
+ })
251
+ })
@@ -0,0 +1,26 @@
1
+ import sendResponse from './sendResponse.js'
2
+ export default (req, res) => {
3
+ const apiNamespace = req.namespace('SUMOR_API')
4
+
5
+ const name = apiNamespace.i18n('API_NOT_FOUND')
6
+ const detail = apiNamespace.i18n('API_NOT_FOUND_DETAIL')
7
+
8
+ const data = {
9
+ svg: 'undraw_alert',
10
+ status: 404,
11
+ json: {
12
+ code: 'API_NOT_FOUND',
13
+ message: detail
14
+ },
15
+ html: {
16
+ title: name,
17
+ major: name,
18
+ minor: detail,
19
+ showMore: true,
20
+ moreButton: '',
21
+ moreContent: ''
22
+ }
23
+ }
24
+
25
+ sendResponse(req, res, data)
26
+ }
@@ -0,0 +1,25 @@
1
+ import alertPage from '../../../alertPage/index.js'
2
+
3
+ export default function sendResponse(req, res, data) {
4
+ const accept = req.headers.accept
5
+ const isJson = accept && accept.indexOf('application/json') !== -1
6
+
7
+ if (isJson) {
8
+ res.set('Content-Type', 'application/json;charset=utf-8')
9
+ res.status(data.status).json(data.json)
10
+ } else {
11
+ res.set('Content-Type', 'text/html;charset=utf-8')
12
+ res.status(data.status).send(
13
+ alertPage({
14
+ svg: data.svg,
15
+ status: data.status,
16
+ title: data.html.title,
17
+ major: data.html.major,
18
+ minor: data.html.minor,
19
+ showMore: data.html.showMore,
20
+ moreButton: data.html.moreButton,
21
+ moreContent: data.html.moreContent
22
+ })
23
+ )
24
+ }
25
+ }
@@ -0,0 +1,30 @@
1
+ import sendResponse from './sendResponse.js'
2
+ export default (req, res, result) => {
3
+ const apiNamespace = req.namespace('SUMOR_API')
4
+
5
+ const name = apiNamespace.i18n('API_CALL_DEFAULT_TITLE')
6
+
7
+ let moreContent = result
8
+ if (typeof result !== 'string') {
9
+ moreContent = JSON.stringify(result, null, 4)
10
+ }
11
+
12
+ const data = {
13
+ svg: 'undraw_celebration', // 'undraw_data-processing',
14
+ status: 200,
15
+ json: {
16
+ code: 'OK',
17
+ data: result
18
+ },
19
+ html: {
20
+ title: name,
21
+ major: name,
22
+ minor: '',
23
+ showMore: false,
24
+ moreButton: apiNamespace.i18n('API_CALL_SHOW_MORE'),
25
+ moreContent
26
+ }
27
+ }
28
+
29
+ sendResponse(req, res, data)
30
+ }
@@ -0,0 +1,14 @@
1
+ import fse from 'fs-extra'
2
+
3
+ const cleanupFiles = async files => {
4
+ if (!files) return
5
+
6
+ for (const fileField in files) {
7
+ const fileArray = files[fileField]
8
+ for (const file of fileArray) {
9
+ await fse.remove(file.path)
10
+ }
11
+ }
12
+ }
13
+
14
+ export default cleanupFiles
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, beforeAll, afterEach, afterAll } from '@jest/globals'
2
+ import cleanupFiles from './cleanupFiles.js'
3
+ import fse from 'fs-extra'
4
+ import path from 'path'
5
+ import tmp from '../../../test-utils/tmp.js'
6
+
7
+ const tempDir = await tmp('bodyMiddlewareCleanupFiles')
8
+ describe('cleanupFiles', () => {
9
+ beforeAll(async () => {
10
+ await fse.ensureDir(tempDir)
11
+ })
12
+
13
+ afterEach(async () => {
14
+ // Ensure temp directory is clean after each test
15
+ const files = await fse.readdir(tempDir)
16
+ await Promise.all(files.map(file => fse.remove(path.join(tempDir, file))))
17
+ })
18
+
19
+ afterAll(async () => {
20
+ // Remove temp directory after all tests
21
+ await fse.remove(tempDir)
22
+ })
23
+
24
+ it('should do nothing if files is undefined', async () => {
25
+ await cleanupFiles(undefined)
26
+ const files = await fse.readdir(tempDir)
27
+ expect(files.length).toBe(0)
28
+ })
29
+
30
+ it('should remove all files in the files object', async () => {
31
+ const filePaths = [
32
+ path.join(tempDir, 'file1.txt'),
33
+ path.join(tempDir, 'file2.txt'),
34
+ path.join(tempDir, 'file3.txt')
35
+ ]
36
+
37
+ // Create temporary files
38
+ await Promise.all(filePaths.map(filePath => fse.writeFile(filePath, 'test content')))
39
+
40
+ const files = {
41
+ field1: [{ path: filePaths[0] }, { path: filePaths[1] }],
42
+ field2: [{ path: filePaths[2] }]
43
+ }
44
+
45
+ await cleanupFiles(files)
46
+
47
+ // Assert files are removed
48
+ await Promise.all(
49
+ filePaths.map(async filePath => {
50
+ expect(await fse.exists(filePath)).toBe(false)
51
+ })
52
+ )
53
+ })
54
+ })