Skip to content

Commit d394a88

Browse files
committed
tests: more test cases
1 parent 94e709f commit d394a88

14 files changed

+469
-32
lines changed

.eslintrc.js

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,192 @@ module.exports = {
1515
],
1616
plugins: ['prettier', 'import', 'unused-imports'],
1717
rules: {
18-
'import/no-unresolved': ['error', { ignore: ['^@bertelsmann-dev'] }],
18+
'unused-imports/no-unused-imports': 'error',
1919
'require-await': 'error',
2020
},
2121
settings: {
22-
'import/resolver': {
23-
typescript: {},
24-
},
22+
'@typescript-eslint/adjacent-overload-signatures': 'error',
23+
'@typescript-eslint/array-type': ['error'],
24+
'@typescript-eslint/await-thenable': 'error',
25+
'@typescript-eslint/ban-types': 'error',
26+
'@typescript-eslint/consistent-type-assertions': 'error',
27+
'@typescript-eslint/consistent-type-definitions': 'error',
28+
'@typescript-eslint/explicit-member-accessibility': [
29+
'off',
30+
{
31+
accessibility: 'explicit',
32+
},
33+
],
34+
'@typescript-eslint/explicit-module-boundary-types': ['error', { allowArgumentsExplicitlyTypedAsAny: true }],
35+
'@typescript-eslint/indent': 'off',
36+
'@typescript-eslint/interface-name-prefix': 'off',
37+
'@typescript-eslint/member-delimiter-style': [
38+
'off',
39+
{
40+
multiline: {
41+
delimiter: 'none',
42+
requireLast: true,
43+
},
44+
singleline: {
45+
delimiter: 'semi',
46+
requireLast: false,
47+
},
48+
},
49+
],
50+
'@typescript-eslint/member-ordering': 'error',
51+
'@typescript-eslint/naming-convention': [
52+
'error',
53+
{
54+
selector: 'typeLike',
55+
format: ['PascalCase'],
56+
},
57+
{
58+
selector: 'variable',
59+
modifiers: ['const'],
60+
format: ['camelCase', 'UPPER_CASE'],
61+
},
62+
{
63+
selector: 'enumMember',
64+
format: ['UPPER_CASE'],
65+
},
66+
{
67+
selector: 'parameter',
68+
format: ['camelCase'],
69+
leadingUnderscore: 'allow',
70+
filter: {
71+
regex: '^(_id|Authorization|Content-Type)$',
72+
match: false,
73+
},
74+
},
75+
{
76+
selector: 'memberLike',
77+
modifiers: ['static', 'readonly'],
78+
format: ['UPPER_CASE'],
79+
leadingUnderscore: 'forbid',
80+
},
81+
{
82+
selector: 'memberLike',
83+
modifiers: ['private', 'static', 'readonly'],
84+
format: ['UPPER_CASE'],
85+
leadingUnderscore: 'forbid',
86+
},
87+
{
88+
selector: 'memberLike',
89+
modifiers: ['private'],
90+
format: ['camelCase'],
91+
leadingUnderscore: 'forbid',
92+
},
93+
{
94+
selector: 'default',
95+
format: ['camelCase'],
96+
filter: {
97+
regex: '^(_id|Authorization|Content-Type)$',
98+
match: false,
99+
},
100+
},
101+
],
102+
'@typescript-eslint/no-empty-function': 'error',
103+
'@typescript-eslint/no-empty-interface': 'error',
104+
'@typescript-eslint/no-explicit-any': 'off',
105+
'@typescript-eslint/no-inferrable-types': 'error',
106+
'@typescript-eslint/no-misused-new': 'error',
107+
'@typescript-eslint/no-magic-numbers': ['error', { ignore: [-1, 0, 1], ignoreEnums: true }],
108+
'@typescript-eslint/no-namespace': 'warn',
109+
'@typescript-eslint/no-non-null-assertion': 'error',
110+
'@typescript-eslint/no-parameter-properties': 'off',
111+
'@typescript-eslint/no-require-imports': 'error',
112+
'@typescript-eslint/no-shadow': ['error'],
113+
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
114+
'@typescript-eslint/no-unused-vars': [
115+
'error',
116+
{
117+
vars: 'all',
118+
args: 'none',
119+
ignoreRestSiblings: false,
120+
},
121+
],
122+
'@typescript-eslint/no-use-before-define': 'off',
123+
'@typescript-eslint/no-var-requires': 'error',
124+
'@typescript-eslint/prefer-for-of': 'error',
125+
'@typescript-eslint/prefer-function-type': 'error',
126+
'@typescript-eslint/prefer-namespace-keyword': 'error',
127+
'@typescript-eslint/quotes': 'off',
128+
'@typescript-eslint/semi': ['off', null],
129+
'@typescript-eslint/space-within-parens': ['off', 'never'],
130+
'@typescript-eslint/triple-slash-reference': 'error',
131+
'@typescript-eslint/type-annotation-spacing': 'off',
132+
'@typescript-eslint/unified-signatures': 'error',
133+
'arrow-body-style': 'error',
134+
'arrow-parens': ['off', 'always'],
135+
'brace-style': ['error'],
136+
camelcase: 'off', // we're using @typescript-eslint/naming-convention instead
137+
'comma-dangle': 'off',
138+
complexity: 'off',
139+
'constructor-super': 'error',
140+
curly: ['error', 'all'],
141+
'dot-notation': 'error',
142+
'eol-last': 'off',
143+
eqeqeq: ['error', 'smart'],
144+
'guard-for-in': 'error',
145+
'id-match': 'error',
146+
'import/order': 'error',
147+
'linebreak-style': 'off',
148+
'max-classes-per-file': 'off',
149+
'max-len': 'off',
150+
'new-parens': 'off',
151+
'newline-per-chained-call': 'off',
152+
'no-bitwise': 'error',
153+
'no-caller': 'error',
154+
'no-cond-assign': 'error',
155+
'no-console': ['error', { allow: [''] }],
156+
'no-debugger': 'error',
157+
'no-empty': 'error',
158+
'no-eval': 'error',
159+
'no-extra-semi': 'off',
160+
'no-fallthrough': 'error',
161+
'no-invalid-this': 'error',
162+
'no-irregular-whitespace': 'off',
163+
'no-magic-numbers': 'off', // we're using @typescript-eslint/no-magic-numbers instead
164+
'no-multiple-empty-lines': 'off',
165+
'no-new-wrappers': 'error',
166+
'no-shadow': 'off', // we're using @typescript-eslint/no-shadow instead
167+
'no-throw-literal': 'error',
168+
'no-trailing-spaces': 'off',
169+
'no-undef-init': 'error',
170+
'no-underscore-dangle': ['error', { allow: ['_id'] }],
171+
'no-unsafe-finally': 'error',
172+
'no-unused-expressions': 'error',
173+
'no-unused-labels': 'error',
174+
'no-var': 'error',
175+
'object-shorthand': 'error',
176+
'one-var': ['error', 'never'],
177+
'prefer-arrow-callback': 'error',
178+
'prefer-const': 'error',
179+
'quote-props': 'off',
180+
radix: 'error',
181+
'space-before-function-paren': 'off',
182+
'spaced-comment': [
183+
'error',
184+
'always',
185+
{
186+
line: {
187+
markers: ['/'],
188+
exceptions: ['-', '+'],
189+
},
190+
block: {
191+
markers: ['!'],
192+
exceptions: ['*'],
193+
balanced: true,
194+
},
195+
},
196+
],
197+
'space-in-parens': ['off', 'never'],
198+
'use-isnan': 'error',
199+
'valid-typeof': 'off',
200+
'no-unused-vars': 'off', // we're using @typescript-eslint/no-unused-vars instead
201+
'no-restricted-imports': ['error', 'rxjs/Rx'],
202+
'no-redeclare': 'error',
203+
'unused-imports/no-unused-imports': 'error',
204+
'require-await': 'error',
25205
},
26206
}

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ the request body and response body against the schema.
99

