@mimik/api-helper 2.0.6 → 2.0.8
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/.claude/settings.local.json +9 -0
- package/.husky/pre-commit +2 -0
- package/.husky/pre-push +2 -0
- package/README.md +57 -63
- package/eslint.config.js +30 -11
- package/index.js +113 -117
- package/lib/ajvHelpers.js +1 -1
- package/lib/baseHandlers.js +2 -3
- package/lib/common.js +1 -1
- package/lib/oauthValidation-helper.js +1 -1
- package/lib/securityHandlers.js +1 -1
- package/package.json +24 -23
- package/test/ajvHelpers.test.js +159 -0
- package/test/baseHandlers.test.js +150 -0
- package/test/extract-helper.test.js +100 -0
- package/test/index-async.test.js +599 -0
- package/test/index-sync.test.js +282 -0
- package/test/oauthValidation-helper.test.js +136 -0
- package/test/securityHandlers.test.js +557 -0
- package/.nycrc +0 -4
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import esmock from 'esmock';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
|
|
4
|
+
let ajvFormats;
|
|
5
|
+
let addedLibFormats;
|
|
6
|
+
let addedCustomFormats;
|
|
7
|
+
|
|
8
|
+
describe('ajvHelpers', () => {
|
|
9
|
+
before(async () => {
|
|
10
|
+
const mod = await esmock('../lib/ajvHelpers.js', {
|
|
11
|
+
'ajv-formats': {
|
|
12
|
+
default: (ajv, formats) => {
|
|
13
|
+
addedLibFormats = formats;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
({ ajvFormats } = mod);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
addedLibFormats = null;
|
|
23
|
+
addedCustomFormats = {};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const createMockAjv = () => ({
|
|
27
|
+
addFormat: (name, format) => {
|
|
28
|
+
addedCustomFormats[name] = format;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('with no extra formats', () => {
|
|
33
|
+
it('should add default library formats', () => {
|
|
34
|
+
const ajv = createMockAjv();
|
|
35
|
+
const configurer = ajvFormats();
|
|
36
|
+
|
|
37
|
+
configurer(ajv);
|
|
38
|
+
expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should add built-in semver and ip custom formats', () => {
|
|
42
|
+
const ajv = createMockAjv();
|
|
43
|
+
const configurer = ajvFormats();
|
|
44
|
+
|
|
45
|
+
configurer(ajv);
|
|
46
|
+
expect(addedCustomFormats).to.have.property('semver');
|
|
47
|
+
expect(addedCustomFormats.semver.type).to.equal('string');
|
|
48
|
+
expect(addedCustomFormats).to.have.property('ip');
|
|
49
|
+
expect(addedCustomFormats.ip.type).to.equal('string');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return the ajv instance', () => {
|
|
53
|
+
const ajv = createMockAjv();
|
|
54
|
+
const configurer = ajvFormats();
|
|
55
|
+
const result = configurer(ajv);
|
|
56
|
+
|
|
57
|
+
expect(result).to.equal(ajv);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('with null extra formats', () => {
|
|
62
|
+
it('should treat null as no extra formats', () => {
|
|
63
|
+
const ajv = createMockAjv();
|
|
64
|
+
const configurer = ajvFormats(null);
|
|
65
|
+
|
|
66
|
+
configurer(ajv);
|
|
67
|
+
expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('with extra library formats (string-only)', () => {
|
|
72
|
+
it('should append format names to the library formats list', () => {
|
|
73
|
+
const ajv = createMockAjv();
|
|
74
|
+
const configurer = ajvFormats({ hostname: {} });
|
|
75
|
+
|
|
76
|
+
configurer(ajv);
|
|
77
|
+
expect(addedLibFormats).to.include('hostname');
|
|
78
|
+
expect(addedLibFormats).to.have.lengthOf(10);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('with extra custom formats (with type)', () => {
|
|
83
|
+
it('should add custom format via ajv.addFormat', () => {
|
|
84
|
+
const customFormat = { type: 'string', validate: /^[A-Z]+$/u };
|
|
85
|
+
const ajv = createMockAjv();
|
|
86
|
+
const configurer = ajvFormats({ uppercase: customFormat });
|
|
87
|
+
|
|
88
|
+
configurer(ajv);
|
|
89
|
+
expect(addedCustomFormats).to.have.property('uppercase');
|
|
90
|
+
expect(addedCustomFormats.uppercase).to.equal(customFormat);
|
|
91
|
+
expect(addedLibFormats).to.not.include('uppercase');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('semver validation', () => {
|
|
96
|
+
it('should accept valid semver strings', () => {
|
|
97
|
+
const ajv = createMockAjv();
|
|
98
|
+
|
|
99
|
+
ajvFormats()(ajv);
|
|
100
|
+
const { validate } = addedCustomFormats.semver;
|
|
101
|
+
|
|
102
|
+
expect(validate.test('1.0.0')).to.equal(true);
|
|
103
|
+
expect(validate.test('0.1.0')).to.equal(true);
|
|
104
|
+
expect(validate.test('1.2.3-alpha.1')).to.equal(true);
|
|
105
|
+
expect(validate.test('1.2.3+build.123')).to.equal(true);
|
|
106
|
+
expect(validate.test('1.2.3-beta+build')).to.equal(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should reject invalid semver strings', () => {
|
|
110
|
+
const ajv = createMockAjv();
|
|
111
|
+
|
|
112
|
+
ajvFormats()(ajv);
|
|
113
|
+
const { validate } = addedCustomFormats.semver;
|
|
114
|
+
|
|
115
|
+
expect(validate.test('1.0')).to.equal(false);
|
|
116
|
+
expect(validate.test('abc')).to.equal(false);
|
|
117
|
+
expect(validate.test('1.0.0.0')).to.equal(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('ip validation', () => {
|
|
122
|
+
it('should accept valid IPv4 addresses', () => {
|
|
123
|
+
const ajv = createMockAjv();
|
|
124
|
+
|
|
125
|
+
ajvFormats()(ajv);
|
|
126
|
+
const { validate } = addedCustomFormats.ip;
|
|
127
|
+
|
|
128
|
+
expect(validate.test('192.168.1.1')).to.equal(true);
|
|
129
|
+
expect(validate.test('10.0.0.1')).to.equal(true);
|
|
130
|
+
expect(validate.test('255.255.255.255')).to.equal(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should accept valid IPv6 addresses', () => {
|
|
134
|
+
const ajv = createMockAjv();
|
|
135
|
+
|
|
136
|
+
ajvFormats()(ajv);
|
|
137
|
+
const { validate } = addedCustomFormats.ip;
|
|
138
|
+
|
|
139
|
+
expect(validate.test('::1')).to.equal(true);
|
|
140
|
+
expect(validate.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.equal(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('DEFAULT_FORMATS isolation', () => {
|
|
145
|
+
it('should not mutate DEFAULT_FORMATS between calls', () => {
|
|
146
|
+
const ajv1 = createMockAjv();
|
|
147
|
+
const ajv2 = createMockAjv();
|
|
148
|
+
|
|
149
|
+
ajvFormats({ extra1: {} })(ajv1);
|
|
150
|
+
const firstCallLength = addedLibFormats.length;
|
|
151
|
+
|
|
152
|
+
ajvFormats()(ajv2);
|
|
153
|
+
const secondCallLength = addedLibFormats.length;
|
|
154
|
+
|
|
155
|
+
expect(secondCallLength).to.equal(9);
|
|
156
|
+
expect(firstCallLength).to.equal(10);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ERROR_CODE } from '@mimik/response-helper';
|
|
2
|
+
import esmock from 'esmock';
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
|
|
5
|
+
let baseHandlers;
|
|
6
|
+
let lastRejectArgs;
|
|
7
|
+
|
|
8
|
+
describe('baseHandlers', () => {
|
|
9
|
+
before(async () => {
|
|
10
|
+
const mod = await esmock('../lib/baseHandlers.js', {
|
|
11
|
+
'@mimik/swagger-helper': {
|
|
12
|
+
rejectRequest: (error, con, res) => {
|
|
13
|
+
lastRejectArgs = { error, con, res };
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
baseHandlers = mod.default;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
lastRejectArgs = null;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('validationFail', () => {
|
|
26
|
+
it('should create error with status 400 and call rejectRequest', () => {
|
|
27
|
+
const con = { validation: { errors: [{ message: 'bad field' }], warnings: ['warn1'] } };
|
|
28
|
+
const req = { method: 'POST', url: '/test' };
|
|
29
|
+
const res = {};
|
|
30
|
+
|
|
31
|
+
baseHandlers.validationFail(con, req, res);
|
|
32
|
+
expect(lastRejectArgs.error.message).to.equal('Failed schema validation');
|
|
33
|
+
expect(lastRejectArgs.error.statusCode).to.equal(ERROR_CODE.PARAMETER);
|
|
34
|
+
expect(lastRejectArgs.error.info).to.deep.equal({
|
|
35
|
+
method: 'POST',
|
|
36
|
+
path: '/test',
|
|
37
|
+
errors: [{ message: 'bad field' }],
|
|
38
|
+
warnings: ['warn1'],
|
|
39
|
+
});
|
|
40
|
+
expect(lastRejectArgs.con).to.equal(con);
|
|
41
|
+
expect(lastRejectArgs.res).to.equal(res);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should default warnings to empty array when not present', () => {
|
|
45
|
+
const con = { validation: { errors: [] } };
|
|
46
|
+
const req = { method: 'GET', url: '/path' };
|
|
47
|
+
const res = {};
|
|
48
|
+
|
|
49
|
+
baseHandlers.validationFail(con, req, res);
|
|
50
|
+
expect(lastRejectArgs.error.info.warnings).to.deep.equal([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('notFound', () => {
|
|
55
|
+
it('should create error with status 404 and the path', () => {
|
|
56
|
+
const con = {};
|
|
57
|
+
const req = { method: 'GET', url: '/unknown' };
|
|
58
|
+
const res = {};
|
|
59
|
+
|
|
60
|
+
baseHandlers.notFound(con, req, res);
|
|
61
|
+
expect(lastRejectArgs.error.message).to.equal('path /unknown not defined in Swagger specification');
|
|
62
|
+
expect(lastRejectArgs.error.statusCode).to.equal(ERROR_CODE.NOT_FOUND);
|
|
63
|
+
expect(lastRejectArgs.error.info).to.deep.equal({ method: 'GET', path: '/unknown' });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('unauthorizedHandler', () => {
|
|
68
|
+
it('should use default Unauthorized error when no scheme error exists', () => {
|
|
69
|
+
const con = { security: { authorized: true, SystemSecurity: {} } };
|
|
70
|
+
const req = {};
|
|
71
|
+
const res = {};
|
|
72
|
+
|
|
73
|
+
baseHandlers.unauthorizedHandler(con, req, res);
|
|
74
|
+
expect(lastRejectArgs.error.message).to.equal('Unauthorized');
|
|
75
|
+
expect(lastRejectArgs.error.statusCode).to.equal(ERROR_CODE.UNAUTHORIZED);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should use scheme error when present', () => {
|
|
79
|
+
const schemeError = new Error('token expired');
|
|
80
|
+
|
|
81
|
+
schemeError.statusCode = ERROR_CODE.FORBIDDEN;
|
|
82
|
+
const con = { security: { authorized: false, SystemSecurity: { error: schemeError } } };
|
|
83
|
+
const req = {};
|
|
84
|
+
const res = {};
|
|
85
|
+
|
|
86
|
+
baseHandlers.unauthorizedHandler(con, req, res);
|
|
87
|
+
expect(lastRejectArgs.error).to.equal(schemeError);
|
|
88
|
+
expect(lastRejectArgs.error.message).to.equal('token expired');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should filter out the authorized key from security schemes', () => {
|
|
92
|
+
const schemeError = new Error('forbidden');
|
|
93
|
+
const con = { security: { authorized: true, AdminSecurity: { error: schemeError } } };
|
|
94
|
+
const req = {};
|
|
95
|
+
const res = {};
|
|
96
|
+
|
|
97
|
+
baseHandlers.unauthorizedHandler(con, req, res);
|
|
98
|
+
expect(lastRejectArgs.error).to.equal(schemeError);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle scheme with no error property', () => {
|
|
102
|
+
const con = { security: { authorized: false, SystemSecurity: null } };
|
|
103
|
+
const req = {};
|
|
104
|
+
const res = {};
|
|
105
|
+
|
|
106
|
+
baseHandlers.unauthorizedHandler(con, req, res);
|
|
107
|
+
expect(lastRejectArgs.error.message).to.equal('Unauthorized');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('notImplemented', () => {
|
|
112
|
+
it('should create error with status 501 and include operationId', () => {
|
|
113
|
+
const con = { operation: { operationId: 'getUsers' } };
|
|
114
|
+
const req = { method: 'GET', url: '/users' };
|
|
115
|
+
const res = {};
|
|
116
|
+
|
|
117
|
+
baseHandlers.notImplemented(con, req, res);
|
|
118
|
+
expect(lastRejectArgs.error.message).to.equal('GET /users defined in Swagger specification, but not implemented');
|
|
119
|
+
expect(lastRejectArgs.error.statusCode).to.equal(ERROR_CODE.NOT_IMPLEMENTED);
|
|
120
|
+
expect(lastRejectArgs.error.info).to.deep.equal({
|
|
121
|
+
method: 'GET',
|
|
122
|
+
path: '/users',
|
|
123
|
+
operationId: 'getUsers',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('methodNotAllowed', () => {
|
|
129
|
+
it('should create error with status 501 for disallowed method', () => {
|
|
130
|
+
const con = {};
|
|
131
|
+
const req = { method: 'DELETE', url: '/items' };
|
|
132
|
+
const res = {};
|
|
133
|
+
|
|
134
|
+
baseHandlers.methodNotAllowed(con, req, res);
|
|
135
|
+
expect(lastRejectArgs.error.message).to.equal('path /items defined in Swagger specification, but the method DELETE is not defined');
|
|
136
|
+
expect(lastRejectArgs.error.statusCode).to.equal(ERROR_CODE.NOT_IMPLEMENTED);
|
|
137
|
+
expect(lastRejectArgs.error.info).to.deep.equal({ method: 'DELETE', path: '/items' });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('exports', () => {
|
|
142
|
+
it('should export all five handlers', () => {
|
|
143
|
+
expect(baseHandlers).to.have.property('validationFail').that.is.a('function');
|
|
144
|
+
expect(baseHandlers).to.have.property('notFound').that.is.a('function');
|
|
145
|
+
expect(baseHandlers).to.have.property('unauthorizedHandler').that.is.a('function');
|
|
146
|
+
expect(baseHandlers).to.have.property('notImplemented').that.is.a('function');
|
|
147
|
+
expect(baseHandlers).to.have.property('methodNotAllowed').that.is.a('function');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import esmock from 'esmock';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
const getRichError = (type, message, info, cause) => {
|
|
8
|
+
const error = new Error(message);
|
|
9
|
+
|
|
10
|
+
error.type = type;
|
|
11
|
+
error.info = info;
|
|
12
|
+
if (cause) error.cause = cause;
|
|
13
|
+
return error;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const logEntries = [];
|
|
17
|
+
const mockLogger = {
|
|
18
|
+
info: (msg, ...args) => logEntries.push({ level: 'info', msg, args }),
|
|
19
|
+
debug: (msg, ...args) => logEntries.push({ level: 'debug', msg, args }),
|
|
20
|
+
warn: (msg, ...args) => logEntries.push({ level: 'warn', msg, args }),
|
|
21
|
+
error: (msg, ...args) => logEntries.push({ level: 'error', msg, args }),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let saveProperties;
|
|
25
|
+
let tmpDir;
|
|
26
|
+
|
|
27
|
+
describe('extract-helper', () => {
|
|
28
|
+
before(async () => {
|
|
29
|
+
const mod = await esmock('../lib/extract-helper.js', {
|
|
30
|
+
'@mimik/response-helper': { getRichError },
|
|
31
|
+
'@mimik/sumologic-winston-logger': mockLogger,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
({ saveProperties } = mod);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'extract-helper-test-'));
|
|
39
|
+
logEntries.length = 0;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create the build directory if it does not exist', () => {
|
|
47
|
+
const buildDir = path.join(tmpDir, 'build');
|
|
48
|
+
|
|
49
|
+
saveProperties({}, buildDir, 'controllers', 'corr-1');
|
|
50
|
+
expect(fs.existsSync(buildDir)).to.equal(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should create register.js with correct imports and exports', () => {
|
|
54
|
+
const extractResult = {
|
|
55
|
+
userController: ['getUser', 'createUser'],
|
|
56
|
+
adminController: ['getAdmin'],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
saveProperties(extractResult, tmpDir, 'controllers', 'corr-2');
|
|
60
|
+
const registerFile = path.join(tmpDir, 'register.js');
|
|
61
|
+
|
|
62
|
+
expect(fs.existsSync(registerFile)).to.equal(true);
|
|
63
|
+
const content = fs.readFileSync(registerFile, 'utf8');
|
|
64
|
+
|
|
65
|
+
expect(content).to.include('import {\n getUser,\n createUser,\n} from \'../controllers/userController.js\';');
|
|
66
|
+
expect(content).to.include('import {\n getAdmin,\n} from \'../controllers/adminController.js\';');
|
|
67
|
+
expect(content).to.include('export {\n getUser,\n createUser,\n getAdmin,\n};');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should include eslint disable comment at the top', () => {
|
|
71
|
+
saveProperties({}, tmpDir, 'controllers', 'corr-3');
|
|
72
|
+
const content = fs.readFileSync(path.join(tmpDir, 'register.js'), 'utf8');
|
|
73
|
+
|
|
74
|
+
expect(content).to.match(/^\/\* eslint sort-imports:"off" \*\//u);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should remove existing register.js before writing', () => {
|
|
78
|
+
const registerFile = path.join(tmpDir, 'register.js');
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(registerFile, 'old content');
|
|
81
|
+
saveProperties({ ctrl: ['op1'] }, tmpDir, 'controllers', 'corr-4');
|
|
82
|
+
const content = fs.readFileSync(registerFile, 'utf8');
|
|
83
|
+
|
|
84
|
+
expect(content).to.not.include('old content');
|
|
85
|
+
expect(content).to.include('op1');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle empty extractResult', () => {
|
|
89
|
+
saveProperties({}, tmpDir, 'controllers', 'corr-5');
|
|
90
|
+
const content = fs.readFileSync(path.join(tmpDir, 'register.js'), 'utf8');
|
|
91
|
+
|
|
92
|
+
expect(content).to.include('export {\n};');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw when build directory cannot be created', () => {
|
|
96
|
+
const badPath = '/nonexistent/deeply/nested/path';
|
|
97
|
+
|
|
98
|
+
expect(() => saveProperties({}, badPath, 'controllers', 'corr-6')).to.throw('file system error');
|
|
99
|
+
});
|
|
100
|
+
});
|