Skip to content

Commit 0c45db9

Browse files
committed
test: add more test cases
1 parent 814e92a commit 0c45db9

17 files changed

+444
-56
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ the request body and response body against the schema.
1313
* Does not support external schemas (inline schemas are supported)
1414
* Does not (yet?) validate headers
1515
* Does not (really) validate path params, but supports them in the definition and request route
16+
* Does not support references to properties (e.g. `$ref: '#/components/schemas/Test1/properties/bar/allOf/0/properties/baz'`)
17+
* Does not support `readOnly` or `writeOnly`.
1618
* This library does not validate the Open API specification itself. I suggest you use another tool for this for now.
1719

20+
To check out what is supported, take a look at the [test fixtures](/test/fixtures/)
21+
1822
## Getting started
1923

2024
Because the Open API specification can come in different flavors and from different sources, loading the specification file is not in scope

package-lock.json

Lines changed: 17 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
@@ -36,6 +36,7 @@
3636
"@azure/functions": "^4.2.0",
3737
"ajv": "^8.12.0",
3838
"ajv-draft-04": "^1.0.0",
39+
"ajv-formats": "^2.1.1",
3940
"allof-merge": "^0.6.1"
4041
},
4142
"devDependencies": {

src/ajv-openapi-validator.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint-disable no-invalid-this */
33
import AjvDraft4 from 'ajv-draft-04'
44
import Ajv, { ErrorObject, ValidateFunction, Options } from 'ajv'
5+
import addFormats from 'ajv-formats'
56
import { OpenAPIV3 } from 'openapi-types'
67
import {
78
convertDatesToISOString,
@@ -14,7 +15,7 @@ import {
1415
hasComponentSchemas,
1516
isReferenceObject,
1617
} from './openapi-validator'
17-
import { DEFAULT_AJV_SETTINGS } from './ajv-opts'
18+
import { AjvExtras, DEFAULT_AJV_EXTRAS, DEFAULT_AJV_SETTINGS } from './ajv-opts'
1819
import { merge, openApiMergeRules } from 'allof-merge'
1920

2021
const REQ_BODY_COMPONENT_PREFIX_LENGTH = 27 // #/components/requestBodies/PetBody
@@ -83,10 +84,25 @@ export class AjvOpenApiValidator {
8384
* @param spec - Parsed OpenAPI V3 specification
8485
* @param ajvOpts - Optional Ajv options
8586
* @param validatorOpts - Optional additional validator options
87+
* @param ajvExtras - Optional additional Ajv features
8688
*/
87-
constructor(spec: OpenAPIV3.Document, validatorOpts?: ValidatorOpts, ajvOpts: Options = DEFAULT_AJV_SETTINGS) {
89+
constructor(
90+
spec: OpenAPIV3.Document,
91+
validatorOpts?: ValidatorOpts,
92+
ajvOpts: Options = DEFAULT_AJV_SETTINGS,
93+
ajvExtras: AjvExtras = DEFAULT_AJV_EXTRAS
94+
) {
8895
// always disable removeAdditional, because it has unexpected results with allOf
8996
this.ajv = new AjvDraft4({ ...DEFAULT_AJV_SETTINGS, ...ajvOpts, removeAdditional: false })
97+
if (ajvExtras?.addStandardFormats === true) {
98+
addFormats(this.ajv)
99+
}
100+
if (ajvExtras?.customKeywords) {
101+
ajvExtras.customKeywords.forEach((kwd) => {
102+
this.ajv.addKeyword(kwd)
103+
})
104+
}
105+
90106
this.validatorOpts = validatorOpts ? { ...DEFAULT_VALIDATOR_OPTS, ...validatorOpts } : DEFAULT_VALIDATOR_OPTS
91107
if (this.validatorOpts.log == undefined) {
92108
this.validatorOpts.log = () => {}

src/ajv-opts.ts

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,20 @@
11
// based on express-openapi-validator released under MIT
22
// Copyright (c) 2019 Carmine M. DiMascio
33

4-
import { Options } from 'ajv'
5-
6-
const maxInt32 = 2 ** 31 - 1
7-
const minInt32 = (-2) ** 31
8-
9-
const maxInt64 = 2 ** 63 - 1
10-
const minInt64 = (-2) ** 63
11-
12-
const maxFloat = (2 - 2 ** -23) * 2 ** 127
13-
const minPosFloat = 2 ** -126
14-
const minFloat = -1 * maxFloat
15-
const maxNegFloat = -1 * minPosFloat
16-
17-
const alwaysTrue = () => true
18-
const base64regExp = /^[A-Za-z0-9+/]*(=|==)?$/
19-
20-
export const AJV_FORMATS = {
21-
int32: {
22-
validate: (i: number) => Number.isInteger(i) && i <= maxInt32 && i >= minInt32,
23-
type: 'number',
24-
},
25-
int64: {
26-
validate: (i: number) => Number.isInteger(i) && i <= maxInt64 && i >= minInt64,
27-
type: 'number',
28-
},
29-
float: {
30-
validate: (i: number) =>
31-
typeof i === 'number' && (i === 0 || (i <= maxFloat && i >= minPosFloat) || (i >= minFloat && i <= maxNegFloat)),
32-
type: 'number',
33-
},
34-
double: {
35-
validate: (i: number) => typeof i === 'number',
36-
type: 'number',
37-
},
38-
byte: (b: string) => b.length % 4 === 0 && base64regExp.test(b),
39-
binary: alwaysTrue,
40-
password: alwaysTrue,
41-
'uri-reference': true,
42-
'date-time': (dateTimeString: unknown) => {
43-
if (typeof dateTimeString === 'object' && dateTimeString instanceof Date) {
44-
dateTimeString = dateTimeString.toISOString()
45-
}
46-
47-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48-
return !isNaN(Date.parse(dateTimeString as any)) // any test that returns true/false
49-
},
50-
uuid: (uuid: string) => {
51-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
52-
return uuidRegex.test(uuid)
53-
},
54-
} as const
4+
import { KeywordDefinition, Options } from 'ajv'
555

566
export const DEFAULT_AJV_SETTINGS: Options = {
577
allErrors: true,
588
useDefaults: true,
599
discriminator: true,
60-
formats: AJV_FORMATS,
61-
coerceTypes: true,
10+
coerceTypes: false,
11+
}
12+
13+
export interface AjvExtras {
14+
addStandardFormats?: boolean
15+
customKeywords?: KeywordDefinition[]
16+
}
17+
18+
export const DEFAULT_AJV_EXTRAS: AjvExtras = {
19+
addStandardFormats: true,
6220
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
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+
},
29+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
8+
schema: {
9+
$ref: '#/components/schemas/Test1',
10+
},
11+
},
12+
},
13+
},
14+
schemas: {
15+
Test1: {
16+
properties: {
17+
foo: {
18+
type: 'string',
19+
},
20+
},
21+
required: ['foo'],
22+
},
23+
},
24+
},
25+
request: {
26+
body: {
27+
foo: 'asdf',
28+
},
29+
},
30+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
8+
schema: {
9+
type: 'object',
10+
properties: {
11+
foo: {
12+
allOf: [{ $ref: '#/components/schemas/MyType' }],
13+
type: 'string',
14+
nullable: true,
15+
},
16+
},
17+
},
18+
},
19+
},
20+
},
21+
schemas: {
22+
MyType: {
23+
type: "string",
24+
pattern: "^[A-Za-z0-9\\u00C0-÷\\u017F\\s!'¿¡@$€_+=\\-;:/.,?>)(<[]*$",
25+
},
26+
},
27+
},
28+
request: {
29+
body: {
30+
foo: null,
31+
},
32+
},
33+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
8+
schema: {
9+
type: 'object',
10+
properties: {
11+
foo: {
12+
oneOf: [
13+
{ type: 'null' },
14+
{
15+
type: 'string',
16+
nullable: true,
17+
enum: ['HOME', 'CAR'],
18+
},
19+
]
20+
},
21+
},
22+
},
23+
},
24+
},
25+
},
26+
schemas: null,
27+
},
28+
request: {
29+
body: {
30+
foo: null,
31+
},
32+
},
33+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
8+
schema: {
9+
type: 'object',
10+
properties: {
11+
foo: {
12+
type: 'string',
13+
nullable: true,
14+
enum: [null, 'HOME', 'CAR'],
15+
},
16+
},
17+
},
18+
},
19+
},
20+
},
21+
schemas: null,
22+
},
23+
request: {
24+
body: {
25+
foo: null,
26+
},
27+
},
28+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
module.exports = {
2+
validateArgs: {
3+
parameters: [],
4+
requestBody: {
5+
description: 'a test body',
6+
content: {
7+
'application/json': {
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+
"something-else": 123
29+
}
30+
},
31+
expectedErrors: [
32+
{
33+
code: 'Validation-additionalProperties',
34+
source: {
35+
pointer: '#/components/schemas/Test1/additionalProperties'
36+
},
37+
status: 400,
38+
"title": 'must NOT have additional properties'
39+
}
40+
]
41+
};

0 commit comments

Comments
 (0)