1010
* Early version: Bugs most likely exist. Bug fixes welcome and more test cases very welcom. :-)
1111
* So far only tested with Open API specifications in v3
12+
* Does not (yet?) validate headers
13+
* Does not (really) validate path params, but supports them
1214
* This library does not validate the Open API specification itself. I suggest you use another tool for this for now.
1315

1416
## Getting started
@@ -70,6 +72,8 @@ Start the function app by running the VScode launch configuration `Debug Functio
7072
Then send some requests to the API, for example:
7173
`curl -X POST -H "Content-Type: application/json" -d '{"name":"hi"}' http://localhost:7071/api/users`
7274

73-
## License
75+
## License and Attribution
7476

7577
The scripts and documentation in this project are released under the [MIT License](LICENSE)
78+
79+
Some of the validation test cases are based on the tests from [openapi-request-validator](`https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-request-validator`) by Kogo Software LLC released under MIT.

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"eslint-plugin-import": "2.29.1",
5252
"eslint-plugin-prettier": "5.1.2",
5353
"eslint-plugin-unused-imports": "3.0.0",
54+
"fast-copy": "^3.0.1",
5455
"fork-ts-checker-webpack-plugin": "9.0.2",
5556
"jest": "29.7.0",
5657
"js-yaml": "^4.1.0",

