@typespec/spector 0.1.0-alpha.2 → 0.1.0-alpha.20-dev.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 +118 -0
- package/README.md +37 -0
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js +5 -2
- 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 +3 -4
- 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-coverage-report.d.ts +2 -1
- package/dist/src/actions/upload-coverage-report.d.ts.map +1 -1
- package/dist/src/actions/upload-coverage-report.js +6 -2
- package/dist/src/actions/upload-coverage-report.js.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.d.ts +3 -2
- package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.js +8 -5
- 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 +24 -15
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/coverage/common.d.ts.map +1 -1
- package/dist/src/coverage/common.js +1 -0
- package/dist/src/coverage/common.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/lib/decorators.d.ts.map +1 -1
- package/dist/src/lib/decorators.js +0 -2
- package/dist/src/lib/decorators.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 +18 -5
- package/dist/src/server/server.js.map +1 -1
- package/dist/src/utils/body-utils.d.ts.map +1 -1
- package/dist/src/utils/request-utils.d.ts.map +1 -1
- package/docs/decorators.md +24 -0
- package/docs/using-spector.md +99 -0
- package/docs/writing-mock-apis.md +116 -0
- package/docs/writing-scenario-spec.md +49 -0
- package/generated-defs/TypeSpec.Spector.ts-test.ts +7 -2
- package/lib/main.tsp +1 -1
- package/package.json +24 -28
- package/src/actions/helper.ts +79 -95
- package/src/actions/serve.ts +3 -4
- package/src/actions/server-test.ts +132 -156
- package/src/actions/upload-coverage-report.ts +7 -1
- package/src/actions/upload-scenario-manifest.ts +11 -7
- 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 +24 -15
- package/src/coverage/common.ts +1 -0
- package/src/coverage/coverage-tracker.ts +2 -2
- package/src/lib/decorators.ts +0 -2
- package/src/logger.ts +39 -8
- package/src/scenarios-resolver.ts +1 -1
- package/src/server/server.ts +21 -7
- package/temp/.tsbuildinfo +1 -1
package/src/actions/serve.ts
CHANGED
|
@@ -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";
|
|
@@ -39,14 +38,14 @@ export async function startInBackground(config: ServeConfig) {
|
|
|
39
38
|
const [nodeExe, entrypoint] = process.argv;
|
|
40
39
|
logger.info(`Starting server in background at port ${config.port}`);
|
|
41
40
|
const scenariosPath = Array.isArray(config.scenariosPath)
|
|
42
|
-
? config.scenariosPath
|
|
43
|
-
: config.scenariosPath;
|
|
41
|
+
? config.scenariosPath
|
|
42
|
+
: [config.scenariosPath];
|
|
44
43
|
const cp = spawn(
|
|
45
44
|
nodeExe,
|
|
46
45
|
[
|
|
47
46
|
entrypoint,
|
|
48
47
|
"serve",
|
|
49
|
-
scenariosPath,
|
|
48
|
+
...scenariosPath,
|
|
50
49
|
"--port",
|
|
51
50
|
config.port.toString(),
|
|
52
51
|
"--coverageFile",
|
|
@@ -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 (body.rawContent !== 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
|
}
|
|
@@ -11,6 +11,7 @@ export interface UploadCoverageReportConfig {
|
|
|
11
11
|
generatorVersion: string;
|
|
12
12
|
generatorCommit?: string;
|
|
13
13
|
generatorMode: string;
|
|
14
|
+
containerName: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function uploadCoverageReport({
|
|
@@ -20,11 +21,16 @@ export async function uploadCoverageReport({
|
|
|
20
21
|
generatorVersion,
|
|
21
22
|
generatorCommit: geenratorCommit,
|
|
22
23
|
generatorMode,
|
|
24
|
+
containerName,
|
|
23
25
|
}: UploadCoverageReportConfig) {
|
|
24
26
|
const content = await readFile(coverageFile);
|
|
25
27
|
const coverage: CoverageReport = JSON.parse(content.toString());
|
|
26
28
|
|
|
27
|
-
const client = new SpecCoverageClient(storageAccountName,
|
|
29
|
+
const client = new SpecCoverageClient(storageAccountName, {
|
|
30
|
+
credential: new AzureCliCredential(),
|
|
31
|
+
containerName,
|
|
32
|
+
});
|
|
33
|
+
await client.createIfNotExists();
|
|
28
34
|
const generatorMetadata: GeneratorMetadata = {
|
|
29
35
|
name: generatorName,
|
|
30
36
|
version: generatorVersion,
|
|
@@ -9,27 +9,31 @@ import { logger } from "../logger.js";
|
|
|
9
9
|
export interface UploadScenarioManifestConfig {
|
|
10
10
|
scenariosPaths: string[];
|
|
11
11
|
storageAccountName: string;
|
|
12
|
-
|
|
12
|
+
setNames: string[];
|
|
13
|
+
containerName: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export async function uploadScenarioManifest({
|
|
16
17
|
scenariosPaths,
|
|
17
18
|
storageAccountName,
|
|
18
|
-
|
|
19
|
+
setNames,
|
|
20
|
+
containerName,
|
|
19
21
|
}: UploadScenarioManifestConfig) {
|
|
20
22
|
const manifests = [];
|
|
21
|
-
for (
|
|
22
|
-
const path = resolve(process.cwd(),
|
|
23
|
+
for (let idx = 0; idx < scenariosPaths.length; idx++) {
|
|
24
|
+
const path = resolve(process.cwd(), scenariosPaths[idx]);
|
|
23
25
|
logger.info(`Computing scenario manifest for ${path}`);
|
|
24
|
-
const [manifest, diagnostics] = await computeScenarioManifest(path,
|
|
26
|
+
const [manifest, diagnostics] = await computeScenarioManifest(path, setNames[idx]);
|
|
25
27
|
if (manifest === undefined || diagnostics.length > 0) {
|
|
26
28
|
process.exit(-1);
|
|
27
29
|
}
|
|
28
30
|
manifests.push(manifest);
|
|
29
31
|
}
|
|
30
|
-
|
|
31
32
|
await writeFile("manifest.json", JSON.stringify(manifests, null, 2));
|
|
32
|
-
const client = new SpecCoverageClient(storageAccountName,
|
|
33
|
+
const client = new SpecCoverageClient(storageAccountName, {
|
|
34
|
+
credential: new AzureCliCredential(),
|
|
35
|
+
containerName,
|
|
36
|
+
});
|
|
33
37
|
await client.createIfNotExists();
|
|
34
38
|
await client.manifest.upload(manifests);
|
|
35
39
|
|
|
@@ -41,7 +41,7 @@ export async function validateMockApis({
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
if (programDiagnostics.length > 0) {
|
|
44
|
-
specCompiler.logDiagnostics(programDiagnostics,
|
|
44
|
+
specCompiler.logDiagnostics(programDiagnostics, specCompiler.NodeHost.logSink);
|
|
45
45
|
diagnostics.reportDiagnostic({
|
|
46
46
|
message: `Scenario ${name} is invalid.`,
|
|
47
47
|
});
|
package/src/app/app.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
expandDyns,
|
|
3
|
+
MockApiDefinition,
|
|
4
|
+
MockBody,
|
|
5
|
+
MockMultipartBody,
|
|
6
|
+
MockRequest,
|
|
7
|
+
RequestExt,
|
|
8
|
+
ResolverConfig,
|
|
9
|
+
ScenarioMockApi,
|
|
10
|
+
} from "@typespec/spec-api";
|
|
2
11
|
import { ScenariosMetadata } from "@typespec/spec-coverage-sdk";
|
|
3
12
|
import { Response, Router } from "express";
|
|
4
13
|
import { getScenarioMetadata } from "../coverage/common.js";
|
|
@@ -19,6 +28,7 @@ export class MockApiApp {
|
|
|
19
28
|
private router = Router();
|
|
20
29
|
private server: MockApiServer;
|
|
21
30
|
private coverageTracker: CoverageTracker;
|
|
31
|
+
private resolverConfig!: ResolverConfig;
|
|
22
32
|
|
|
23
33
|
constructor(private config: ApiMockAppConfig) {
|
|
24
34
|
this.server = new MockApiServer({ port: config.port });
|
|
@@ -54,79 +64,77 @@ export class MockApiApp {
|
|
|
54
64
|
});
|
|
55
65
|
|
|
56
66
|
this.server.use("/", this.router);
|
|
57
|
-
|
|
67
|
+
// Getting the resolved port as setting 0 in the config will have express resolve on of the available ports
|
|
68
|
+
const port = await this.server.start();
|
|
69
|
+
this.resolverConfig = {
|
|
70
|
+
baseUrl: `http://localhost:${port}`,
|
|
71
|
+
};
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
private registerScenario(name: string, scenario: ScenarioMockApi) {
|
|
61
75
|
for (const endpoint of scenario.apis) {
|
|
62
|
-
if (endpoint.
|
|
63
|
-
|
|
64
|
-
processRequest(
|
|
65
|
-
this.coverageTracker,
|
|
66
|
-
name,
|
|
67
|
-
endpoint.uri,
|
|
68
|
-
req,
|
|
69
|
-
res,
|
|
70
|
-
endpoint.handler,
|
|
71
|
-
).catch((e) => {
|
|
72
|
-
logger.error("Unexpected request error", e);
|
|
73
|
-
res.status(500).end();
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
} else {
|
|
77
|
-
if (!endpoint.handler) {
|
|
78
|
-
endpoint.handler = createHandler(endpoint);
|
|
79
|
-
}
|
|
80
|
-
this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
|
|
81
|
-
processRequest(
|
|
82
|
-
this.coverageTracker,
|
|
83
|
-
name,
|
|
84
|
-
endpoint.uri,
|
|
85
|
-
req,
|
|
86
|
-
res,
|
|
87
|
-
endpoint.handler!,
|
|
88
|
-
).catch((e) => {
|
|
89
|
-
logger.error("Unexpected request error", e);
|
|
90
|
-
res.status(500).end();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
76
|
+
if (!endpoint.handler) {
|
|
77
|
+
endpoint.handler = createHandler(endpoint, this.resolverConfig);
|
|
93
78
|
}
|
|
79
|
+
this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
|
|
80
|
+
processRequest(
|
|
81
|
+
this.coverageTracker,
|
|
82
|
+
name,
|
|
83
|
+
endpoint.uri,
|
|
84
|
+
req,
|
|
85
|
+
res,
|
|
86
|
+
endpoint.handler!,
|
|
87
|
+
this.resolverConfig,
|
|
88
|
+
).catch((e) => {
|
|
89
|
+
logger.error("Unexpected request error", e);
|
|
90
|
+
res.status(500).end();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
function
|
|
99
|
-
|
|
97
|
+
function validateBody(
|
|
98
|
+
req: MockRequest,
|
|
99
|
+
body: MockBody | MockMultipartBody,
|
|
100
|
+
config: ResolverConfig,
|
|
101
|
+
) {
|
|
102
|
+
if ("kind" in body) {
|
|
103
|
+
// custom handler for now.
|
|
104
|
+
} else {
|
|
105
|
+
if (Buffer.isBuffer(body.rawContent)) {
|
|
106
|
+
req.expect.rawBodyEquals(body.rawContent);
|
|
107
|
+
} else {
|
|
108
|
+
const raw =
|
|
109
|
+
typeof body.rawContent === "string" ? body.rawContent : body.rawContent?.serialize(config);
|
|
110
|
+
switch (body.contentType) {
|
|
111
|
+
case "application/json":
|
|
112
|
+
req.expect.coercedBodyEquals(JSON.parse(raw as any));
|
|
113
|
+
break;
|
|
114
|
+
case "application/xml":
|
|
115
|
+
req.expect.xmlBodyEquals(
|
|
116
|
+
(raw as any).replace(`<?xml version='1.0' encoding='UTF-8'?>`, ""),
|
|
117
|
+
);
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
req.expect.rawBodyEquals(raw);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
function createHandler(apiDefinition: MockApiDefinition) {
|
|
126
|
+
function createHandler(apiDefinition: MockApiDefinition, config: ResolverConfig) {
|
|
103
127
|
return (req: MockRequest) => {
|
|
128
|
+
const body = apiDefinition.request?.body;
|
|
104
129
|
// Validate body if present in the request
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
apiDefinition.request.headers &&
|
|
108
|
-
apiDefinition.request.headers["Content-Type"] === "application/xml"
|
|
109
|
-
) {
|
|
110
|
-
req.expect.xmlBodyEquals(
|
|
111
|
-
apiDefinition.request.body.rawContent.replace(
|
|
112
|
-
`<?xml version='1.0' encoding='UTF-8'?>`,
|
|
113
|
-
"",
|
|
114
|
-
),
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
if (isObject(apiDefinition.request.body)) {
|
|
118
|
-
Object.entries(apiDefinition.request.body).forEach(([key, value]) => {
|
|
119
|
-
req.expect.deepEqual(req.body[key], value);
|
|
120
|
-
});
|
|
121
|
-
} else {
|
|
122
|
-
req.expect.coercedBodyEquals(apiDefinition.request.body);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
130
|
+
if (body) {
|
|
131
|
+
validateBody(req, body, config);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
// Validate headers if present in the request
|
|
128
|
-
if (apiDefinition.request
|
|
129
|
-
|
|
135
|
+
if (apiDefinition.request?.headers) {
|
|
136
|
+
const headers = expandDyns(apiDefinition.request.headers, config);
|
|
137
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
130
138
|
if (key.toLowerCase() !== "content-type") {
|
|
131
139
|
if (Array.isArray(value)) {
|
|
132
140
|
req.expect.deepEqual(req.headers[key], value);
|
|
@@ -137,21 +145,12 @@ function createHandler(apiDefinition: MockApiDefinition) {
|
|
|
137
145
|
});
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (Array.isArray(value)) {
|
|
145
|
-
req.expect.deepEqual(req.params[key], value);
|
|
146
|
-
} else {
|
|
147
|
-
req.expect.deepEqual(req.params[key], String(value));
|
|
148
|
-
}
|
|
148
|
+
if (apiDefinition.request?.query) {
|
|
149
|
+
Object.entries(apiDefinition.request.query).forEach(([key, value]) => {
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
req.expect.deepEqual(req.query[key], value);
|
|
149
152
|
} else {
|
|
150
|
-
|
|
151
|
-
req.expect.deepEqual(req.query[key], value);
|
|
152
|
-
} else {
|
|
153
|
-
req.expect.containsQueryParam(key, String(value));
|
|
154
|
-
}
|
|
153
|
+
req.expect.containsQueryParam(key, String(value));
|
|
155
154
|
}
|
|
156
155
|
});
|
|
157
156
|
}
|