@schmock/validation 1.1.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/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/package.json +43 -0
- package/src/index.test.ts +140 -0
- package/src/index.ts +141 -0
- package/src/steps/validation-plugin.steps.ts +268 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
interface ValidationRules {
|
|
3
|
+
request?: {
|
|
4
|
+
body?: JSONSchema7;
|
|
5
|
+
query?: JSONSchema7;
|
|
6
|
+
headers?: JSONSchema7;
|
|
7
|
+
};
|
|
8
|
+
response?: {
|
|
9
|
+
body?: JSONSchema7;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
interface ValidationPluginOptions extends ValidationRules {
|
|
13
|
+
/** Custom status code for request validation failures (default: 400) */
|
|
14
|
+
requestErrorStatus?: number;
|
|
15
|
+
/** Custom status code for response validation failures (default: 500) */
|
|
16
|
+
responseErrorStatus?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function validationPlugin(options: ValidationPluginOptions): Schmock.Plugin;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,KAAK,CAAC,EAAE,WAAW,CAAC;QACpB,OAAO,CAAC,EAAE,WAAW,CAAC;KACvB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,WAAW,CAAC;KACpB,CAAC;CACH;AAED,UAAU,uBAAwB,SAAQ,eAAe;IACvD,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,MAAM,CAmHhB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
export function validationPlugin(options) {
|
|
4
|
+
const requestErrorStatus = options.requestErrorStatus ?? 400;
|
|
5
|
+
const responseErrorStatus = options.responseErrorStatus ?? 500;
|
|
6
|
+
// Pre-compile all validators at plugin creation time
|
|
7
|
+
const ajv = new Ajv({ allErrors: true });
|
|
8
|
+
const validators = {};
|
|
9
|
+
if (options.request?.body) {
|
|
10
|
+
validators.requestBody = ajv.compile(options.request.body);
|
|
11
|
+
}
|
|
12
|
+
if (options.request?.query) {
|
|
13
|
+
validators.requestQuery = ajv.compile(options.request.query);
|
|
14
|
+
}
|
|
15
|
+
if (options.request?.headers) {
|
|
16
|
+
validators.requestHeaders = ajv.compile(options.request.headers);
|
|
17
|
+
}
|
|
18
|
+
if (options.response?.body) {
|
|
19
|
+
validators.responseBody = ajv.compile(options.response.body);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
name: "validation",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
process(context, response) {
|
|
25
|
+
// Validate request body (skip when no body provided, e.g. GET requests)
|
|
26
|
+
if (validators.requestBody && context.body !== undefined) {
|
|
27
|
+
if (!validators.requestBody(context.body)) {
|
|
28
|
+
return {
|
|
29
|
+
context,
|
|
30
|
+
response: {
|
|
31
|
+
status: requestErrorStatus,
|
|
32
|
+
body: {
|
|
33
|
+
error: "Request validation failed",
|
|
34
|
+
code: "REQUEST_VALIDATION_ERROR",
|
|
35
|
+
details: validators.requestBody.errors,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Validate request query parameters
|
|
42
|
+
if (validators.requestQuery && context.query) {
|
|
43
|
+
if (!validators.requestQuery(context.query)) {
|
|
44
|
+
return {
|
|
45
|
+
context,
|
|
46
|
+
response: {
|
|
47
|
+
status: requestErrorStatus,
|
|
48
|
+
body: {
|
|
49
|
+
error: "Query parameter validation failed",
|
|
50
|
+
code: "QUERY_VALIDATION_ERROR",
|
|
51
|
+
details: validators.requestQuery.errors,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Validate request headers
|
|
58
|
+
if (validators.requestHeaders && context.headers) {
|
|
59
|
+
// Lowercase all header names for comparison
|
|
60
|
+
const normalizedHeaders = {};
|
|
61
|
+
for (const [key, value] of Object.entries(context.headers)) {
|
|
62
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
63
|
+
}
|
|
64
|
+
if (!validators.requestHeaders(normalizedHeaders)) {
|
|
65
|
+
return {
|
|
66
|
+
context,
|
|
67
|
+
response: {
|
|
68
|
+
status: requestErrorStatus,
|
|
69
|
+
body: {
|
|
70
|
+
error: "Header validation failed",
|
|
71
|
+
code: "HEADER_VALIDATION_ERROR",
|
|
72
|
+
details: validators.requestHeaders.errors,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Validate response body (if response exists)
|
|
79
|
+
if (validators.responseBody && response !== undefined) {
|
|
80
|
+
// Unwrap tuple responses: [status, body] or [status, body, headers]
|
|
81
|
+
const isTuple = Array.isArray(response) && typeof response[0] === "number";
|
|
82
|
+
const responseBody = isTuple ? response[1] : response;
|
|
83
|
+
if (!validators.responseBody(responseBody)) {
|
|
84
|
+
return {
|
|
85
|
+
context,
|
|
86
|
+
response: {
|
|
87
|
+
status: responseErrorStatus,
|
|
88
|
+
body: {
|
|
89
|
+
error: "Response validation failed",
|
|
90
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
91
|
+
details: validators.responseBody.errors,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { context, response };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schmock/validation",
|
|
3
|
+
"description": "Request/response validation plugin for Schmock",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build:lib && bun build:types",
|
|
22
|
+
"build:lib": "bun build --minify --outdir=dist src/index.ts",
|
|
23
|
+
"build:types": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:watch": "vitest --watch",
|
|
26
|
+
"test:bdd": "vitest run --config vitest.config.bdd.ts",
|
|
27
|
+
"lint": "biome check src/*.ts",
|
|
28
|
+
"lint:fix": "biome check --write --unsafe src/*.ts"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"ajv": "^8.17.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@schmock/core": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@amiceli/vitest-cucumber": "^6.2.0",
|
|
39
|
+
"@types/json-schema": "^7.0.15",
|
|
40
|
+
"@types/node": "^25.1.0",
|
|
41
|
+
"vitest": "^4.0.15"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { validationPlugin } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("validationPlugin", () => {
|
|
5
|
+
it("creates a plugin with correct name", () => {
|
|
6
|
+
const plugin = validationPlugin({
|
|
7
|
+
request: {
|
|
8
|
+
body: { type: "object" },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
expect(plugin.name).toBe("validation");
|
|
12
|
+
expect(plugin.version).toBe("1.0.0");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("passes through non-matching requests", async () => {
|
|
16
|
+
const plugin = validationPlugin({
|
|
17
|
+
request: {
|
|
18
|
+
body: {
|
|
19
|
+
type: "object",
|
|
20
|
+
required: ["name"],
|
|
21
|
+
properties: { name: { type: "string" } },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// No body in context — validation skipped
|
|
27
|
+
const result = await plugin.process(
|
|
28
|
+
{
|
|
29
|
+
path: "/test",
|
|
30
|
+
route: {},
|
|
31
|
+
method: "GET",
|
|
32
|
+
params: {},
|
|
33
|
+
query: {},
|
|
34
|
+
headers: {},
|
|
35
|
+
state: new Map(),
|
|
36
|
+
},
|
|
37
|
+
"original response",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(result.response).toBe("original response");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects invalid request body", async () => {
|
|
44
|
+
const plugin = validationPlugin({
|
|
45
|
+
request: {
|
|
46
|
+
body: {
|
|
47
|
+
type: "object",
|
|
48
|
+
required: ["name"],
|
|
49
|
+
properties: { name: { type: "string" } },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await plugin.process(
|
|
55
|
+
{
|
|
56
|
+
path: "/test",
|
|
57
|
+
route: {},
|
|
58
|
+
method: "POST",
|
|
59
|
+
params: {},
|
|
60
|
+
query: {},
|
|
61
|
+
headers: {},
|
|
62
|
+
body: { age: 25 },
|
|
63
|
+
state: new Map(),
|
|
64
|
+
},
|
|
65
|
+
undefined,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result.response).toEqual(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
status: 400,
|
|
71
|
+
body: expect.objectContaining({
|
|
72
|
+
code: "REQUEST_VALIDATION_ERROR",
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("validates response body", async () => {
|
|
79
|
+
const plugin = validationPlugin({
|
|
80
|
+
response: {
|
|
81
|
+
body: {
|
|
82
|
+
type: "object",
|
|
83
|
+
required: ["id"],
|
|
84
|
+
properties: { id: { type: "number" } },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await plugin.process(
|
|
90
|
+
{
|
|
91
|
+
path: "/test",
|
|
92
|
+
route: {},
|
|
93
|
+
method: "GET",
|
|
94
|
+
params: {},
|
|
95
|
+
query: {},
|
|
96
|
+
headers: {},
|
|
97
|
+
state: new Map(),
|
|
98
|
+
},
|
|
99
|
+
{ id: "not-a-number" },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.response).toEqual(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
status: 500,
|
|
105
|
+
body: expect.objectContaining({
|
|
106
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("uses custom error status codes", async () => {
|
|
113
|
+
const plugin = validationPlugin({
|
|
114
|
+
request: {
|
|
115
|
+
body: {
|
|
116
|
+
type: "object",
|
|
117
|
+
required: ["name"],
|
|
118
|
+
properties: { name: { type: "string" } },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
requestErrorStatus: 422,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await plugin.process(
|
|
125
|
+
{
|
|
126
|
+
path: "/test",
|
|
127
|
+
route: {},
|
|
128
|
+
method: "POST",
|
|
129
|
+
params: {},
|
|
130
|
+
query: {},
|
|
131
|
+
headers: {},
|
|
132
|
+
body: {},
|
|
133
|
+
state: new Map(),
|
|
134
|
+
},
|
|
135
|
+
undefined,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(result.response).toEqual(expect.objectContaining({ status: 422 }));
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
|
|
3
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
4
|
+
import type { JSONSchema7 } from "json-schema";
|
|
5
|
+
|
|
6
|
+
interface ValidationRules {
|
|
7
|
+
request?: {
|
|
8
|
+
body?: JSONSchema7;
|
|
9
|
+
query?: JSONSchema7;
|
|
10
|
+
headers?: JSONSchema7;
|
|
11
|
+
};
|
|
12
|
+
response?: {
|
|
13
|
+
body?: JSONSchema7;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ValidationPluginOptions extends ValidationRules {
|
|
18
|
+
/** Custom status code for request validation failures (default: 400) */
|
|
19
|
+
requestErrorStatus?: number;
|
|
20
|
+
/** Custom status code for response validation failures (default: 500) */
|
|
21
|
+
responseErrorStatus?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validationPlugin(
|
|
25
|
+
options: ValidationPluginOptions,
|
|
26
|
+
): Schmock.Plugin {
|
|
27
|
+
const requestErrorStatus = options.requestErrorStatus ?? 400;
|
|
28
|
+
const responseErrorStatus = options.responseErrorStatus ?? 500;
|
|
29
|
+
|
|
30
|
+
// Pre-compile all validators at plugin creation time
|
|
31
|
+
const ajv = new Ajv({ allErrors: true });
|
|
32
|
+
const validators: {
|
|
33
|
+
requestBody?: ValidateFunction;
|
|
34
|
+
requestQuery?: ValidateFunction;
|
|
35
|
+
requestHeaders?: ValidateFunction;
|
|
36
|
+
responseBody?: ValidateFunction;
|
|
37
|
+
} = {};
|
|
38
|
+
|
|
39
|
+
if (options.request?.body) {
|
|
40
|
+
validators.requestBody = ajv.compile(options.request.body);
|
|
41
|
+
}
|
|
42
|
+
if (options.request?.query) {
|
|
43
|
+
validators.requestQuery = ajv.compile(options.request.query);
|
|
44
|
+
}
|
|
45
|
+
if (options.request?.headers) {
|
|
46
|
+
validators.requestHeaders = ajv.compile(options.request.headers);
|
|
47
|
+
}
|
|
48
|
+
if (options.response?.body) {
|
|
49
|
+
validators.responseBody = ajv.compile(options.response.body);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: "validation",
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
|
|
56
|
+
process(
|
|
57
|
+
context: Schmock.PluginContext,
|
|
58
|
+
response?: unknown,
|
|
59
|
+
): Schmock.PluginResult {
|
|
60
|
+
// Validate request body (skip when no body provided, e.g. GET requests)
|
|
61
|
+
if (validators.requestBody && context.body !== undefined) {
|
|
62
|
+
if (!validators.requestBody(context.body)) {
|
|
63
|
+
return {
|
|
64
|
+
context,
|
|
65
|
+
response: {
|
|
66
|
+
status: requestErrorStatus,
|
|
67
|
+
body: {
|
|
68
|
+
error: "Request validation failed",
|
|
69
|
+
code: "REQUEST_VALIDATION_ERROR",
|
|
70
|
+
details: validators.requestBody.errors,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate request query parameters
|
|
78
|
+
if (validators.requestQuery && context.query) {
|
|
79
|
+
if (!validators.requestQuery(context.query)) {
|
|
80
|
+
return {
|
|
81
|
+
context,
|
|
82
|
+
response: {
|
|
83
|
+
status: requestErrorStatus,
|
|
84
|
+
body: {
|
|
85
|
+
error: "Query parameter validation failed",
|
|
86
|
+
code: "QUERY_VALIDATION_ERROR",
|
|
87
|
+
details: validators.requestQuery.errors,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate request headers
|
|
95
|
+
if (validators.requestHeaders && context.headers) {
|
|
96
|
+
// Lowercase all header names for comparison
|
|
97
|
+
const normalizedHeaders: Record<string, string> = {};
|
|
98
|
+
for (const [key, value] of Object.entries(context.headers)) {
|
|
99
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
100
|
+
}
|
|
101
|
+
if (!validators.requestHeaders(normalizedHeaders)) {
|
|
102
|
+
return {
|
|
103
|
+
context,
|
|
104
|
+
response: {
|
|
105
|
+
status: requestErrorStatus,
|
|
106
|
+
body: {
|
|
107
|
+
error: "Header validation failed",
|
|
108
|
+
code: "HEADER_VALIDATION_ERROR",
|
|
109
|
+
details: validators.requestHeaders.errors,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate response body (if response exists)
|
|
117
|
+
if (validators.responseBody && response !== undefined) {
|
|
118
|
+
// Unwrap tuple responses: [status, body] or [status, body, headers]
|
|
119
|
+
const isTuple =
|
|
120
|
+
Array.isArray(response) && typeof response[0] === "number";
|
|
121
|
+
const responseBody = isTuple ? response[1] : response;
|
|
122
|
+
|
|
123
|
+
if (!validators.responseBody(responseBody)) {
|
|
124
|
+
return {
|
|
125
|
+
context,
|
|
126
|
+
response: {
|
|
127
|
+
status: responseErrorStatus,
|
|
128
|
+
body: {
|
|
129
|
+
error: "Response validation failed",
|
|
130
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
131
|
+
details: validators.responseBody.errors,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { context, response };
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
|
+
import { schmock } from "@schmock/core";
|
|
3
|
+
import { expect } from "vitest";
|
|
4
|
+
import { validationPlugin } from "../index";
|
|
5
|
+
|
|
6
|
+
const feature = await loadFeature("../../features/validation-plugin.feature");
|
|
7
|
+
|
|
8
|
+
describeFeature(feature, ({ Scenario }) => {
|
|
9
|
+
let mock: any;
|
|
10
|
+
let response: any;
|
|
11
|
+
|
|
12
|
+
Scenario("Valid request body passes validation", ({ Given, When, Then, And }) => {
|
|
13
|
+
Given("I create a validated mock that requires name and email", () => {
|
|
14
|
+
mock = schmock();
|
|
15
|
+
mock("POST /users", ({ body }: any) => [201, body])
|
|
16
|
+
.pipe(validationPlugin({
|
|
17
|
+
request: {
|
|
18
|
+
body: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
name: { type: "string" },
|
|
22
|
+
email: { type: "string" },
|
|
23
|
+
},
|
|
24
|
+
required: ["name", "email"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
When("I send a valid POST with name {string} and email {string}", async (_, name: string, email: string) => {
|
|
31
|
+
response = await mock.handle("POST", "/users", {
|
|
32
|
+
body: { name, email },
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
37
|
+
expect(response.status).toBe(status);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
And("the response body should have property {string} with value {string}", (_, prop: string, value: string) => {
|
|
41
|
+
expect(response.body[prop]).toBe(value);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
Scenario("Invalid request body returns 400", ({ Given, When, Then, And }) => {
|
|
46
|
+
Given("I create a validated mock that requires name and email", () => {
|
|
47
|
+
mock = schmock();
|
|
48
|
+
mock("POST /users", ({ body }: any) => [201, body])
|
|
49
|
+
.pipe(validationPlugin({
|
|
50
|
+
request: {
|
|
51
|
+
body: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
name: { type: "string" },
|
|
55
|
+
email: { type: "string" },
|
|
56
|
+
},
|
|
57
|
+
required: ["name", "email"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
When("I send an invalid POST missing required fields", async () => {
|
|
64
|
+
response = await mock.handle("POST", "/users", {
|
|
65
|
+
body: { name: "John" },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
70
|
+
expect(response.status).toBe(status);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
And("the response body should have error code {string}", (_, code: string) => {
|
|
74
|
+
expect(response.body.code).toBe(code);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Scenario("Invalid response body returns 500", ({ Given, When, Then, And }) => {
|
|
79
|
+
Given("I create a mock with response validation that expects a number id", () => {
|
|
80
|
+
mock = schmock();
|
|
81
|
+
mock("GET /item", { id: "not-a-number", name: "Test" })
|
|
82
|
+
.pipe(validationPlugin({
|
|
83
|
+
response: {
|
|
84
|
+
body: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
id: { type: "number" },
|
|
88
|
+
name: { type: "string" },
|
|
89
|
+
},
|
|
90
|
+
required: ["id"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
When("I request the endpoint", async () => {
|
|
97
|
+
response = await mock.handle("GET", "/item");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
101
|
+
expect(response.status).toBe(status);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
And("the response body should have error code {string}", (_, code: string) => {
|
|
105
|
+
expect(response.body.code).toBe(code);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Scenario("Valid response passes validation", ({ Given, When, Then, And }) => {
|
|
110
|
+
Given("I create a mock with valid response and response validation", () => {
|
|
111
|
+
mock = schmock();
|
|
112
|
+
mock("GET /item", { id: 42, name: "Test" })
|
|
113
|
+
.pipe(validationPlugin({
|
|
114
|
+
response: {
|
|
115
|
+
body: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
id: { type: "number" },
|
|
119
|
+
name: { type: "string" },
|
|
120
|
+
},
|
|
121
|
+
required: ["id"],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
When("I request the endpoint", async () => {
|
|
128
|
+
response = await mock.handle("GET", "/item");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
132
|
+
expect(response.status).toBe(status);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
And("the response body should have property {string} with numeric value", (_, prop: string) => {
|
|
136
|
+
expect(typeof response.body[prop]).toBe("number");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
Scenario("Header validation rejects missing required headers", ({ Given, When, Then, And }) => {
|
|
141
|
+
Given("I create a mock requiring an authorization header", () => {
|
|
142
|
+
mock = schmock();
|
|
143
|
+
mock("GET /secure", { data: "secret" })
|
|
144
|
+
.pipe(validationPlugin({
|
|
145
|
+
request: {
|
|
146
|
+
headers: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
authorization: { type: "string" },
|
|
150
|
+
},
|
|
151
|
+
required: ["authorization"],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
When("I request without authorization header", async () => {
|
|
158
|
+
response = await mock.handle("GET", "/secure");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
162
|
+
expect(response.status).toBe(status);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
And("the response body should have error code {string}", (_, code: string) => {
|
|
166
|
+
expect(response.body.code).toBe(code);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
Scenario("Header validation passes with required headers", ({ Given, When, Then }) => {
|
|
171
|
+
Given("I create a mock requiring an authorization header", () => {
|
|
172
|
+
mock = schmock();
|
|
173
|
+
mock("GET /secure", { data: "secret" })
|
|
174
|
+
.pipe(validationPlugin({
|
|
175
|
+
request: {
|
|
176
|
+
headers: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
authorization: { type: "string" },
|
|
180
|
+
},
|
|
181
|
+
required: ["authorization"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
When("I request with authorization header {string}", async (_, token: string) => {
|
|
188
|
+
response = await mock.handle("GET", "/secure", {
|
|
189
|
+
headers: { authorization: token },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
194
|
+
expect(response.status).toBe(status);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
Scenario("Query parameter validation", ({ Given, When, Then, And }) => {
|
|
199
|
+
let invalidQueryResponse: any;
|
|
200
|
+
|
|
201
|
+
Given("I create a mock requiring page query parameter", () => {
|
|
202
|
+
mock = schmock();
|
|
203
|
+
mock("GET /items", [{ id: 1 }])
|
|
204
|
+
.pipe(validationPlugin({
|
|
205
|
+
request: {
|
|
206
|
+
query: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
page: { type: "string" },
|
|
210
|
+
},
|
|
211
|
+
required: ["page"],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
When("I request with query page {string}", async (_, page: string) => {
|
|
218
|
+
response = await mock.handle("GET", "/items", {
|
|
219
|
+
query: { page },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
224
|
+
expect(response.status).toBe(status);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
When("I request without required query parameter", async () => {
|
|
228
|
+
invalidQueryResponse = await mock.handle("GET", "/items");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
Then("the invalid query status should be {int}", (_, status: number) => {
|
|
232
|
+
expect(invalidQueryResponse.status).toBe(status);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
And("the invalid query response should have error code {string}", (_, code: string) => {
|
|
236
|
+
expect(invalidQueryResponse.body.code).toBe(code);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
Scenario("Custom error status codes", ({ Given, When, Then }) => {
|
|
241
|
+
Given("I create a validated mock with custom error status {int}", (_, status: number) => {
|
|
242
|
+
mock = schmock();
|
|
243
|
+
mock("POST /users", ({ body }: any) => [201, body])
|
|
244
|
+
.pipe(validationPlugin({
|
|
245
|
+
request: {
|
|
246
|
+
body: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
name: { type: "string" },
|
|
250
|
+
},
|
|
251
|
+
required: ["name"],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
requestErrorStatus: status,
|
|
255
|
+
}));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
When("I send an invalid request body", async () => {
|
|
259
|
+
response = await mock.handle("POST", "/users", {
|
|
260
|
+
body: {},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
Then("the status should be {int}", (_, status: number) => {
|
|
265
|
+
expect(response.status).toBe(status);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|