@typespec/spector 0.1.0-alpha.9 → 0.1.0-dev.1
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 +127 -0
- package/README.md +37 -0
- package/dist/generated-defs/TypeSpec.Spector.d.ts +4 -4
- package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -1
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js +6 -3
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -1
- package/dist/src/actions/helper.d.ts +8 -15
- package/dist/src/actions/helper.d.ts.map +1 -1
- package/dist/src/actions/helper.js +61 -69
- package/dist/src/actions/helper.js.map +1 -1
- package/dist/src/actions/serve.d.ts.map +1 -1
- package/dist/src/actions/serve.js +0 -1
- package/dist/src/actions/serve.js.map +1 -1
- package/dist/src/actions/server-test.d.ts +3 -5
- package/dist/src/actions/server-test.d.ts.map +1 -1
- package/dist/src/actions/server-test.js +108 -132
- package/dist/src/actions/server-test.js.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.d.ts +4 -3
- package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.js +20 -13
- package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
- package/dist/src/actions/validate-mock-apis.js +1 -1
- package/dist/src/actions/validate-mock-apis.js.map +1 -1
- package/dist/src/app/app.d.ts +1 -0
- package/dist/src/app/app.d.ts.map +1 -1
- package/dist/src/app/app.js +48 -54
- package/dist/src/app/app.js.map +1 -1
- package/dist/src/app/request-processor.d.ts +2 -2
- package/dist/src/app/request-processor.d.ts.map +1 -1
- package/dist/src/app/request-processor.js +10 -6
- package/dist/src/app/request-processor.js.map +1 -1
- package/dist/src/cli/cli.js +20 -21
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/config/config.js +2 -2
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/coverage/coverage-tracker.d.ts.map +1 -1
- package/dist/src/coverage/coverage-tracker.js.map +1 -1
- package/dist/src/coverage/scenario-manifest.d.ts +3 -2
- package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
- package/dist/src/coverage/scenario-manifest.js +17 -5
- package/dist/src/coverage/scenario-manifest.js.map +1 -1
- package/dist/src/logger.d.ts +16 -2
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +27 -8
- package/dist/src/logger.js.map +1 -1
- package/dist/src/server/server.d.ts +1 -1
- package/dist/src/server/server.d.ts.map +1 -1
- package/dist/src/server/server.js +17 -4
- package/dist/src/server/server.js.map +1 -1
- package/dist/src/utils/misc-utils.d.ts +5 -3
- package/dist/src/utils/misc-utils.d.ts.map +1 -1
- package/dist/src/utils/misc-utils.js +1 -1
- package/dist/src/utils/misc-utils.js.map +1 -1
- package/generated-defs/TypeSpec.Spector.ts +4 -3
- package/generated-defs/TypeSpec.Spector.ts-test.ts +8 -3
- package/lib/main.tsp +1 -1
- package/package.json +25 -31
- package/src/actions/helper.ts +79 -95
- package/src/actions/serve.ts +0 -1
- package/src/actions/server-test.ts +132 -156
- package/src/actions/upload-scenario-manifest.ts +29 -18
- package/src/actions/validate-mock-apis.ts +1 -1
- package/src/app/app.ts +71 -72
- package/src/app/request-processor.ts +16 -4
- package/src/cli/cli.ts +21 -21
- package/src/config/config.ts +2 -2
- package/src/coverage/coverage-tracker.ts +2 -2
- package/src/coverage/scenario-manifest.ts +18 -8
- package/src/logger.ts +39 -8
- package/src/scenarios-resolver.ts +1 -1
- package/src/server/server.ts +20 -6
- package/src/utils/misc-utils.ts +6 -4
- package/temp/.tsbuildinfo +1 -1
- package/vitest.config.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typespec/spector",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-dev.1",
|
|
4
4
|
"description": "Typespec Core Tool to validate, run mock api, collect coverage.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,50 +26,44 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/microsoft/typespec#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@azure/identity": "~4.
|
|
30
|
-
"@
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
29
|
+
"@azure/identity": "~4.13.0",
|
|
30
|
+
"@typespec/compiler": "^1.10.0 || >= 1.11.0-dev.3",
|
|
31
|
+
"@typespec/http": "^1.10.0 || >= 1.11.0-dev.1",
|
|
32
|
+
"@typespec/rest": "^0.80.0 || >= 0.81.0-dev.0",
|
|
33
|
+
"@typespec/spec-api": "^0.1.0-alpha.13 || >= 0.1.0-alpha.14-dev.1",
|
|
34
|
+
"@typespec/spec-coverage-sdk": "^0.1.0-alpha.16 || >= 0.1.0-dev.0",
|
|
35
|
+
"@typespec/versioning": "^0.80.0 || >= 0.81.0-dev.0",
|
|
36
|
+
"ajv": "~8.18.0",
|
|
37
|
+
"body-parser": "^2.2.0",
|
|
34
38
|
"deep-equal": "^2.2.0",
|
|
35
|
-
"express": "^
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"globby": "~14.1.0",
|
|
39
|
-
"jackspeak": "4.1.0",
|
|
40
|
-
"js-yaml": "^4.1.0",
|
|
39
|
+
"express": "^5.2.1",
|
|
40
|
+
"globby": "~16.1.0",
|
|
41
|
+
"micromatch": "^4.0.8",
|
|
41
42
|
"morgan": "^1.10.0",
|
|
42
|
-
"multer": "^
|
|
43
|
-
"node-fetch": "^3.3.1",
|
|
43
|
+
"multer": "^2.0.1",
|
|
44
44
|
"picocolors": "~1.1.1",
|
|
45
45
|
"source-map-support": "~0.5.21",
|
|
46
|
-
"winston": "^3.17.0",
|
|
47
46
|
"xml2js": "^0.6.2",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"@typespec/http": "^0.67.0",
|
|
51
|
-
"@typespec/spec-api": "^0.1.0-alpha.2",
|
|
52
|
-
"@typespec/rest": "^0.67.0",
|
|
53
|
-
"@typespec/spec-coverage-sdk": "^0.1.0-alpha.4",
|
|
54
|
-
"@typespec/versioning": "^0.67.0"
|
|
47
|
+
"yaml": "~2.8.2",
|
|
48
|
+
"yargs": "~18.0.0"
|
|
55
49
|
},
|
|
56
50
|
"devDependencies": {
|
|
57
51
|
"@types/body-parser": "^1.19.2",
|
|
58
52
|
"@types/deep-equal": "^1.0.1",
|
|
59
|
-
"@types/express": "^5.0.
|
|
60
|
-
"@types/
|
|
61
|
-
"@types/
|
|
62
|
-
"@types/
|
|
63
|
-
"@types/node
|
|
53
|
+
"@types/express": "^5.0.6",
|
|
54
|
+
"@types/micromatch": "^4.0.9",
|
|
55
|
+
"@types/morgan": "^1.9.9",
|
|
56
|
+
"@types/multer": "^2.0.0",
|
|
57
|
+
"@types/node": "~25.3.0",
|
|
64
58
|
"@types/xml2js": "^0.4.11",
|
|
65
59
|
"@types/yargs": "~17.0.33",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
60
|
+
"@typespec/tspd": "^0.74.1 || >= 0.74.2-dev.1",
|
|
61
|
+
"rimraf": "~6.1.3",
|
|
62
|
+
"typescript": "~5.9.3"
|
|
69
63
|
},
|
|
70
64
|
"scripts": {
|
|
71
65
|
"watch": "tsc -p ./tsconfig.build.json --watch",
|
|
72
|
-
"build": "
|
|
66
|
+
"build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json",
|
|
73
67
|
"clean": "rimraf dist/ temp/",
|
|
74
68
|
"gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
|
|
75
69
|
"test": "vitest run",
|
package/src/actions/helper.ts
CHANGED
|
@@ -1,113 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
expandDyns,
|
|
3
|
+
HttpMethod,
|
|
4
|
+
MockBody,
|
|
5
|
+
MockMultipartBody,
|
|
6
|
+
ResolverConfig,
|
|
7
|
+
} from "@typespec/spec-api";
|
|
4
8
|
|
|
5
9
|
export interface ServiceRequest {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
method: HttpMethod;
|
|
11
|
+
url: string;
|
|
12
|
+
body?: MockBody | MockMultipartBody;
|
|
13
|
+
headers?: Record<string, unknown>;
|
|
14
|
+
query?: Record<string, unknown>;
|
|
15
|
+
pathParams?: Record<string, unknown>;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
formData.append(key, JSON.stringify(request.options.requestBody[key]));
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (request.options.files) {
|
|
23
|
-
request.options.files.forEach((file) => {
|
|
24
|
-
formData.append(`${file.fieldname}`, file.buffer, {
|
|
25
|
-
filename: file.originalname,
|
|
26
|
-
contentType: file.mimetype,
|
|
27
|
-
});
|
|
28
|
-
});
|
|
18
|
+
function renderMultipartRequest(body: MockMultipartBody) {
|
|
19
|
+
const formData = new FormData();
|
|
20
|
+
if (body.parts) {
|
|
21
|
+
for (const key in body.parts) {
|
|
22
|
+
formData.append(key, JSON.stringify(body.parts[key]));
|
|
29
23
|
}
|
|
30
|
-
request.options.requestBody = formData;
|
|
31
|
-
request.options.config = {
|
|
32
|
-
...request.options.config,
|
|
33
|
-
headers: formData.getHeaders(),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function checkAndUpdateEndpoint(request: ServiceRequest) {
|
|
39
|
-
if (request.options?.config?.params) {
|
|
40
|
-
for (const key in request.options.config.params) {
|
|
41
|
-
request.endPoint = request.endPoint.replace(`:${key}`, request.options.config.params[key]);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
request.endPoint = request.endPoint.replace(/\[:\]/g, ":");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function makeServiceCall(
|
|
48
|
-
serviceCallType: HttpMethod,
|
|
49
|
-
request: ServiceRequest,
|
|
50
|
-
): Promise<AxiosResponse<any, any>> {
|
|
51
|
-
checkAndUpdateEndpoint(request);
|
|
52
|
-
checkAndAddFormDataIfRequired(request);
|
|
53
|
-
if (serviceCallType === "put") {
|
|
54
|
-
return await makePutCall(request);
|
|
55
|
-
}
|
|
56
|
-
if (serviceCallType === "post") {
|
|
57
|
-
return await makePostCall(request);
|
|
58
|
-
}
|
|
59
|
-
if (serviceCallType === "get") {
|
|
60
|
-
return await makeGetCall(request);
|
|
61
|
-
}
|
|
62
|
-
if (serviceCallType === "delete") {
|
|
63
|
-
return await makeDeleteCall(request);
|
|
64
24
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
25
|
+
if (body.files) {
|
|
26
|
+
body.files.forEach((file) => {
|
|
27
|
+
formData.append(
|
|
28
|
+
`${file.fieldname}`,
|
|
29
|
+
new Blob([file.buffer as any], { type: file.mimetype }),
|
|
30
|
+
file.originalname,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
67
33
|
}
|
|
68
|
-
return await makePatchCall(request);
|
|
69
|
-
}
|
|
70
34
|
|
|
71
|
-
|
|
72
|
-
const response = await axios.put(
|
|
73
|
-
request.endPoint,
|
|
74
|
-
request.options?.requestBody,
|
|
75
|
-
request.options?.config,
|
|
76
|
-
);
|
|
77
|
-
return response;
|
|
35
|
+
return formData;
|
|
78
36
|
}
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
request.endPoint,
|
|
83
|
-
request.options?.requestBody,
|
|
84
|
-
request.options?.config,
|
|
85
|
-
);
|
|
86
|
-
return response;
|
|
87
|
-
}
|
|
38
|
+
function resolveUrl(request: ServiceRequest) {
|
|
39
|
+
let endpoint = request.url;
|
|
88
40
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
41
|
+
if (request.pathParams) {
|
|
42
|
+
for (const [key, value] of Object.entries(request.pathParams)) {
|
|
43
|
+
endpoint = endpoint.replaceAll(`:${key}`, String(value));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
const response = await axios.patch(
|
|
96
|
-
request.endPoint,
|
|
97
|
-
request.options?.requestBody,
|
|
98
|
-
request.options?.config,
|
|
99
|
-
);
|
|
100
|
-
return response;
|
|
101
|
-
}
|
|
47
|
+
endpoint = endpoint.replaceAll("\\:", ":");
|
|
102
48
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
49
|
+
if (request.query) {
|
|
50
|
+
const query = new URLSearchParams();
|
|
51
|
+
for (const [key, value] of Object.entries(request.query)) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
for (const v of value) {
|
|
54
|
+
query.append(key, v);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
query.append(key, value as any);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
endpoint = `${endpoint}?${query.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
return endpoint;
|
|
106
63
|
}
|
|
107
64
|
|
|
108
|
-
export async function
|
|
109
|
-
|
|
110
|
-
|
|
65
|
+
export async function makeServiceCall(
|
|
66
|
+
request: ServiceRequest,
|
|
67
|
+
config: ResolverConfig,
|
|
68
|
+
): Promise<Response> {
|
|
69
|
+
const url = resolveUrl(request);
|
|
70
|
+
let body;
|
|
71
|
+
let headers = expandDyns(request.headers, config) as Record<string, string>;
|
|
72
|
+
if (request.body) {
|
|
73
|
+
if ("kind" in request.body) {
|
|
74
|
+
const formData = renderMultipartRequest(request.body);
|
|
75
|
+
body = formData;
|
|
76
|
+
} else {
|
|
77
|
+
if (typeof request.body.rawContent === "string" || Buffer.isBuffer(request.body.rawContent)) {
|
|
78
|
+
body = request.body.rawContent as any;
|
|
79
|
+
} else {
|
|
80
|
+
body = request.body.rawContent?.serialize(config);
|
|
81
|
+
}
|
|
82
|
+
headers = {
|
|
83
|
+
...headers,
|
|
84
|
+
...(request.body?.contentType && {
|
|
85
|
+
"Content-Type": request.body.contentType,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return await fetch(url, {
|
|
91
|
+
method: request.method.toUpperCase(),
|
|
92
|
+
body,
|
|
93
|
+
headers,
|
|
94
|
+
});
|
|
111
95
|
}
|
|
112
96
|
|
|
113
97
|
type EncodingType = "utf-8" | "base64" | "base64url" | "hex";
|
package/src/actions/serve.ts
CHANGED
|
@@ -1,157 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
expandDyns,
|
|
3
|
+
MockApiDefinition,
|
|
4
|
+
MockBody,
|
|
5
|
+
ResolverConfig,
|
|
6
|
+
ValidationError,
|
|
7
|
+
} from "@typespec/spec-api";
|
|
8
|
+
import deepEqual from "deep-equal";
|
|
9
|
+
import micromatch from "micromatch";
|
|
10
|
+
import { inspect } from "node:util";
|
|
4
11
|
import pc from "picocolors";
|
|
5
12
|
import { logger } from "../logger.js";
|
|
6
13
|
import { loadScenarioMockApis } from "../scenarios-resolver.js";
|
|
7
|
-
import { makeServiceCall
|
|
14
|
+
import { makeServiceCall } from "./helper.js";
|
|
8
15
|
|
|
9
16
|
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
10
17
|
|
|
11
18
|
export interface ServerTestDiagnostics {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
message: any;
|
|
19
|
+
scenarioName: string;
|
|
20
|
+
message: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
class ServerTestsGenerator {
|
|
18
24
|
private name: string = "";
|
|
19
25
|
private mockApiDefinition: MockApiDefinition;
|
|
20
26
|
private serverBasePath: string = "";
|
|
27
|
+
private resolverConfig: ResolverConfig;
|
|
21
28
|
|
|
22
29
|
constructor(name: string, mockApiDefinition: MockApiDefinition, serverBasePath: string) {
|
|
23
30
|
this.name = name;
|
|
24
31
|
this.mockApiDefinition = mockApiDefinition;
|
|
25
32
|
this.serverBasePath = serverBasePath;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let config = {};
|
|
30
|
-
if (this.mockApiDefinition.request.status) {
|
|
31
|
-
const validStatusCode = this.mockApiDefinition.request.status;
|
|
32
|
-
config = {
|
|
33
|
-
validateStatus: function (status: number) {
|
|
34
|
-
return (status >= 200 && status < 300) || validStatusCode === status;
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (this.mockApiDefinition.request.params) {
|
|
39
|
-
config = {
|
|
40
|
-
...config,
|
|
41
|
-
params: this.mockApiDefinition.request.params,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (this.mockApiDefinition.request.headers) {
|
|
45
|
-
config = {
|
|
46
|
-
...config,
|
|
47
|
-
headers: this.mockApiDefinition.request.headers,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
if (
|
|
51
|
-
["head", "get", "delete"].includes(this.mockApiDefinition.method) &&
|
|
52
|
-
this.mockApiDefinition.request.body
|
|
53
|
-
) {
|
|
54
|
-
config = {
|
|
55
|
-
...config,
|
|
56
|
-
data: this.mockApiDefinition.request.body,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
return config;
|
|
33
|
+
this.resolverConfig = {
|
|
34
|
+
baseUrl: serverBasePath,
|
|
35
|
+
};
|
|
60
36
|
}
|
|
61
37
|
|
|
62
38
|
public async executeScenario() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const response = await makeServiceCall(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
39
|
+
log(`Executing ${this.name} endpoint - Method: ${this.mockApiDefinition.method}`);
|
|
40
|
+
|
|
41
|
+
const response = await makeServiceCall(
|
|
42
|
+
{
|
|
43
|
+
method: this.mockApiDefinition.method,
|
|
44
|
+
url: `${this.serverBasePath}${this.mockApiDefinition.uri}`,
|
|
45
|
+
body: this.mockApiDefinition.request?.body,
|
|
46
|
+
headers: this.mockApiDefinition.request?.headers,
|
|
47
|
+
query: this.mockApiDefinition.request?.query,
|
|
48
|
+
pathParams: this.mockApiDefinition.request?.pathParams,
|
|
71
49
|
},
|
|
72
|
-
|
|
50
|
+
this.resolverConfig,
|
|
51
|
+
);
|
|
73
52
|
|
|
74
53
|
if (this.mockApiDefinition.response.status !== response.status) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
54
|
+
throw new ValidationError(
|
|
55
|
+
"Status code mismatch",
|
|
56
|
+
this.mockApiDefinition.response.status,
|
|
57
|
+
response.status,
|
|
78
58
|
);
|
|
79
|
-
throw new Error(`Status code mismatch for ${this.name} endpoint`);
|
|
80
59
|
}
|
|
60
|
+
|
|
81
61
|
if (this.mockApiDefinition.response.body) {
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
JSON.stringify(this.mockApiDefinition.response.body.rawContent) !==
|
|
85
|
-
JSON.stringify(response.data)
|
|
86
|
-
) {
|
|
87
|
-
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
88
|
-
logger.error(
|
|
89
|
-
`Expected: ${this.mockApiDefinition.response.body["rawContent"]} - Actual: ${response.data}`,
|
|
90
|
-
);
|
|
91
|
-
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
92
|
-
}
|
|
93
|
-
} else if (Buffer.isBuffer(this.mockApiDefinition.response.body.rawContent)) {
|
|
94
|
-
if (
|
|
95
|
-
this.mockApiDefinition.request.headers &&
|
|
96
|
-
this.mockApiDefinition.request.headers["accept"] === "application/json"
|
|
97
|
-
) {
|
|
98
|
-
if (
|
|
99
|
-
response.data.content !==
|
|
100
|
-
this.mockApiDefinition.response.body.rawContent.toString("base64")
|
|
101
|
-
) {
|
|
102
|
-
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
if (
|
|
106
|
-
uint8ArrayToString(response.data, "utf-8") !==
|
|
107
|
-
this.mockApiDefinition.response.body.rawContent.toString()
|
|
108
|
-
) {
|
|
109
|
-
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} else if (this.mockApiDefinition.response.body.contentType === "text/plain") {
|
|
113
|
-
if (this.mockApiDefinition.response.body.rawContent !== response.data) {
|
|
114
|
-
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
115
|
-
logger.error(
|
|
116
|
-
`Expected: ${this.mockApiDefinition.response.body} - Actual: ${response.data}`,
|
|
117
|
-
);
|
|
118
|
-
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
const responseData = JSON.stringify(response.data);
|
|
122
|
-
if (
|
|
123
|
-
this.mockApiDefinition.response.body.rawContent !==
|
|
124
|
-
responseData.replace(this.serverBasePath, "")
|
|
125
|
-
) {
|
|
126
|
-
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
127
|
-
logger.error(
|
|
128
|
-
`Expected: ${this.mockApiDefinition.response.body} - Actual: ${response.data}`,
|
|
129
|
-
);
|
|
130
|
-
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
62
|
+
await this.#validateBody(response, this.mockApiDefinition.response.body);
|
|
133
63
|
}
|
|
64
|
+
|
|
134
65
|
if (this.mockApiDefinition.response.headers) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`Expected: ${this.mockApiDefinition.response.headers[key]} - Actual: ${response.headers[key]}`,
|
|
66
|
+
const headers = expandDyns(this.mockApiDefinition.response.headers, this.resolverConfig);
|
|
67
|
+
for (const key in headers) {
|
|
68
|
+
if (headers[key] !== response.headers.get(key)) {
|
|
69
|
+
throw new ValidationError(
|
|
70
|
+
`Response headers mismatch`,
|
|
71
|
+
headers[key],
|
|
72
|
+
response.headers.get(key),
|
|
143
73
|
);
|
|
144
|
-
throw new Error(`Response headers mismatch for ${this.name} endpoint`);
|
|
145
74
|
}
|
|
146
75
|
}
|
|
147
76
|
}
|
|
148
77
|
}
|
|
78
|
+
|
|
79
|
+
async #validateBody(response: Response, body: MockBody) {
|
|
80
|
+
if (Buffer.isBuffer(body.rawContent)) {
|
|
81
|
+
const responseData = Buffer.from(await response.arrayBuffer());
|
|
82
|
+
if (!deepEqual(responseData, body.rawContent)) {
|
|
83
|
+
throw new ValidationError(`Raw body mismatch`, body.rawContent, responseData);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
const responseData = await response.text();
|
|
87
|
+
const raw =
|
|
88
|
+
typeof body.rawContent === "string"
|
|
89
|
+
? body.rawContent
|
|
90
|
+
: body.rawContent?.serialize(this.resolverConfig);
|
|
91
|
+
switch (body.contentType) {
|
|
92
|
+
case "application/xml":
|
|
93
|
+
case "text/plain":
|
|
94
|
+
if (raw !== responseData) {
|
|
95
|
+
throw new ValidationError("Response data mismatch", raw, responseData);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case "application/json":
|
|
99
|
+
const expected = JSON.parse(raw as any);
|
|
100
|
+
const actual = JSON.parse(responseData);
|
|
101
|
+
if (!deepEqual(actual, expected, { strict: true })) {
|
|
102
|
+
throw new ValidationError("Response data mismatch", expected, actual);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
149
107
|
}
|
|
150
108
|
|
|
151
109
|
export interface ServerTestOptions {
|
|
152
110
|
baseUrl?: string;
|
|
153
|
-
|
|
154
|
-
runScenariosFromFile?: string;
|
|
111
|
+
filter?: string;
|
|
155
112
|
}
|
|
156
113
|
|
|
157
114
|
async function delay(ms: number) {
|
|
@@ -159,7 +116,7 @@ async function delay(ms: number) {
|
|
|
159
116
|
}
|
|
160
117
|
|
|
161
118
|
async function waitForServer(baseUrl: string) {
|
|
162
|
-
logger.
|
|
119
|
+
logger.debug(`Executing server tests with base URL: ${baseUrl}`);
|
|
163
120
|
let retry = 0;
|
|
164
121
|
|
|
165
122
|
while (retry < 3) {
|
|
@@ -178,40 +135,44 @@ async function waitForServer(baseUrl: string) {
|
|
|
178
135
|
export async function serverTest(scenariosPath: string, options: ServerTestOptions = {}) {
|
|
179
136
|
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
180
137
|
await waitForServer(baseUrl);
|
|
181
|
-
// 1. Get Testcases to run
|
|
182
|
-
const testCasesToRun: string[] = [];
|
|
183
|
-
if (options.runSingleScenario) {
|
|
184
|
-
testCasesToRun.push(options.runSingleScenario);
|
|
185
|
-
} else if (options.runScenariosFromFile) {
|
|
186
|
-
const data = fs.readFileSync(path.resolve(options.runScenariosFromFile), "utf8");
|
|
187
|
-
const lines = data.split("\n");
|
|
188
|
-
lines.forEach((line) => {
|
|
189
|
-
testCasesToRun.push(line.trim());
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
// 2. Load all the scenarios
|
|
193
138
|
const scenarios = await loadScenarioMockApis(scenariosPath);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
139
|
+
const successfullScenarios: { name: string }[] = [];
|
|
140
|
+
const failureDiagnostics: ServerTestDiagnostics[] = [];
|
|
141
|
+
|
|
142
|
+
const allScenarioEntries = Object.entries(scenarios);
|
|
143
|
+
const scenarioEntries = allScenarioEntries.filter(([name]) => {
|
|
144
|
+
const pathlikeName = name.replaceAll("_", "/").toLowerCase();
|
|
145
|
+
const filter = options.filter?.toLowerCase();
|
|
146
|
+
if (filter && !micromatch.isMatch(pathlikeName, filter)) {
|
|
147
|
+
logger.debug(`Skipping scenario: ${pathlikeName}, does not match filter: ${filter}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
197
152
|
// 3. Execute each scenario
|
|
198
|
-
for (const [name, scenario] of
|
|
153
|
+
for (const [name, scenario] of scenarioEntries) {
|
|
199
154
|
if (!Array.isArray(scenario.apis)) continue;
|
|
200
155
|
for (const api of scenario.apis) {
|
|
201
156
|
if (api.kind !== "MockApiDefinition") continue;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
157
|
+
const obj: ServerTestsGenerator = new ServerTestsGenerator(name, api, baseUrl);
|
|
158
|
+
try {
|
|
159
|
+
await obj.executeScenario();
|
|
160
|
+
successfullScenarios.push({
|
|
161
|
+
name,
|
|
162
|
+
});
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
if (e instanceof ValidationError) {
|
|
165
|
+
failureDiagnostics.push({
|
|
166
|
+
scenarioName: name,
|
|
167
|
+
message: [
|
|
168
|
+
`Validation failed: ${e.message}:`,
|
|
169
|
+
` Expected:\n ${inspect(e.expected)}`,
|
|
170
|
+
` Actual:\n ${inspect(e.actual)}`,
|
|
171
|
+
].join("\n"),
|
|
210
172
|
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
status: "failure",
|
|
173
|
+
} else {
|
|
174
|
+
failureDiagnostics.push({
|
|
175
|
+
scenarioName: name,
|
|
215
176
|
message: `code = ${e.code} \n message = ${e.message} \n name = ${e.name} \n stack = ${e.stack} \n status = ${e.status}`,
|
|
216
177
|
});
|
|
217
178
|
}
|
|
@@ -220,20 +181,35 @@ export async function serverTest(scenariosPath: string, options: ServerTestOptio
|
|
|
220
181
|
}
|
|
221
182
|
|
|
222
183
|
// 4. Print diagnostics
|
|
223
|
-
|
|
184
|
+
log("");
|
|
185
|
+
log("Server Tests Diagnostics Summary");
|
|
186
|
+
|
|
187
|
+
if (successfullScenarios.length === 0 && failureDiagnostics.length === 0) {
|
|
188
|
+
logger.error("No scenarios were executed");
|
|
189
|
+
process.exit(-1);
|
|
190
|
+
}
|
|
224
191
|
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
192
|
+
if (successfullScenarios.length > 0) log("Successfull scenarios");
|
|
193
|
+
successfullScenarios.forEach((diagnostic) => {
|
|
194
|
+
log(`${pc.green("✓")} Scenario: ${pc.cyan(diagnostic.name)}`);
|
|
228
195
|
});
|
|
229
196
|
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
logger.error(`${diagnostic.message}`);
|
|
197
|
+
if (failureDiagnostics.length > 0) {
|
|
198
|
+
log("Failed scenarios");
|
|
199
|
+
failureDiagnostics.forEach((diagnostic) => {
|
|
200
|
+
log(`${pc.red("✘")} Scenario: ${pc.cyan(diagnostic.scenarioName)}`);
|
|
201
|
+
log(`${diagnostic.message}`);
|
|
236
202
|
});
|
|
237
|
-
process.exit(-1);
|
|
238
203
|
}
|
|
204
|
+
log(pc.bold(pc.green(`✓ ${scenarioEntries.length} passed`)));
|
|
205
|
+
if (failureDiagnostics.length > 0) {
|
|
206
|
+
log(pc.red(`✘ ${failureDiagnostics.length} failed`));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.exit(failureDiagnostics.length > 0 ? 1 : 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function log(message: string) {
|
|
213
|
+
// eslint-disable-next-line no-console
|
|
214
|
+
console.log(message);
|
|
239
215
|
}
|