@typespec/spec-api 0.1.0-alpha.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/CHANGELOG.md +2 -0
- package/LICENSE +21 -0
- package/dist/expectation.d.ts +63 -0
- package/dist/expectation.d.ts.map +1 -0
- package/dist/expectation.js +87 -0
- package/dist/expectation.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-request.d.ts +22 -0
- package/dist/mock-request.d.ts.map +1 -0
- package/dist/mock-request.js +23 -0
- package/dist/mock-request.js.map +1 -0
- package/dist/request-validations.d.ts +24 -0
- package/dist/request-validations.d.ts.map +1 -0
- package/dist/request-validations.js +164 -0
- package/dist/request-validations.js.map +1 -0
- package/dist/response-utils.d.ts +15 -0
- package/dist/response-utils.d.ts.map +1 -0
- package/dist/response-utils.js +24 -0
- package/dist/response-utils.js.map +1 -0
- package/dist/routes.d.ts +65 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +79 -0
- package/dist/routes.js.map +1 -0
- package/dist/scenarios.d.ts +26 -0
- package/dist/scenarios.d.ts.map +1 -0
- package/dist/scenarios.js +50 -0
- package/dist/scenarios.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation-error.d.ts +14 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +20 -0
- package/dist/validation-error.js.map +1 -0
- package/package.json +54 -0
- package/src/expectation.ts +106 -0
- package/src/index.ts +48 -0
- package/src/mock-request.ts +31 -0
- package/src/request-validations.ts +216 -0
- package/src/response-utils.ts +26 -0
- package/src/routes.ts +92 -0
- package/src/scenarios.ts +76 -0
- package/src/types.ts +125 -0
- package/src/validation-error.ts +21 -0
- package/temp/.tsbuildinfo +1 -0
- package/test/expectation.test.ts +53 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import deepEqual from "deep-equal";
|
|
2
|
+
import * as prettier from "prettier";
|
|
3
|
+
import { parseString } from "xml2js";
|
|
4
|
+
import { CollectionFormat, RequestExt } from "./types.js";
|
|
5
|
+
import { ValidationError } from "./validation-error.js";
|
|
6
|
+
|
|
7
|
+
export const BODY_NOT_EQUAL_ERROR_MESSAGE = "Body provided doesn't match expected body";
|
|
8
|
+
export const BODY_EMPTY_ERROR_MESSAGE = "Body should exists";
|
|
9
|
+
export const BODY_NOT_EMPTY_ERROR_MESSAGE = "Body should be empty";
|
|
10
|
+
|
|
11
|
+
export const validateRawBodyEquals = (
|
|
12
|
+
request: RequestExt,
|
|
13
|
+
expectedRawBody: string | Buffer | undefined,
|
|
14
|
+
): void => {
|
|
15
|
+
const actualRawBody = request.rawBody;
|
|
16
|
+
|
|
17
|
+
if (expectedRawBody == null) {
|
|
18
|
+
if (!isBodyEmpty(actualRawBody)) {
|
|
19
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedRawBody, actualRawBody);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!deepEqual(actualRawBody, expectedRawBody, { strict: true })) {
|
|
25
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedRawBody, actualRawBody);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const validateBodyEquals = (
|
|
30
|
+
request: RequestExt,
|
|
31
|
+
expectedBody: unknown | undefined,
|
|
32
|
+
): void => {
|
|
33
|
+
if (expectedBody == null) {
|
|
34
|
+
if (!isBodyEmpty(request.rawBody)) {
|
|
35
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.rawBody);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!deepEqual(request.body, expectedBody, { strict: true })) {
|
|
41
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.body);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const validateXmlBodyEquals = (request: RequestExt, expectedBody: string): void => {
|
|
46
|
+
if (request.rawBody === undefined || isBodyEmpty(request.rawBody)) {
|
|
47
|
+
throw new ValidationError(BODY_EMPTY_ERROR_MESSAGE, expectedBody, request.rawBody);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expectedBody = `<?xml version='1.0' encoding='UTF-8'?>` + expectedBody;
|
|
51
|
+
|
|
52
|
+
let actualParsedBody = "";
|
|
53
|
+
parseString(request.rawBody, (err: Error | null, result: any): void => {
|
|
54
|
+
if (err !== null) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
actualParsedBody = result;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let expectedParsedBody = "";
|
|
61
|
+
parseString(expectedBody, (err: Error | null, result: any): void => {
|
|
62
|
+
if (err !== null) {
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
expectedParsedBody = result;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!deepEqual(actualParsedBody, expectedParsedBody, { strict: true })) {
|
|
69
|
+
throw new ValidationError(
|
|
70
|
+
BODY_NOT_EQUAL_ERROR_MESSAGE,
|
|
71
|
+
prettier.format(expectedBody),
|
|
72
|
+
prettier.format(request.body),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const validateCoercedDateBodyEquals = (
|
|
78
|
+
request: RequestExt,
|
|
79
|
+
expectedBody: unknown | undefined,
|
|
80
|
+
): void => {
|
|
81
|
+
if (expectedBody == null) {
|
|
82
|
+
if (!isBodyEmpty(request.rawBody)) {
|
|
83
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.rawBody);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!deepEqual(coerceDate(request.body), expectedBody, { strict: true })) {
|
|
89
|
+
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.body);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const validateBodyEmpty = (request: RequestExt): void => {
|
|
94
|
+
if (isBodyEmpty(request.rawBody)) {
|
|
95
|
+
if (request.body instanceof Buffer) {
|
|
96
|
+
if (request.body.length > 0) {
|
|
97
|
+
throw new ValidationError(BODY_NOT_EMPTY_ERROR_MESSAGE, undefined, request.rawBody);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
throw new ValidationError(BODY_EMPTY_ERROR_MESSAGE, undefined, request.rawBody);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const validateBodyNotEmpty = (request: RequestExt): void => {
|
|
106
|
+
if (isBodyEmpty(request.rawBody)) {
|
|
107
|
+
if (request.body instanceof Buffer) {
|
|
108
|
+
if (request.body.length === 0) {
|
|
109
|
+
throw new ValidationError(BODY_EMPTY_ERROR_MESSAGE, undefined, request.rawBody);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
throw new ValidationError(BODY_EMPTY_ERROR_MESSAGE, undefined, request.rawBody);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if the provided body is empty.
|
|
119
|
+
* @param body express.js request body.
|
|
120
|
+
*/
|
|
121
|
+
const isBodyEmpty = (body: string | Buffer | undefined | null) => {
|
|
122
|
+
return body == null || body === "" || body.length === 0;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check whether the request header contains the given name/value pair
|
|
127
|
+
*/
|
|
128
|
+
export const validateHeader = (request: RequestExt, headerName: string, expected: string): void => {
|
|
129
|
+
const actual = request.headers[headerName];
|
|
130
|
+
if (actual !== expected) {
|
|
131
|
+
throw new ValidationError(`Expected ${expected} but got ${actual}`, expected, actual);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check whether the query string contains the given parameter name and value.
|
|
137
|
+
* Supports query param as string or collection. e.g. if it's a collection, one can call the method like this: validateQueryParam(request, ["a", "b", "c"], "multi")
|
|
138
|
+
*/
|
|
139
|
+
export const validateQueryParam = (
|
|
140
|
+
request: RequestExt,
|
|
141
|
+
paramName: string,
|
|
142
|
+
expected: string | string[],
|
|
143
|
+
collectionFormat?: CollectionFormat,
|
|
144
|
+
): void => {
|
|
145
|
+
const actual = request.query[paramName];
|
|
146
|
+
const splitterMap = {
|
|
147
|
+
csv: ",",
|
|
148
|
+
ssv: " ",
|
|
149
|
+
tsv: "\t",
|
|
150
|
+
pipes: "|",
|
|
151
|
+
};
|
|
152
|
+
let isExpected = false;
|
|
153
|
+
if (collectionFormat && Array.isArray(expected)) {
|
|
154
|
+
// verify query parameter as collection
|
|
155
|
+
if (collectionFormat === "multi" && Array.isArray(actual)) {
|
|
156
|
+
isExpected = deepEqual(actual, expected);
|
|
157
|
+
} else if (collectionFormat !== "multi" && typeof actual === "string") {
|
|
158
|
+
const expectedString = expected.join(splitterMap[collectionFormat]);
|
|
159
|
+
isExpected = expectedString === decodeURIComponent(actual);
|
|
160
|
+
}
|
|
161
|
+
if (!isExpected) {
|
|
162
|
+
throw new ValidationError(
|
|
163
|
+
`Expected query param collection ${paramName}=${expected} in ${collectionFormat}, but got ${actual}`,
|
|
164
|
+
expected,
|
|
165
|
+
actual,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
} else if (actual !== expected) {
|
|
169
|
+
throw new ValidationError(
|
|
170
|
+
`Expected query param ${paramName}=${expected} but got ${actual}`,
|
|
171
|
+
expected,
|
|
172
|
+
actual,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const coerceDate = (targetObject: Record<string, unknown>): Record<string, unknown> => {
|
|
178
|
+
let stringRep = JSON.stringify(targetObject);
|
|
179
|
+
stringRep = stringRep.replace(
|
|
180
|
+
/(\d\d\d\d-\d\d-\d\d[Tt]\d\d:\d\d:\d\d)(\.\d{3,7})?([Zz]|[+-]00:00)/g,
|
|
181
|
+
"$1Z",
|
|
182
|
+
);
|
|
183
|
+
return JSON.parse(stringRep);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check whether the value follow the right format.
|
|
188
|
+
*/
|
|
189
|
+
export const validateValueFormat = (
|
|
190
|
+
value: string,
|
|
191
|
+
format: "uuid" | "rfc7231" | "rfc3339",
|
|
192
|
+
): void => {
|
|
193
|
+
switch (format) {
|
|
194
|
+
case "uuid":
|
|
195
|
+
if (!/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value)) {
|
|
196
|
+
throw new ValidationError(`Expected uuid format but got ${value}`, "uuid", value);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
case "rfc7231":
|
|
200
|
+
if (
|
|
201
|
+
!/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT$/i.test(
|
|
202
|
+
value,
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
throw new ValidationError(`Expected rfc7231 format but got ${value}`, "rfc7231", value);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
case "rfc3339":
|
|
209
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/i.test(value)) {
|
|
210
|
+
throw new ValidationError(`Expected rfc3339 format but got ${value}`, "rfc3339", value);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
throw new ValidationError(`Unsupported format ${format}`, format, value);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MockResponseBody } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serialize the provided content as json to use in a MockResponse body.
|
|
5
|
+
* @content Object to return as json.
|
|
6
|
+
* @returns {MockResponseBody} response body with application/json content type.
|
|
7
|
+
*/
|
|
8
|
+
export function json(content: unknown): MockResponseBody {
|
|
9
|
+
return {
|
|
10
|
+
contentType: "application/json",
|
|
11
|
+
rawContent: JSON.stringify(content),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sends the provided XML string in a MockResponse body.
|
|
17
|
+
* The XML declaration prefix will automatically be added to xmlString.
|
|
18
|
+
* @content Object to return as XML.
|
|
19
|
+
* @returns {MockResponseBody} response body with application/xml content type.
|
|
20
|
+
*/
|
|
21
|
+
export function xml(xmlString: string): MockResponseBody {
|
|
22
|
+
return {
|
|
23
|
+
contentType: "application/xml",
|
|
24
|
+
rawContent: `<?xml version='1.0' encoding='UTF-8'?>` + xmlString,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { HttpMethod, MockApiForHandler, MockRequestHandler } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register a GET request for the provided uri.
|
|
5
|
+
* @param uri URI to match.
|
|
6
|
+
* @param func Request handler.
|
|
7
|
+
*/
|
|
8
|
+
function get<const T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
9
|
+
return request("get", uri, func);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register a POST request for the provided uri.
|
|
14
|
+
* @param uri URI to match.
|
|
15
|
+
* @param func Request handler.
|
|
16
|
+
*/
|
|
17
|
+
function post<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
18
|
+
return request("post", uri, func);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a PUT request for the provided uri.
|
|
23
|
+
* @param uri URI to match.
|
|
24
|
+
* @param func Request handler.
|
|
25
|
+
*/
|
|
26
|
+
function put<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
27
|
+
return request("put", uri, func);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a PATCH request for the provided uri.
|
|
32
|
+
* @param uri URI to match.
|
|
33
|
+
* @param func Request handler.
|
|
34
|
+
*/
|
|
35
|
+
function patch<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
36
|
+
return request("patch", uri, func);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register a DELETE request for the provided uri.
|
|
41
|
+
* @param uri URI to match.
|
|
42
|
+
* @param func Request handler.
|
|
43
|
+
*/
|
|
44
|
+
function deleteReq<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
45
|
+
return request("delete", uri, func);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a Options request for the provided uri.
|
|
50
|
+
* @param uri URI to match.
|
|
51
|
+
* @param func Request handler.
|
|
52
|
+
*/
|
|
53
|
+
function options<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
54
|
+
return request("options", uri, func);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register a HEAD request for the provided uri.
|
|
59
|
+
* @param uri URI to match.
|
|
60
|
+
* @param name Name of the scenario(For coverage).
|
|
61
|
+
* @param func Request handler.
|
|
62
|
+
*/
|
|
63
|
+
function head<T extends MockRequestHandler>(uri: string, func: T): MockApiForHandler<T> {
|
|
64
|
+
return request<T>("head", uri, func);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register a request for the provided uri.
|
|
69
|
+
* @param method Method to use.
|
|
70
|
+
* @param uri URI to match.
|
|
71
|
+
* @param func Request handler.
|
|
72
|
+
*
|
|
73
|
+
* @note prefer to use the corresponding method method directly instead of `request()`(i.e `get(), post()`)
|
|
74
|
+
*/
|
|
75
|
+
function request<T extends MockRequestHandler>(
|
|
76
|
+
method: HttpMethod,
|
|
77
|
+
uri: string,
|
|
78
|
+
handler: T,
|
|
79
|
+
): MockApiForHandler<T> {
|
|
80
|
+
return { method, uri, handler } as any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const mockapi = {
|
|
84
|
+
get,
|
|
85
|
+
post,
|
|
86
|
+
put,
|
|
87
|
+
patch,
|
|
88
|
+
delete: deleteReq,
|
|
89
|
+
options,
|
|
90
|
+
head,
|
|
91
|
+
request,
|
|
92
|
+
};
|
package/src/scenarios.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KeyedMockApi,
|
|
3
|
+
KeyedMockApiDefinition,
|
|
4
|
+
MockApi,
|
|
5
|
+
MockApiDefinition,
|
|
6
|
+
PassByKeyScenario,
|
|
7
|
+
PassByServiceKeyScenario,
|
|
8
|
+
PassOnCodeScenario,
|
|
9
|
+
PassOnSuccessScenario,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Specify that the scenario should be a `pass` if all the endpoints are called and the API response with 2xx exit code.
|
|
14
|
+
* @param apis Endpoint or List of endpoints for this scenario
|
|
15
|
+
*/
|
|
16
|
+
export function passOnSuccess(
|
|
17
|
+
apis: MockApi | readonly MockApi[] | MockApiDefinition | readonly MockApiDefinition[],
|
|
18
|
+
): PassOnSuccessScenario {
|
|
19
|
+
return {
|
|
20
|
+
passCondition: "response-success",
|
|
21
|
+
apis: Array.isArray(apis) ? apis : [apis],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Specify that the scenario should be a `pass` if all the endpoints are called and the API response with the given exit code.
|
|
26
|
+
* @param code Status code all endpoint should return
|
|
27
|
+
* @param apis Endpoint or List of endpoints for this scenario
|
|
28
|
+
*/
|
|
29
|
+
export function passOnCode(
|
|
30
|
+
code: number,
|
|
31
|
+
apis: MockApi | readonly MockApi[] | MockApiDefinition,
|
|
32
|
+
): PassOnCodeScenario {
|
|
33
|
+
return {
|
|
34
|
+
passCondition: "status-code",
|
|
35
|
+
code,
|
|
36
|
+
apis: Array.isArray(apis) ? apis : [apis],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WithKeysScenarioExpect<K extends string> {
|
|
41
|
+
pass(api: KeyedMockApi<K>): PassByKeyScenario<K>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Specify a list of keys that must be hit to this scenario to pass
|
|
45
|
+
* @param keys List of keys
|
|
46
|
+
* @param api Mock api that in the MockResponse can return a pass key.
|
|
47
|
+
*/
|
|
48
|
+
export function withKeys<const K extends string>(keys: K[]): WithKeysScenarioExpect<K> {
|
|
49
|
+
return {
|
|
50
|
+
pass: (api) => {
|
|
51
|
+
return {
|
|
52
|
+
passCondition: "by-key",
|
|
53
|
+
keys,
|
|
54
|
+
apis: [api],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WithServiceKeysScenarioExpect<K extends string> {
|
|
61
|
+
pass(api: KeyedMockApiDefinition<K> | KeyedMockApiDefinition<K>[]): PassByServiceKeyScenario<K>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function withServiceKeys<const K extends string>(
|
|
65
|
+
keys: K[],
|
|
66
|
+
): WithServiceKeysScenarioExpect<K> {
|
|
67
|
+
return {
|
|
68
|
+
pass: (api: KeyedMockApiDefinition<K> | KeyedMockApiDefinition<K>[]) => {
|
|
69
|
+
return {
|
|
70
|
+
passCondition: "by-key",
|
|
71
|
+
keys,
|
|
72
|
+
apis: Array.isArray(api) ? api : [api],
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Request } from "express";
|
|
2
|
+
import { MockRequest } from "./mock-request.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extension of the express.js request which include a rawBody.
|
|
6
|
+
*/
|
|
7
|
+
export interface RequestExt extends Request {
|
|
8
|
+
rawBody?: string | Buffer;
|
|
9
|
+
files?:
|
|
10
|
+
| {
|
|
11
|
+
[fieldname: string]: Express.Multer.File[];
|
|
12
|
+
}
|
|
13
|
+
| Express.Multer.File[]
|
|
14
|
+
| undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ScenarioPassCondition = "response-success" | "status-code";
|
|
18
|
+
|
|
19
|
+
export interface PassOnSuccessScenario {
|
|
20
|
+
passCondition: "response-success";
|
|
21
|
+
apis: MockApi[] | MockApiDefinition[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PassOnCodeScenario {
|
|
25
|
+
passCondition: "status-code";
|
|
26
|
+
code: number;
|
|
27
|
+
apis: MockApi[] | MockApiDefinition[];
|
|
28
|
+
}
|
|
29
|
+
export interface PassByKeyScenario<K extends string = string> {
|
|
30
|
+
passCondition: "by-key";
|
|
31
|
+
keys: K[];
|
|
32
|
+
apis: KeyedMockApi<K>[];
|
|
33
|
+
}
|
|
34
|
+
export interface PassByServiceKeyScenario<K extends string = string> {
|
|
35
|
+
passCondition: "by-key";
|
|
36
|
+
keys: K[];
|
|
37
|
+
apis: KeyedMockApiDefinition<K>[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ScenarioMockApi =
|
|
41
|
+
| PassOnSuccessScenario
|
|
42
|
+
| PassOnCodeScenario
|
|
43
|
+
| PassByKeyScenario
|
|
44
|
+
| PassByServiceKeyScenario;
|
|
45
|
+
export type MockRequestHandler = SimpleMockRequestHandler | KeyedMockRequestHandler;
|
|
46
|
+
export type SimpleMockRequestHandler = (req: MockRequest) => MockResponse | Promise<MockResponse>;
|
|
47
|
+
export type KeyedMockRequestHandler<T extends string = string> = (
|
|
48
|
+
req: MockRequest,
|
|
49
|
+
) => KeyedMockResponse<T> | Promise<KeyedMockResponse<T>>;
|
|
50
|
+
export type KeyedServiceRequestHandler<T extends string = string> = (
|
|
51
|
+
req: ServiceRequest,
|
|
52
|
+
) => KeyedMockResponse<T> | Promise<KeyedMockResponse<T>>;
|
|
53
|
+
|
|
54
|
+
export type HttpMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options";
|
|
55
|
+
|
|
56
|
+
export type MockApiForHandler<Handler extends MockRequestHandler> =
|
|
57
|
+
Handler extends KeyedMockRequestHandler<infer K> ? KeyedMockApi<K> : MockApi;
|
|
58
|
+
|
|
59
|
+
export interface MockApi {
|
|
60
|
+
method: HttpMethod;
|
|
61
|
+
uri: string;
|
|
62
|
+
handler: MockRequestHandler;
|
|
63
|
+
kind: "MockApi";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MockApiDefinition {
|
|
67
|
+
uri: string;
|
|
68
|
+
method: HttpMethod;
|
|
69
|
+
request: ServiceRequest;
|
|
70
|
+
response: MockResponse;
|
|
71
|
+
handler?: MockRequestHandler;
|
|
72
|
+
kind: "MockApiDefinition";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ServiceRequestFile {
|
|
76
|
+
fieldname: string;
|
|
77
|
+
originalname: string;
|
|
78
|
+
buffer: Buffer;
|
|
79
|
+
mimetype: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ServiceRequest {
|
|
83
|
+
body?: any;
|
|
84
|
+
status?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Query parameters to match to the request.
|
|
87
|
+
*/
|
|
88
|
+
params?: Record<string, unknown>;
|
|
89
|
+
headers?: Record<string, unknown>;
|
|
90
|
+
files?: ServiceRequestFile[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Fail = Symbol.for("Fail");
|
|
94
|
+
export interface KeyedMockApi<K extends string> extends MockApi {
|
|
95
|
+
handler: KeyedMockRequestHandler<K>;
|
|
96
|
+
}
|
|
97
|
+
export interface KeyedMockApiDefinition<K extends string> extends MockApiDefinition {
|
|
98
|
+
handler: KeyedMockRequestHandler<K>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface MockResponse {
|
|
102
|
+
status: number;
|
|
103
|
+
headers?: {
|
|
104
|
+
[key: string]: string | null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
body?: MockResponseBody;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Let the mock API know that this request was successful to counting coverage regardless of the status code.
|
|
111
|
+
* By default only 2xx status code will count toward success.
|
|
112
|
+
*/
|
|
113
|
+
testSuccessful?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface KeyedMockResponse<K extends string = string> extends MockResponse {
|
|
117
|
+
pass: K | typeof Fail;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface MockResponseBody {
|
|
121
|
+
contentType: string;
|
|
122
|
+
rawContent: string | Buffer | undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type CollectionFormat = "multi" | "csv" | "ssv" | "tsv" | "pipes";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class ValidationError extends Error {
|
|
2
|
+
public status = 400;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown there there is a validation issue.
|
|
6
|
+
* @param message Message describing the error.
|
|
7
|
+
* @param expected expected value.
|
|
8
|
+
* @param actual actual value.
|
|
9
|
+
*/
|
|
10
|
+
constructor(
|
|
11
|
+
message: string,
|
|
12
|
+
public expected: unknown | undefined,
|
|
13
|
+
public actual: unknown | undefined,
|
|
14
|
+
) {
|
|
15
|
+
super(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public toJSON(): string {
|
|
19
|
+
return JSON.stringify({ message: this.message, expected: this.expected, actual: this.actual });
|
|
20
|
+
}
|
|
21
|
+
}
|