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.
- package/.eslintignore +6 -0
- package/.eslintrc.yml +4 -0
- package/.gitattributes +1 -0
- package/.github/workflows/audit.yml +22 -0
- package/.github/workflows/available.yml +22 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/coverage.yml +23 -0
- package/.github/workflows/publish.yml +31 -0
- package/.github/workflows/ut.yml +24 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +10 -0
- package/.prettierrc.yml +7 -0
- package/LICENSE +21 -0
- package/demo/api/error.js +3 -0
- package/demo/api/file.js +4 -0
- package/demo/api/file.yaml +11 -0
- package/demo/api/hello.js +4 -0
- package/demo/api/hello.yaml +13 -0
- package/demo/api/login.js +4 -0
- package/demo/api/logout.js +4 -0
- package/demo/api/long.js +9 -0
- package/demo/api/long.yaml +2 -0
- package/demo/api/user/admin.js +12 -0
- package/demo/api/user/info.js +8 -0
- package/demo/config/config.yaml +2 -0
- package/demo/i18n/demo.yaml +1 -0
- package/demo/package.json +6 -0
- package/i18n/README.md +32 -0
- package/i18n/sumor_api.yaml +16 -0
- package/i18n/sumor_app.yaml +3 -0
- package/i18n/sumor_internal.yaml +1 -0
- package/i18n/sumor_token.yaml +2 -0
- package/i18nExt/sumor_api.de.yaml +3 -0
- package/i18nExt/sumor_api.en.yaml +3 -0
- package/i18nExt/sumor_api.es.yaml +3 -0
- package/i18nExt/sumor_api.fr.yaml +3 -0
- package/i18nExt/sumor_api.it.yaml +3 -0
- package/i18nExt/sumor_api.ja.yaml +3 -0
- package/i18nExt/sumor_api.ko.yaml +3 -0
- package/i18nExt/sumor_api.pt.yaml +3 -0
- package/i18nExt/sumor_api.zh-TW.yaml +3 -0
- package/i18nExt/sumor_api.zh.yaml +3 -0
- package/i18nExt/sumor_app.de.yaml +3 -0
- package/i18nExt/sumor_app.en.yaml +3 -0
- package/i18nExt/sumor_app.es.yaml +3 -0
- package/i18nExt/sumor_app.zh-TW.yaml +3 -0
- package/i18nExt/sumor_app.zh.yaml +3 -0
- package/i18nExt/sumor_internal.de.yaml +1 -0
- package/i18nExt/sumor_internal.en.yaml +1 -0
- package/i18nExt/sumor_internal.es.yaml +1 -0
- package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
- package/i18nExt/sumor_internal.zh.yaml +1 -0
- package/i18nExt/sumor_token.de.yaml +2 -0
- package/i18nExt/sumor_token.es.yaml +2 -0
- package/i18nExt/sumor_token.fr.yaml +2 -0
- package/i18nExt/sumor_token.it.yaml +2 -0
- package/i18nExt/sumor_token.ja.yaml +2 -0
- package/i18nExt/sumor_token.ko.yaml +2 -0
- package/i18nExt/sumor_token.pt.yaml +2 -0
- package/i18nExt/sumor_token.ru.yaml +2 -0
- package/i18nExt/sumor_token.tr.yaml +2 -0
- package/i18nExt/sumor_token.zh-TW.yaml +2 -0
- package/i18nExt/sumor_token.zh.yaml +2 -0
- package/jest.config.json +26 -0
- package/modules/alertPage/html/template.html +162 -0
- package/modules/alertPage/html/undraw_access-denied.svg +52 -0
- package/modules/alertPage/html/undraw_alert.svg +1 -0
- package/modules/alertPage/html/undraw_celebration.svg +78 -0
- package/modules/alertPage/html/undraw_complete-form.svg +1 -0
- package/modules/alertPage/html/undraw_login.svg +1 -0
- package/modules/alertPage/index.js +46 -0
- package/modules/alertPage/index.test.js +12 -0
- package/modules/i18n/README.md +90 -0
- package/modules/i18n/convertI18nValue/README.md +31 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
- package/modules/i18n/convertI18nValue/index.js +14 -0
- package/modules/i18n/convertI18nValue/index.test.js +55 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
- package/modules/i18n/index.js +26 -0
- package/modules/i18n/index.test.js +13 -0
- package/modules/i18n/load/README.md +28 -0
- package/modules/i18n/load/load.js +31 -0
- package/modules/i18n/load/load.test.js +30 -0
- package/modules/i18n/registry.js +48 -0
- package/modules/i18n/registry.test.js +84 -0
- package/modules/logger/convert/parseFile.js +14 -0
- package/modules/logger/convert/parseFile.test.js +28 -0
- package/modules/logger/convert/stringifyCMD.js +48 -0
- package/modules/logger/convert/stringifyCMD.test.js +37 -0
- package/modules/logger/convert/stringifyFile.js +24 -0
- package/modules/logger/convert/stringifyFile.test.js +79 -0
- package/modules/logger/index.js +82 -0
- package/modules/logger/index.test.js +124 -0
- package/modules/logger/logFileOperator.js +50 -0
- package/modules/logger/logFileOperator.test.js +69 -0
- package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
- package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
- package/modules/middlewares/apiMiddleware/index.js +111 -0
- package/modules/middlewares/apiMiddleware/index.test.js +145 -0
- package/modules/middlewares/apiMiddleware/load/index.js +35 -0
- package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
- package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
- package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
- package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
- package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
- package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
- package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
- package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
- package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
- package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
- package/modules/middlewares/bodyMiddleware/index.js +12 -0
- package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
- package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
- package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
- package/modules/middlewares/i18nMiddleware/index.js +82 -0
- package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
- package/modules/middlewares/tokenMiddleware/Token.js +115 -0
- package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
- package/modules/middlewares/tokenMiddleware/index.js +32 -0
- package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
- package/modules/serve/formatConfig.js +7 -0
- package/modules/serve/formatConfig.test.js +28 -0
- package/modules/serve/index.js +30 -0
- package/modules/serve/index.test.js +69 -0
- package/modules/serve/listenApp.js +11 -0
- package/modules/system/getSystemLanguage.js +9 -0
- package/modules/system/getSystemLanguage.test.js +19 -0
- package/modules/utils/getError.js +56 -0
- package/modules/utils/getError.test.js +97 -0
- package/modules/utils/loadConfig.js +73 -0
- package/modules/utils/loadConfig.test.js +129 -0
- package/modules/utils/pathUtils.js +43 -0
- package/modules/utils/pathUtils.test.js +52 -0
- package/modules/utils/type.js +14 -0
- package/modules/utils/type.test.js +40 -0
- package/package.json +61 -1
- package/src/app.js +40 -0
- package/src/cli.js +28 -0
- package/src/index.js +3 -0
- package/test-utils/codeTest/README.md +4 -0
- package/test-utils/codeTest/custom.css +47 -0
- package/test-utils/codeTest/index.js +80 -0
- package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
- package/test-utils/codeTest/utils/getTestFiles.js +20 -0
- package/test-utils/codeTest/utils/runTests.js +38 -0
- package/test-utils/codeTest/utils/startServer.js +23 -0
- package/test-utils/tmp.js +15 -0
- package/cli.js +0 -3
- package/index.es.js +0 -578
- package/template/web/AppWithFrame.vue +0 -20
- package/template/web/index.html +0 -19
- package/template/web/src/App.vue +0 -15
- package/template/web/src/entry-client.js +0 -9
- package/template/web/src/entry-server.js +0 -69
- package/template/web/src/main.js +0 -41
- 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
|
+
}
|
|
Binary file
|
|
@@ -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
|
+
})
|