@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +37 -0
  3. package/dist/generated-defs/TypeSpec.Spector.d.ts +4 -4
  4. package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -1
  5. package/dist/generated-defs/TypeSpec.Spector.ts-test.js +6 -3
  6. package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -1
  7. package/dist/src/actions/helper.d.ts +8 -15
  8. package/dist/src/actions/helper.d.ts.map +1 -1
  9. package/dist/src/actions/helper.js +61 -69
  10. package/dist/src/actions/helper.js.map +1 -1
  11. package/dist/src/actions/serve.d.ts.map +1 -1
  12. package/dist/src/actions/serve.js +0 -1
  13. package/dist/src/actions/serve.js.map +1 -1
  14. package/dist/src/actions/server-test.d.ts +3 -5
  15. package/dist/src/actions/server-test.d.ts.map +1 -1
  16. package/dist/src/actions/server-test.js +108 -132
  17. package/dist/src/actions/server-test.js.map +1 -1
  18. package/dist/src/actions/upload-scenario-manifest.d.ts +4 -3
  19. package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
  20. package/dist/src/actions/upload-scenario-manifest.js +20 -13
  21. package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
  22. package/dist/src/actions/validate-mock-apis.js +1 -1
  23. package/dist/src/actions/validate-mock-apis.js.map +1 -1
  24. package/dist/src/app/app.d.ts +1 -0
  25. package/dist/src/app/app.d.ts.map +1 -1
  26. package/dist/src/app/app.js +48 -54
  27. package/dist/src/app/app.js.map +1 -1
  28. package/dist/src/app/request-processor.d.ts +2 -2
  29. package/dist/src/app/request-processor.d.ts.map +1 -1
  30. package/dist/src/app/request-processor.js +10 -6
  31. package/dist/src/app/request-processor.js.map +1 -1
  32. package/dist/src/cli/cli.js +20 -21
  33. package/dist/src/cli/cli.js.map +1 -1
  34. package/dist/src/config/config.js +2 -2
  35. package/dist/src/config/config.js.map +1 -1
  36. package/dist/src/coverage/coverage-tracker.d.ts.map +1 -1
  37. package/dist/src/coverage/coverage-tracker.js.map +1 -1
  38. package/dist/src/coverage/scenario-manifest.d.ts +3 -2
  39. package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
  40. package/dist/src/coverage/scenario-manifest.js +17 -5
  41. package/dist/src/coverage/scenario-manifest.js.map +1 -1
  42. package/dist/src/logger.d.ts +16 -2
  43. package/dist/src/logger.d.ts.map +1 -1
  44. package/dist/src/logger.js +27 -8
  45. package/dist/src/logger.js.map +1 -1
  46. package/dist/src/server/server.d.ts +1 -1
  47. package/dist/src/server/server.d.ts.map +1 -1
  48. package/dist/src/server/server.js +17 -4
  49. package/dist/src/server/server.js.map +1 -1
  50. package/dist/src/utils/misc-utils.d.ts +5 -3
  51. package/dist/src/utils/misc-utils.d.ts.map +1 -1
  52. package/dist/src/utils/misc-utils.js +1 -1
  53. package/dist/src/utils/misc-utils.js.map +1 -1
  54. package/generated-defs/TypeSpec.Spector.ts +4 -3
  55. package/generated-defs/TypeSpec.Spector.ts-test.ts +8 -3
  56. package/lib/main.tsp +1 -1
  57. package/package.json +25 -31
  58. package/src/actions/helper.ts +79 -95
  59. package/src/actions/serve.ts +0 -1
  60. package/src/actions/server-test.ts +132 -156
  61. package/src/actions/upload-scenario-manifest.ts +29 -18
  62. package/src/actions/validate-mock-apis.ts +1 -1
  63. package/src/app/app.ts +71 -72
  64. package/src/app/request-processor.ts +16 -4
  65. package/src/cli/cli.ts +21 -21
  66. package/src/config/config.ts +2 -2
  67. package/src/coverage/coverage-tracker.ts +2 -2
  68. package/src/coverage/scenario-manifest.ts +18 -8
  69. package/src/logger.ts +39 -8
  70. package/src/scenarios-resolver.ts +1 -1
  71. package/src/server/server.ts +20 -6
  72. package/src/utils/misc-utils.ts +6 -4
  73. package/temp/.tsbuildinfo +1 -1
  74. 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-alpha.9",
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.7.0",
30
- "@types/js-yaml": "^4.0.5",
31
- "ajv": "~8.17.1",
32
- "axios": "^1.8.1",
33
- "body-parser": "^1.20.3",
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": "^4.21.2",
36
- "express-promise-router": "^4.1.1",
37
- "form-data": "^4.0.2",
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": "^1.4.5-lts.1",
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
- "yargs": "~17.7.2",
49
- "@typespec/compiler": "^0.67.0",
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.0",
60
- "@types/morgan": "^1.9.4",
61
- "@types/multer": "^1.4.10",
62
- "@types/node": "~22.13.9",
63
- "@types/node-fetch": "^2.6.12",
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
- "rimraf": "~6.0.1",
67
- "typescript": "~5.8.2",
68
- "@typespec/tspd": "^0.46.0"
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": "npm run gen-extern-signature && tsc -p tsconfig.build.json",
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",
@@ -1,113 +1,97 @@
1
- import { HttpMethod, ServiceRequestFile } from "@typespec/spec-api";
2
- import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
3
- import FormData from "form-data";
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
- endPoint: string;
7
- options?: {
8
- requestBody?: any;
9
- files?: ServiceRequestFile[];
10
- config?: AxiosRequestConfig<any> | undefined;
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 checkAndAddFormDataIfRequired(request: ServiceRequest) {
15
- if (request.options?.config?.headers?.["Content-Type"] === "multipart/form-data") {
16
- const formData = new FormData();
17
- if (request.options?.requestBody) {
18
- for (const key in request.options.requestBody) {
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 (serviceCallType === "head") {
66
- return await makeHeadCall(request);
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
- export async function makePutCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
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
- export async function makePostCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
81
- const response = await axios.post(
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
- export async function makeGetCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
90
- const response = await axios.get(request.endPoint, request.options?.config);
91
- return response;
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
- export async function makePatchCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
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
- export async function makeDeleteCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
104
- const response = await axios.delete(request.endPoint, request.options?.config);
105
- return response;
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 makeHeadCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
109
- const response = await axios.head(request.endPoint, request.options?.config);
110
- return response;
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";
@@ -1,5 +1,4 @@
1
1
  import { spawn } from "child_process";
2
- import fetch from "node-fetch";
3
2
  import { resolve } from "path";
4
3
  import { MockApiApp } from "../app/app.js";
5
4
  import { AdminUrls } from "../constants.js";
@@ -1,157 +1,114 @@
1
- import { MockApiDefinition } from "@typespec/spec-api";
2
- import * as fs from "fs";
3
- import * as path from "path";
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, uint8ArrayToString } from "./helper.js";
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
- scenario_name: string;
13
- status: "success" | "failure";
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
- private getConfigObj() {
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
- logger.info(`Executing ${this.name} endpoint - Method: ${this.mockApiDefinition.method}`);
64
-
65
- const response = await makeServiceCall(this.mockApiDefinition.method, {
66
- endPoint: `${this.serverBasePath}${this.mockApiDefinition.uri}`,
67
- options: {
68
- requestBody: this.mockApiDefinition.request.body,
69
- files: this.mockApiDefinition.request.files,
70
- config: this.getConfigObj(),
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
- logger.error(`Status code mismatch for ${this.name} endpoint`);
76
- logger.error(
77
- `Expected: ${this.mockApiDefinition.response.status} - Actual: ${response.status}`,
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
- if (this.mockApiDefinition.response.body.contentType === "application/xml") {
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
- for (const key in this.mockApiDefinition.response.headers) {
136
- if (
137
- this.mockApiDefinition.response.headers[key] !==
138
- response.headers[key].replace(this.serverBasePath, "")
139
- ) {
140
- logger.error(`Response headers mismatch for ${this.name} endpoint`);
141
- logger.error(
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
- runSingleScenario?: string;
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.info(`Executing server tests with base URL: ${baseUrl}`);
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 success_diagnostics: ServerTestDiagnostics[] = [];
195
- const failure_diagnostics: ServerTestDiagnostics[] = [];
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 Object.entries(scenarios)) {
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
- if (testCasesToRun.length === 0 || testCasesToRun.includes(name)) {
203
- const obj: ServerTestsGenerator = new ServerTestsGenerator(name, api, baseUrl);
204
- try {
205
- await obj.executeScenario();
206
- success_diagnostics.push({
207
- scenario_name: name,
208
- status: "success",
209
- message: "executed successfully",
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
- } catch (e: any) {
212
- failure_diagnostics.push({
213
- scenario_name: name,
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
- logger.info("Server Tests Diagnostics Summary");
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 (success_diagnostics.length > 0) logger.info("Success Scenarios");
226
- success_diagnostics.forEach((diagnostic) => {
227
- logger.info(`${pc.green("✓")} Scenario: ${diagnostic.scenario_name} - ${diagnostic.message}`);
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 (failure_diagnostics.length > 0) logger.error("Failure Scenarios");
231
- if (failure_diagnostics.length > 0) {
232
- logger.error("Failed Scenario details");
233
- failure_diagnostics.forEach((diagnostic) => {
234
- logger.error(`${pc.red("✘")} Scenario: ${diagnostic.scenario_name}`);
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
  }