src/ajv-openapi-validator.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export class AjvOpenApiValidator {
264264
if (spec.components?.requestBodies && spec.components?.requestBodies[requestBodyName]) {
265265
const response = spec.components?.requestBodies[requestBodyName]
266266
if (!isReferenceObject(response)) {
267-
schema = response.content['application/json']?.schema
267+
schema = this.getJsonContent(response.content)?.schema
268268
required = !!response.required
269269
} else {
270270
errorStr = `A reference was not expected here: '${response.$ref}'`
@@ -278,9 +278,12 @@ export class AjvOpenApiValidator {
278278
if (errorStr && this.validatorOpts.strict) {
279279
throw new Error(errorStr)
280280
}
281-
} else if (operation.requestBody.content['application/json']) {
282-
schema = operation.requestBody.content['application/json'].schema
283-
required = !!operation.requestBody.required
281+
} else {
282+
const content = this.getJsonContent(operation.requestBody.content)
283+
if (content) {
284+
schema = content.schema
285+
required = !!operation.requestBody.required
286+
}
284287
}
285288

286289
if (schema) {
@@ -313,7 +316,8 @@ export class AjvOpenApiValidator {
313316
if (spec.components?.responses && spec.components?.responses[respName]) {
314317
const response = spec.components?.responses[respName]
315318
if (!isReferenceObject(response)) {
316-
schema = response.content ? response.content['application/json']?.schema : undefined
319+
const content = this.getJsonContent(response.content)
320+
schema = content ? content.schema : undefined
317321
} else {
318322
errorStr = `A reference was not expected here: '${response.$ref}'`
319323
}
@@ -327,7 +331,7 @@ export class AjvOpenApiValidator {
327331
throw new Error(errorStr)
328332
}
329333
} else if (response.content) {
330-
schema = response.content['application/json']?.schema
334+
schema = this.getJsonContent(response.content)?.schema
331335
}
332336

333337
if (schema) {
@@ -375,11 +379,14 @@ export class AjvOpenApiValidator {
375379
}
376380

377381
// TODO could also add support for other parameters such as headers here
378-
if (resolvedParam?.in === 'query' && resolvedParam.schema) {
382+
if (resolvedParam?.in === 'query') {
379383
const schemaName = `#/paths${path.replace(/[{}]/g, '')}/${method}/parameters/${resolvedParam.name}`
380384
this.validatorOpts.log(`Adding parameter validator '${path}', '${method}', '${resolvedParam.name}'`)
381-
this.ajv.addSchema(resolvedParam.schema, schemaName)
385+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
386+
const schema = resolvedParam.schema ?? { type: (resolvedParam as any).type } // stricly speaking this isn't valid, but we support it
387+
this.ajv.addSchema(schema ?? resolvedParam, schemaName)
382388
const validator = this.ajv.compile({ $ref: schemaName })
389+
383390
this.paramsValidators.push({
384391
path: path.toLowerCase(),
385392
method: method.toLowerCase() as string,
@@ -401,4 +408,17 @@ export class AjvOpenApiValidator {
401408
throw new Error('The following schemas failed to compile: ' + schemaCompileFailures.join(', '))
402409
}
403410
}
411+
412+
private getJsonContent(content?: { [media: string]: OpenAPIV3.MediaTypeObject }): OpenAPIV3.MediaTypeObject | undefined {
413+
if (!content) {
414+
return undefined
415+
} else if (content['application/json']) {
416+
return content['application/json']
417+
} else if (content['application/json; charset=utf-8']) {
418+
return content['application/json; charset=utf-8']
419+
} else {
420+
const key = Object.keys(content).find((key) => key.toLowerCase().startsWith('application/json;'))
421+
return key ? content[key] : undefined
422+
}
423+
}
404424
}

src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function setupValidation(
106106

107107
if (
108108
requiredHookOpts.requestBodyValidationMode &&
109+
request.headers.get('content-type')?.includes('application/json') &&
109110
(!exclusion || (exclusion.validation !== false && exclusion.validation.requestBody !== false))
110111
) {
111112
context.debug(`Validating request body for '${path}', '${method}'`)

src/openapi-validator.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,30 +50,16 @@ export interface DocWithCompSchemas extends OpenAPIV3.Document {
5050
components: WithRequired<OpenAPIV3.ComponentsObject, 'schemas'>
5151
}
5252
export function hasComponentSchemas(doc: OpenAPIV3.Document): doc is DocWithCompSchemas {
53-
return doc.components?.schemas !== undefined
53+
return doc.components?.schemas !== undefined && doc.components?.schemas !== null
5454
}
5555

5656
export interface DocWithReqBodies extends OpenAPIV3.Document {
5757
components: WithRequired<OpenAPIV3.ComponentsObject, 'requestBodies'>
5858
}
5959
export function hasComponentRequestBodies(doc: OpenAPIV3.Document): doc is DocWithReqBodies {
60-
return doc.components?.requestBodies !== undefined
60+
return doc.components?.requestBodies !== undefined && doc.components?.requestBodies !== null
6161
}
6262

63-
// export interface DocWithResponses extends OpenAPIV3.Document {
64-
// components: WithRequired<OpenAPIV3.ComponentsObject, 'responses'>
65-
// }
66-
// export function hasComponentResponses(doc: OpenAPIV3.Document): doc is DocWithResponses {
67-
// return doc.components?.responses !== undefined;
68-
// }
69-
70-
// export interface DocWithParameters extends OpenAPIV3.Document {
71-
// components: WithRequired<OpenAPIV3.ComponentsObject, 'parameters'>
72-
// }
73-
// export function hasComponentParameters(doc: OpenAPIV3.Document): doc is DocWithParameters {
74-
// return doc.components?.parameters !== undefined;
75-
// }
76-
7763
export function isReferenceObject(
7864
parameter: OpenAPIV3.ParameterObject | OpenAPIV3.SchemaObject | OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject
7965
): parameter is OpenAPIV3.ReferenceObject {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json; charset=utf-8': {
8+
schema: {
9+
$ref: '#/components/schemas/Test1',
10+
},
11+
},
12+
},
13+
},
14+
schemas: {
15+
Test1: {
16+
properties: {
17+
foo: {
18+
type: 'string',
19+
default: 'foo',
20+
},
21+
},
22+
required: ['foo'],
23+
},
24+
},
25+
},
26+
request: {
27+
body: {},
28+
headers: {
29+
'content-type': 'application/json',
30+
},
31+
},
32+
}

0 commit comments

Comments
 (0)