@mimik/api-helper 3.0.0 → 3.0.2

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.
@@ -36,14 +36,7 @@ const SCOPE_INDEX = 0;
36
36
  const CLAIMS_INDEX = 1;
37
37
  const RESOURCE_INDEX = 0;
38
38
 
39
- const getScopes = (conf, securityType) => {
40
- let scopes = [];
41
-
42
- conf.operation.security.forEach((security) => {
43
- if (security[securityType]) scopes = scopes.concat(security[securityType]);
44
- });
45
- return scopes;
46
- };
39
+ const getScopes = (conf, securityType) => conf.operation.security.flatMap(security => security[securityType] || []);
47
40
 
48
41
  const getError = (message, statusCode) => {
49
42
  const error = new Error(message);
@@ -53,15 +46,8 @@ const getError = (message, statusCode) => {
53
46
  };
54
47
 
55
48
  const checkToken = (authToken) => {
56
- let token;
49
+ const token = jwt.decode(authToken);
57
50
 
58
- try {
59
- token = jwt.decode(authToken);
60
- }
61
- catch (err) {
62
- err.statusCode = UNAUTHORIZED_ERROR;
63
- throw err;
64
- }
65
51
  if (!token) {
66
52
  throw getError('invalid token', UNAUTHORIZED_ERROR);
67
53
  }
@@ -93,17 +79,17 @@ const checkScopes = (tokenScopes, defScopes, definition) => {
93
79
  if (!tokenScopes) {
94
80
  throw getError('no scope in authorization token', UNAUTHORIZED_ERROR);
95
81
  }
96
- let claims = [];
82
+ const claims = [];
97
83
  let onBehalf = false;
98
84
 
99
85
  if (defScopes && defScopes.length !== EMPTY) {
100
86
  const currentScopes = tokenScopes.split(SCOPES_SEPARATOR);
101
87
  const intersects = [];
102
- let resourceIndex = FIRST;
103
88
 
104
89
  currentScopes.forEach((currentScope) => {
105
90
  const analyzedScope = currentScope.split(SCOPE_CLAIMS_SEPARATOR);
106
91
  const analyzedResource = analyzedScope[SCOPE_INDEX].split(RESOURCE_SEPARATOR);
92
+ let resourceIndex = FIRST;
107
93
 
108
94
  if (analyzedResource[RESOURCE_INDEX] === ON_BEHALF) {
109
95
  onBehalf = true;
@@ -129,7 +115,7 @@ const checkScopes = (tokenScopes, defScopes, definition) => {
129
115
  if (claimsIntersects.length !== includedClaims.length) {
130
116
  throw getError(`incorrect claims included: ${includedClaims.filter(cla => !claimsIntersects.includes(cla))}`, FORBIDDEN_ERROR);
131
117
  }
132
- claims = claims.concat(claimsIntersects);
118
+ claims.push(...claimsIntersects);
133
119
  }
134
120
  intersects.push(analyzedScope[SCOPE_INDEX]);
135
121
  }
@@ -141,13 +127,26 @@ const checkScopes = (tokenScopes, defScopes, definition) => {
141
127
  return { onBehalf, claims };
142
128
  };
143
129
 
130
+ const setParam = (req, request, key, value) => {
131
+ req[key] = value;
132
+ request[key] = value;
133
+ };
134
+
135
+ const setParams = (req, request, params) => {
136
+ Object.keys(params).forEach(key => setParam(req, request, key, params[key]));
137
+ };
138
+
139
+ const createMockHandler = params => (con, req) => {
140
+ setParams(req, con.request, params);
141
+ return true;
142
+ };
143
+
144
144
  export const securityLib = (config) => {
145
145
  const verifyTokenClientCredentials = (authToken) => {
146
146
  const { server, generic } = config.security;
147
147
  const options = {
148
148
  audience: (generic.audience === NO_GENERIC) ? server.audience : generic.audience,
149
149
  issuer: server.issuer,
150
- // subject: `${config.serverSettings.id}@clients`,
151
150
  };
152
151
 
153
152
  try {
@@ -186,19 +185,24 @@ export const securityLib = (config) => {
186
185
  }
187
186
  };
188
187
 
188
+ const setClientParams = (req, request, token, scopeResult) => {
189
+ setParam(req, request, TOKEN_PARAMS.claims, scopeResult.claims);
190
+ if (scopeResult.onBehalf) setParam(req, request, TOKEN_PARAMS.onBehalf, true);
191
+ if (token.subType) setParam(req, request, TOKEN_PARAMS.tokenType, token.subType);
192
+ if (token.sub) setParam(req, request, TOKEN_PARAMS.clientId, token.sub);
193
+ if (token.cust) setParam(req, request, TOKEN_PARAMS.customer, token.cust);
194
+ };
195
+
189
196
  const AdminSecurity = {
190
197
  regular: (con, req) => {
191
198
  const authToken = checkHeaders(req.headers);
192
199
  const token = checkToken(authToken);
193
- const { request } = con;
194
200
 
195
201
  if (token.subType !== ADMIN && token.subType !== SUB_ADMIN) {
196
202
  throw getError('invalid token: wrong type', FORBIDDEN_ERROR);
197
203
  }
198
204
  if (token.subType === SUB_ADMIN) {
199
- if (!token.cust) {
200
- throw getError('invalid token: no customer', FORBIDDEN_ERROR);
201
- }
205
+ if (!token.cust) throw getError('invalid token: no customer', FORBIDDEN_ERROR);
202
206
  }
203
207
  else if (token.sub !== `${config.security.admin.externalId}${CLIENT}`) {
204
208
  throw getError(`jwt subject invalid: ${token.sub}`, FORBIDDEN_ERROR);
@@ -206,42 +210,21 @@ export const securityLib = (config) => {
206
210
  verifyTokenClientCredentials(authToken);
207
211
  const scopeResult = checkScopes(token.scope, getScopes(con, ADMIN_SECURITY), con.api.definition);
208
212
 
209
- req[TOKEN_PARAMS.claims] = scopeResult.claims;
210
- request[TOKEN_PARAMS.claims] = scopeResult.claims;
211
- if (token.subType) {
212
- req[TOKEN_PARAMS.tokenType] = token.subType;
213
- request[TOKEN_PARAMS.tokenType] = token.subType;
214
- }
215
- if (token.sub) {
216
- req[TOKEN_PARAMS.clientId] = token.sub;
217
- request[TOKEN_PARAMS.clientId] = token.sub;
218
- }
219
- if (token.cust) {
220
- req[TOKEN_PARAMS.customer] = token.cust;
221
- request[TOKEN_PARAMS.customer] = token.cust;
222
- }
223
- return true;
224
- },
225
- mock: (con, req) => {
226
- const { request } = con;
227
-
228
- req[TOKEN_PARAMS.claims] = ['dummyClaims'];
229
- req[TOKEN_PARAMS.tokenType] = ADMIN;
230
- req[TOKEN_PARAMS.clientId] = 'dummyClientId';
231
- req[TOKEN_PARAMS.customer] = 'dummyCustomer';
232
- request[TOKEN_PARAMS.claims] = ['dummyClaims'];
233
- request[TOKEN_PARAMS.tokenType] = ADMIN;
234
- request[TOKEN_PARAMS.clientId] = 'dummyClientId';
235
- request[TOKEN_PARAMS.customer] = 'dummyCustomer';
213
+ setClientParams(req, con.request, token, scopeResult);
236
214
  return true;
237
215
  },
216
+ mock: createMockHandler({
217
+ [TOKEN_PARAMS.claims]: ['dummyClaims'],
218
+ [TOKEN_PARAMS.tokenType]: ADMIN,
219
+ [TOKEN_PARAMS.clientId]: 'dummyClientId',
220
+ [TOKEN_PARAMS.customer]: 'dummyCustomer',
221
+ }),
238
222
  };
239
223
 
240
224
  const SystemSecurity = {
241
225
  regular: (con, req) => {
242
226
  const authToken = checkHeaders(req.headers);
243
227
  const token = checkToken(authToken);
244
- const { request } = con;
245
228
 
246
229
  if (token.subType === ADMIN || token.subType === SUB_ADMIN) {
247
230
  throw getError('invalid token: wrong type', FORBIDDEN_ERROR);
@@ -249,112 +232,56 @@ export const securityLib = (config) => {
249
232
  verifyTokenClientCredentials(authToken);
250
233
  const scopeResult = checkScopes(token.scope, getScopes(con, SYSTEM_SECURITY), con.api.definition);
251
234
 
252
- if (scopeResult.onBehalf) {
253
- req[TOKEN_PARAMS.onBehalf] = true;
254
- request[TOKEN_PARAMS.onBehalf] = true;
255
- }
256
- req[TOKEN_PARAMS.claims] = scopeResult.claims;
257
- request[TOKEN_PARAMS.claims] = scopeResult.claims;
258
- if (token.subType) {
259
- req[TOKEN_PARAMS.tokenType] = token.subType;
260
- request[TOKEN_PARAMS.tokenType] = token.subType;
261
- }
262
- if (token.sub) {
263
- req[TOKEN_PARAMS.clientId] = token.sub;
264
- request[TOKEN_PARAMS.clientId] = token.sub;
265
- }
266
- if (token.cust) {
267
- req[TOKEN_PARAMS.customer] = token.cust;
268
- request[TOKEN_PARAMS.customer] = token.cust;
269
- }
270
- if (token.type === CLUSTER) {
271
- req[TOKEN_PARAMS.cluster] = true;
272
- request[TOKEN_PARAMS.cluster] = true;
273
- }
274
- return true;
275
- },
276
- mock: (con, req) => {
277
- const { request } = con;
278
-
279
- req[TOKEN_PARAMS.claims] = ['dummyClaims'];
280
- req[TOKEN_PARAMS.tokenType] = 'dummyServiceType';
281
- req[TOKEN_PARAMS.clientId] = 'dummyClientId';
282
- req[TOKEN_PARAMS.customer] = 'dummyCustomer';
283
- request[TOKEN_PARAMS.claims] = ['dummyClaims'];
284
- request[TOKEN_PARAMS.tokenType] = 'dummyServiceType';
285
- request[TOKEN_PARAMS.clientId] = 'dummyClientId';
286
- request[TOKEN_PARAMS.customer] = 'dummyCustomer';
235
+ setClientParams(req, con.request, token, scopeResult);
236
+ if (token.type === CLUSTER) setParam(req, con.request, TOKEN_PARAMS.cluster, true);
287
237
  return true;
288
238
  },
239
+ mock: createMockHandler({
240
+ [TOKEN_PARAMS.claims]: ['dummyClaims'],
241
+ [TOKEN_PARAMS.tokenType]: 'dummyServiceType',
242
+ [TOKEN_PARAMS.clientId]: 'dummyClientId',
243
+ [TOKEN_PARAMS.customer]: 'dummyCustomer',
244
+ }),
289
245
  };
290
246
 
291
247
  const UserSecurity = {
292
248
  regular: (con, req) => {
293
249
  const authToken = checkHeaders(req.headers);
294
250
  const token = checkToken(authToken);
295
- const { request } = con;
296
251
 
297
252
  verifyTokenImplicit(authToken);
298
253
  const scopeResult = checkScopes(token.scope, getScopes(con, USER_SECURITY), con.api.definition);
299
254
 
300
- if (scopeResult.onBehalf) {
301
- req[TOKEN_PARAMS.onBehalf] = true;
302
- request[TOKEN_PARAMS.onBehalf] = true;
303
- }
304
- req[TOKEN_PARAMS.claims] = scopeResult.claims;
305
- request[TOKEN_PARAMS.claims] = scopeResult.claims;
306
- req[TOKEN_PARAMS.tokenType] = USER;
307
- request[TOKEN_PARAMS.tokenType] = USER;
308
- if (token.sub) {
309
- req[TOKEN_PARAMS.userId] = token.sub;
310
- request[TOKEN_PARAMS.userId] = token.sub;
311
- }
312
- if (token.azp) {
313
- req[TOKEN_PARAMS.appId] = token.azp;
314
- request[TOKEN_PARAMS.appId] = token.azp;
315
- }
255
+ if (scopeResult.onBehalf) setParam(req, con.request, TOKEN_PARAMS.onBehalf, true);
256
+ setParam(req, con.request, TOKEN_PARAMS.claims, scopeResult.claims);
257
+ setParam(req, con.request, TOKEN_PARAMS.tokenType, USER);
258
+ if (token.sub) setParam(req, con.request, TOKEN_PARAMS.userId, token.sub);
259
+ if (token.azp) setParam(req, con.request, TOKEN_PARAMS.appId, token.azp);
316
260
  if (token.may_act && token.may_act.sub) {
317
- req[TOKEN_PARAMS.onBehalfId] = token.sub;
318
- request[TOKEN_PARAMS.onBehalfId] = token.sub;
319
- req[TOKEN_PARAMS.userId] = token.may_act.sub;
320
- request[TOKEN_PARAMS.userId] = token.may_act.sub;
261
+ setParam(req, con.request, TOKEN_PARAMS.onBehalfId, token.sub);
262
+ setParam(req, con.request, TOKEN_PARAMS.userId, token.may_act.sub);
321
263
  }
322
264
  return true;
323
265
  },
324
- mock: (con, req) => {
325
- const { request } = con;
326
-
327
- req[TOKEN_PARAMS.claims] = ['dummyClaims'];
328
- req[TOKEN_PARAMS.userId] = 'dummyUserId';
329
- req[TOKEN_PARAMS.appId] = 'dummyAppId';
330
- req[TOKEN_PARAMS.tokenType] = USER;
331
- request[TOKEN_PARAMS.claims] = ['dummyClaims'];
332
- request[TOKEN_PARAMS.userId] = 'dummyUserId';
333
- request[TOKEN_PARAMS.appId] = 'dummyAppId';
334
- request[TOKEN_PARAMS.tokenType] = USER;
335
- return true;
336
- },
266
+ mock: createMockHandler({
267
+ [TOKEN_PARAMS.claims]: ['dummyClaims'],
268
+ [TOKEN_PARAMS.userId]: 'dummyUserId',
269
+ [TOKEN_PARAMS.appId]: 'dummyAppId',
270
+ [TOKEN_PARAMS.tokenType]: USER,
271
+ }),
337
272
  };
338
273
 
339
274
  const ApiKeySecurity = {
340
275
  regular: (con, req) => {
341
276
  const apiKey = req.headers ? req.headers[API_KEY_NAME.toLowerCase()] : null;
342
- const { request } = con;
343
277
 
344
- if (config.security.apiKeys.includes(apiKey)) {
345
- req[API_KEY_NAME] = apiKey;
346
- request[API_KEY_NAME] = apiKey;
278
+ if (config.security.apiKeys && config.security.apiKeys.includes(apiKey)) {
279
+ setParam(req, con.request, API_KEY_NAME, apiKey);
347
280
  return true;
348
281
  }
349
282
  throw getError('invalid API key', UNAUTHORIZED_ERROR);
350
283
  },
351
- mock: (con, req) => {
352
- const { request } = con;
353
-
354
- req[API_KEY_NAME] = 'dummyApiKey';
355
- request[API_KEY_NAME] = 'dummyApiKey';
356
- return true;
357
- },
284
+ mock: createMockHandler({ [API_KEY_NAME]: 'dummyApiKey' }),
358
285
  };
359
286
 
360
287
  return {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@mimik/api-helper",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "helper for openAPI backend and mimik service",
5
5
  "main": "./index.js",
6
6
  "type": "module",
7
+ "exports": "./index.js",
7
8
  "scripts": {
8
9
  "lint": "eslint . --no-error-on-unmatched-pattern",
9
10
  "docs": "jsdoc2md index.js > README.md",
10
- "test": "mocha test/ --recursive",
11
+ "test": "mocha --reporter mochawesome --bail --check-leaks --exit test/ --recursive",
11
12
  "test-ci": "c8 --reporter=lcov --reporter=text npm test",
12
13
  "prepublishOnly": "npm run docs && npm run lint && npm run test-ci",
13
14
  "commit-ready": "npm run docs && npm run lint && npm run test-ci"
@@ -27,29 +28,31 @@
27
28
  "url": "https://bitbucket.org/mimiktech/api-helper"
28
29
  },
29
30
  "dependencies": {
30
- "@mimik/request-retry": "^4.0.9",
31
- "@mimik/response-helper": "^4.0.10",
32
- "@mimik/sumologic-winston-logger": "^2.1.14",
33
- "@mimik/swagger-helper": "^5.0.3",
31
+ "@mimik/request-retry": "^4.0.11",
32
+ "@mimik/response-helper": "^4.0.11",
33
+ "@mimik/sumologic-winston-logger": "^2.2.2",
34
+ "@mimik/swagger-helper": "^5.0.4",
34
35
  "ajv-formats": "3.0.1",
35
36
  "js-base64": "3.7.8",
36
37
  "js-yaml": "4.1.1",
37
38
  "jsonwebtoken": "9.0.3",
38
39
  "openapi-backend": "5.16.1",
39
- "swagger-client": "3.36.2"
40
+ "swagger-client": "3.37.1"
40
41
  },
41
42
  "devDependencies": {
42
- "@eslint/js": "9.32.0",
43
- "@mimik/eslint-plugin-document-env": "^2.0.8",
44
- "@stylistic/eslint-plugin": "5.9.0",
43
+ "@eslint/js": "9.39.4",
44
+ "@mimik/eslint-plugin-document-env": "^2.0.9",
45
+ "@mimik/eslint-plugin-logger": "^1.0.3",
46
+ "@stylistic/eslint-plugin": "5.10.0",
45
47
  "c8": "11.0.0",
46
48
  "chai": "6.2.2",
47
- "eslint": "9.32.0",
49
+ "eslint": "9.39.4",
48
50
  "eslint-plugin-import": "2.32.0",
49
51
  "esmock": "2.7.3",
50
- "globals": "17.3.0",
52
+ "globals": "17.4.0",
51
53
  "husky": "9.1.7",
52
54
  "jsdoc-to-markdown": "9.1.3",
53
- "mocha": "11.7.5"
55
+ "mocha": "11.7.5",
56
+ "mochawesome": "7.1.4"
54
57
  }
55
58
  }
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npx mocha:*)",
5
- "Bash(npm test:*)",
6
- "Bash(npx eslint:*)"
7
- ]
8
- }
9
- }
package/.husky/pre-commit DELETED
@@ -1,2 +0,0 @@
1
- #!/bin/sh
2
- npm run commit-ready
package/.husky/pre-push DELETED
@@ -1,2 +0,0 @@
1
- #!/bin/sh
2
- npm run test
package/eslint.config.js DELETED
@@ -1,82 +0,0 @@
1
- import globals from 'globals';
2
- import importPlugin from 'eslint-plugin-import';
3
- import js from '@eslint/js';
4
- import processDoc from '@mimik/eslint-plugin-document-env';
5
- import stylistic from '@stylistic/eslint-plugin';
6
-
7
- const MAX_LENGTH_LINE = 180;
8
- const MAX_FUNCTION_PARAMETERS = 6;
9
- const MAX_LINES_IN_FILES = 600;
10
- const MAX_LINES_IN_FUNCTION = 150;
11
- const MAX_STATEMENTS_IN_FUNCTION = 45;
12
- const MIN_KEYS_IN_OBJECT = 10;
13
- const MAX_COMPLEXITY = 30;
14
- const ECMA_VERSION = 'latest';
15
- const MAX_DEPTH = 6;
16
- const ALLOWED_CONSTANTS = [0, 1, -1];
17
-
18
- export default [
19
- {
20
- ignores: ['mochawesome-report/**', 'node_modules/**', 'dist/**'],
21
- },
22
- importPlugin.flatConfigs.recommended,
23
- stylistic.configs.recommended,
24
- js.configs.all,
25
- {
26
- plugins: {
27
- processDoc,
28
- },
29
- languageOptions: {
30
- ecmaVersion: ECMA_VERSION,
31
- globals: {
32
- ...globals.mocha,
33
- ...globals.nodeBuiltin,
34
- },
35
- sourceType: 'module',
36
- },
37
- rules: {
38
- '@stylistic/brace-style': ['warn', 'stroustrup', { allowSingleLine: true }],
39
- '@stylistic/line-comment-position': ['off'],
40
- '@stylistic/max-len': ['warn', MAX_LENGTH_LINE, { ignoreComments: true, ignoreStrings: true, ignoreRegExpLiterals: true }],
41
- '@stylistic/quotes': ['warn', 'single'],
42
- '@stylistic/semi': ['error', 'always'],
43
- 'capitalized-comments': ['off'],
44
- 'complexity': ['error', MAX_COMPLEXITY],
45
- 'curly': ['off'],
46
- 'id-length': ['error', { exceptions: ['x', 'y', 'z', 'i', 'j', 'k'] }],
47
- 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
48
- 'import/no-unresolved': ['error', { amd: true, caseSensitiveStrict: true, commonjs: true }],
49
- 'init-declarations': ['off'],
50
- 'linebreak-style': ['off'],
51
- 'max-depth': ['error', MAX_DEPTH],
52
- 'max-len': ['off'],
53
- 'max-lines': ['warn', { max: MAX_LINES_IN_FILES, skipComments: true, skipBlankLines: true }],
54
- 'max-lines-per-function': ['warn', { max: MAX_LINES_IN_FUNCTION, skipComments: true, skipBlankLines: true }],
55
- 'max-params': ['error', MAX_FUNCTION_PARAMETERS],
56
- 'max-statements': ['warn', MAX_STATEMENTS_IN_FUNCTION],
57
- 'no-confusing-arrow': ['off'],
58
- 'no-inline-comments': ['off'],
59
- 'no-magic-numbers': ['error', { ignore: ALLOWED_CONSTANTS, enforceConst: true, detectObjects: true }],
60
- 'no-process-env': ['error'],
61
- 'no-ternary': ['off'],
62
- 'no-undefined': ['off'],
63
- 'one-var': ['error', 'never'],
64
- 'processDoc/validate-document-env': ['error'],
65
- 'quotes': ['off'],
66
- 'sort-imports': ['error', { allowSeparatedGroups: true }],
67
- 'sort-keys': ['error', 'asc', { caseSensitive: true, minKeys: MIN_KEYS_IN_OBJECT, natural: false, allowLineSeparatedGroups: true }],
68
- },
69
- },
70
- {
71
- files: ['test/**/*.js'],
72
- rules: {
73
- 'class-methods-use-this': ['off'],
74
- 'max-classes-per-file': ['off'],
75
- 'max-lines': ['off'],
76
- 'max-lines-per-function': ['off'],
77
- 'max-statements': ['off'],
78
- 'no-empty-function': ['off'],
79
- 'no-magic-numbers': ['off'],
80
- },
81
- },
82
- ];
@@ -1,159 +0,0 @@
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
- });