@typespec/spector 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 +1 -0
- package/LICENSE +21 -0
- package/cmd/cli.mjs +3 -0
- package/dist/generated-defs/TypeSpec.Spector.d.ts +22 -0
- package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -0
- package/dist/generated-defs/TypeSpec.Spector.js +2 -0
- package/dist/generated-defs/TypeSpec.Spector.js.map +1 -0
- package/dist/generated-defs/TypeSpec.Spector.ts-test.d.ts +2 -0
- package/dist/generated-defs/TypeSpec.Spector.ts-test.d.ts.map +1 -0
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js +5 -0
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -0
- package/dist/src/actions/check-coverage.d.ts +11 -0
- package/dist/src/actions/check-coverage.d.ts.map +1 -0
- package/dist/src/actions/check-coverage.js +77 -0
- package/dist/src/actions/check-coverage.js.map +1 -0
- package/dist/src/actions/generate-scenario-summary.d.ts +9 -0
- package/dist/src/actions/generate-scenario-summary.d.ts.map +1 -0
- package/dist/src/actions/generate-scenario-summary.js +49 -0
- package/dist/src/actions/generate-scenario-summary.js.map +1 -0
- package/dist/src/actions/helper.d.ts +21 -0
- package/dist/src/actions/helper.d.ts.map +1 -0
- package/dist/src/actions/helper.js +81 -0
- package/dist/src/actions/helper.js.map +1 -0
- package/dist/src/actions/index.d.ts +2 -0
- package/dist/src/actions/index.d.ts.map +1 -0
- package/dist/src/actions/index.js +2 -0
- package/dist/src/actions/index.js.map +1 -0
- package/dist/src/actions/serve.d.ts +12 -0
- package/dist/src/actions/serve.d.ts.map +1 -0
- package/dist/src/actions/serve.js +73 -0
- package/dist/src/actions/serve.js.map +1 -0
- package/dist/src/actions/server-test.d.ts +7 -0
- package/dist/src/actions/server-test.d.ts.map +1 -0
- package/dist/src/actions/server-test.js +165 -0
- package/dist/src/actions/server-test.js.map +1 -0
- package/dist/src/actions/upload-coverage-report.d.ts +10 -0
- package/dist/src/actions/upload-coverage-report.d.ts.map +1 -0
- package/dist/src/actions/upload-coverage-report.js +19 -0
- package/dist/src/actions/upload-coverage-report.js.map +1 -0
- package/dist/src/actions/upload-scenario-manifest.d.ts +6 -0
- package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -0
- package/dist/src/actions/upload-scenario-manifest.js +18 -0
- package/dist/src/actions/upload-scenario-manifest.js.map +1 -0
- package/dist/src/actions/validate-mock-apis.d.ts +7 -0
- package/dist/src/actions/validate-mock-apis.d.ts.map +1 -0
- package/dist/src/actions/validate-mock-apis.js +71 -0
- package/dist/src/actions/validate-mock-apis.js.map +1 -0
- package/dist/src/actions/validate-scenarios.d.ts +7 -0
- package/dist/src/actions/validate-scenarios.d.ts.map +1 -0
- package/dist/src/actions/validate-scenarios.js +25 -0
- package/dist/src/actions/validate-scenarios.js.map +1 -0
- package/dist/src/app/app.d.ts +17 -0
- package/dist/src/app/app.d.ts.map +1 -0
- package/dist/src/app/app.js +134 -0
- package/dist/src/app/app.js.map +1 -0
- package/dist/src/app/config.d.ts +15 -0
- package/dist/src/app/config.d.ts.map +1 -0
- package/dist/src/app/config.js +2 -0
- package/dist/src/app/config.js.map +1 -0
- package/dist/src/app/index.d.ts +3 -0
- package/dist/src/app/index.d.ts.map +1 -0
- package/dist/src/app/index.js +3 -0
- package/dist/src/app/index.js.map +1 -0
- package/dist/src/app/request-processor.d.ts +5 -0
- package/dist/src/app/request-processor.d.ts.map +1 -0
- package/dist/src/app/request-processor.js +44 -0
- package/dist/src/app/request-processor.js.map +1 -0
- package/dist/src/cli/cli.d.ts +3 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +316 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/config/config-schema.d.ts +4 -0
- package/dist/src/config/config-schema.d.ts.map +1 -0
- package/dist/src/config/config-schema.js +14 -0
- package/dist/src/config/config-schema.js.map +1 -0
- package/dist/src/config/config.d.ts +4 -0
- package/dist/src/config/config.d.ts.map +1 -0
- package/dist/src/config/config.js +12 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/schema-validator.d.ts +18 -0
- package/dist/src/config/schema-validator.d.ts.map +1 -0
- package/dist/src/config/schema-validator.js +43 -0
- package/dist/src/config/schema-validator.js.map +1 -0
- package/dist/src/config/types.d.ts +4 -0
- package/dist/src/config/types.d.ts.map +1 -0
- package/dist/src/config/types.js +2 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/constants.d.ts +4 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +4 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/coverage/common.d.ts +3 -0
- package/dist/src/coverage/common.d.ts.map +1 -0
- package/dist/src/coverage/common.js +9 -0
- package/dist/src/coverage/common.js.map +1 -0
- package/dist/src/coverage/coverage-report.d.ts +3 -0
- package/dist/src/coverage/coverage-report.d.ts.map +1 -0
- package/dist/src/coverage/coverage-report.js +9 -0
- package/dist/src/coverage/coverage-report.js.map +1 -0
- package/dist/src/coverage/coverage-tracker.d.ts +17 -0
- package/dist/src/coverage/coverage-tracker.d.ts.map +1 -0
- package/dist/src/coverage/coverage-tracker.js +125 -0
- package/dist/src/coverage/coverage-tracker.js.map +1 -0
- package/dist/src/coverage/index.d.ts +2 -0
- package/dist/src/coverage/index.d.ts.map +1 -0
- package/dist/src/coverage/index.js +2 -0
- package/dist/src/coverage/index.js.map +1 -0
- package/dist/src/coverage/scenario-manifest.d.ts +6 -0
- package/dist/src/coverage/scenario-manifest.d.ts.map +1 -0
- package/dist/src/coverage/scenario-manifest.js +32 -0
- package/dist/src/coverage/scenario-manifest.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/decorators.d.ts +24 -0
- package/dist/src/lib/decorators.d.ts.map +1 -0
- package/dist/src/lib/decorators.js +158 -0
- package/dist/src/lib/decorators.js.map +1 -0
- package/dist/src/lib/index.d.ts +4 -0
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js +6 -0
- package/dist/src/lib/index.js.map +1 -0
- package/dist/src/lib/lib.d.ts +33 -0
- package/dist/src/lib/lib.d.ts.map +1 -0
- package/dist/src/lib/lib.js +31 -0
- package/dist/src/lib/lib.js.map +1 -0
- package/dist/src/lib/tsp-index.d.ts +2 -0
- package/dist/src/lib/tsp-index.d.ts.map +1 -0
- package/dist/src/lib/tsp-index.js +11 -0
- package/dist/src/lib/tsp-index.js.map +1 -0
- package/dist/src/lib/validate.d.ts +3 -0
- package/dist/src/lib/validate.d.ts.map +1 -0
- package/dist/src/lib/validate.js +41 -0
- package/dist/src/lib/validate.js.map +1 -0
- package/dist/src/logger.d.ts +3 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +10 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/routes/admin.d.ts +3 -0
- package/dist/src/routes/admin.d.ts.map +1 -0
- package/dist/src/routes/admin.js +13 -0
- package/dist/src/routes/admin.js.map +1 -0
- package/dist/src/routes/index.d.ts +3 -0
- package/dist/src/routes/index.d.ts.map +1 -0
- package/dist/src/routes/index.js +6 -0
- package/dist/src/routes/index.js.map +1 -0
- package/dist/src/scenarios-resolver.d.ts +17 -0
- package/dist/src/scenarios-resolver.d.ts.map +1 -0
- package/dist/src/scenarios-resolver.js +163 -0
- package/dist/src/scenarios-resolver.js.map +1 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +2 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/server.d.ts +14 -0
- package/dist/src/server/server.d.ts.map +1 -0
- package/dist/src/server/server.js +68 -0
- package/dist/src/server/server.js.map +1 -0
- package/dist/src/spec-utils/import-spec.d.ts +6 -0
- package/dist/src/spec-utils/import-spec.d.ts.map +1 -0
- package/dist/src/spec-utils/import-spec.js +39 -0
- package/dist/src/spec-utils/import-spec.js.map +1 -0
- package/dist/src/spec-utils/index.d.ts +2 -0
- package/dist/src/spec-utils/index.d.ts.map +1 -0
- package/dist/src/spec-utils/index.js +2 -0
- package/dist/src/spec-utils/index.js.map +1 -0
- package/dist/src/utils/body-utils.d.ts +8 -0
- package/dist/src/utils/body-utils.d.ts.map +1 -0
- package/dist/src/utils/body-utils.js +8 -0
- package/dist/src/utils/body-utils.js.map +1 -0
- package/dist/src/utils/diagnostic-reporter.d.ts +13 -0
- package/dist/src/utils/diagnostic-reporter.d.ts.map +1 -0
- package/dist/src/utils/diagnostic-reporter.js +35 -0
- package/dist/src/utils/diagnostic-reporter.js.map +1 -0
- package/dist/src/utils/exec.d.ts +9 -0
- package/dist/src/utils/exec.d.ts.map +1 -0
- package/dist/src/utils/exec.js +30 -0
- package/dist/src/utils/exec.js.map +1 -0
- package/dist/src/utils/file-utils.d.ts +7 -0
- package/dist/src/utils/file-utils.d.ts.map +1 -0
- package/dist/src/utils/file-utils.js +13 -0
- package/dist/src/utils/file-utils.js.map +1 -0
- package/dist/src/utils/index.d.ts +6 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +6 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/misc-utils.d.ts +8 -0
- package/dist/src/utils/misc-utils.d.ts.map +1 -0
- package/dist/src/utils/misc-utils.js +47 -0
- package/dist/src/utils/misc-utils.js.map +1 -0
- package/dist/src/utils/path-utils.d.ts +2 -0
- package/dist/src/utils/path-utils.d.ts.map +1 -0
- package/dist/src/utils/path-utils.js +4 -0
- package/dist/src/utils/path-utils.js.map +1 -0
- package/dist/src/utils/request-utils.d.ts +3 -0
- package/dist/src/utils/request-utils.d.ts.map +1 -0
- package/dist/src/utils/request-utils.js +2 -0
- package/dist/src/utils/request-utils.js.map +1 -0
- package/generated-defs/TypeSpec.Spector.ts +46 -0
- package/generated-defs/TypeSpec.Spector.ts-test.ts +5 -0
- package/lib/main.tsp +37 -0
- package/package.json +79 -0
- package/src/actions/check-coverage.ts +98 -0
- package/src/actions/generate-scenario-summary.ts +63 -0
- package/src/actions/helper.ts +116 -0
- package/src/actions/index.ts +1 -0
- package/src/actions/serve.ts +88 -0
- package/src/actions/server-test.ts +198 -0
- package/src/actions/upload-coverage-report.ts +41 -0
- package/src/actions/upload-scenario-manifest.ts +30 -0
- package/src/actions/validate-mock-apis.ts +91 -0
- package/src/actions/validate-scenarios.ts +32 -0
- package/src/app/app.ts +166 -0
- package/src/app/config.ts +16 -0
- package/src/app/index.ts +2 -0
- package/src/app/request-processor.ts +71 -0
- package/src/cli/cli.ts +368 -0
- package/src/config/config-schema.ts +16 -0
- package/src/config/config.ts +15 -0
- package/src/config/schema-validator.ts +57 -0
- package/src/config/types.ts +3 -0
- package/src/constants.ts +3 -0
- package/src/coverage/common.ts +10 -0
- package/src/coverage/coverage-report.ts +13 -0
- package/src/coverage/coverage-tracker.ts +141 -0
- package/src/coverage/index.ts +1 -0
- package/src/coverage/scenario-manifest.ts +43 -0
- package/src/index.ts +1 -0
- package/src/lib/decorators.ts +225 -0
- package/src/lib/index.ts +18 -0
- package/src/lib/lib.ts +33 -0
- package/src/lib/tsp-index.ts +12 -0
- package/src/lib/validate.ts +55 -0
- package/src/logger.ts +10 -0
- package/src/routes/admin.ts +15 -0
- package/src/routes/index.ts +7 -0
- package/src/scenarios-resolver.ts +209 -0
- package/src/server/index.ts +1 -0
- package/src/server/server.ts +99 -0
- package/src/spec-utils/import-spec.ts +49 -0
- package/src/spec-utils/index.ts +1 -0
- package/src/utils/body-utils.test.ts +18 -0
- package/src/utils/body-utils.ts +8 -0
- package/src/utils/diagnostic-reporter.ts +49 -0
- package/src/utils/exec.ts +36 -0
- package/src/utils/file-utils.ts +14 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/misc-utils.ts +54 -0
- package/src/utils/path-utils.ts +3 -0
- package/src/utils/request-utils.ts +4 -0
- package/temp/.tsbuildinfo +1 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { MockApiDefinition } from "@typespec/spec-api";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
import { loadScenarioMockApis } from "../scenarios-resolver.js";
|
|
6
|
+
import { makeServiceCall, uint8ArrayToString } from "./helper.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
9
|
+
|
|
10
|
+
class ServerTestsGenerator {
|
|
11
|
+
private name: string = "";
|
|
12
|
+
private mockApiDefinition: MockApiDefinition;
|
|
13
|
+
private serverBasePath: string = "";
|
|
14
|
+
|
|
15
|
+
constructor(name: string, mockApiDefinition: MockApiDefinition, serverBasePath: string) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.mockApiDefinition = mockApiDefinition;
|
|
18
|
+
this.serverBasePath = serverBasePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getConfigObj() {
|
|
22
|
+
let config = {};
|
|
23
|
+
if (this.mockApiDefinition.request.status) {
|
|
24
|
+
const validStatusCode = this.mockApiDefinition.request.status;
|
|
25
|
+
config = {
|
|
26
|
+
validateStatus: function (status: number) {
|
|
27
|
+
return (status >= 200 && status < 300) || validStatusCode === status;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (this.mockApiDefinition.request.params) {
|
|
32
|
+
config = {
|
|
33
|
+
...config,
|
|
34
|
+
params: this.mockApiDefinition.request.params,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (this.mockApiDefinition.request.headers) {
|
|
38
|
+
config = {
|
|
39
|
+
...config,
|
|
40
|
+
headers: this.mockApiDefinition.request.headers,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
["head", "get", "delete"].includes(this.mockApiDefinition.method) &&
|
|
45
|
+
this.mockApiDefinition.request.body
|
|
46
|
+
) {
|
|
47
|
+
config = {
|
|
48
|
+
...config,
|
|
49
|
+
data: this.mockApiDefinition.request.body,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async executeScenario() {
|
|
56
|
+
logger.info(`Executing ${this.name} endpoint - Method: ${this.mockApiDefinition.method}`);
|
|
57
|
+
|
|
58
|
+
const response = await makeServiceCall(this.mockApiDefinition.method, {
|
|
59
|
+
endPoint: `${this.serverBasePath}${this.mockApiDefinition.uri}`,
|
|
60
|
+
options: {
|
|
61
|
+
requestBody: this.mockApiDefinition.request.body,
|
|
62
|
+
files: this.mockApiDefinition.request.files,
|
|
63
|
+
config: this.getConfigObj(),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (this.mockApiDefinition.response.status !== response.status) {
|
|
68
|
+
logger.error(`Status code mismatch for ${this.name} endpoint`);
|
|
69
|
+
logger.error(
|
|
70
|
+
`Expected: ${this.mockApiDefinition.response.status} - Actual: ${response.status}`,
|
|
71
|
+
);
|
|
72
|
+
throw new Error(`Status code mismatch for ${this.name} endpoint`);
|
|
73
|
+
}
|
|
74
|
+
if (this.mockApiDefinition.response.body) {
|
|
75
|
+
if (this.mockApiDefinition.response.body.contentType === "application/xml") {
|
|
76
|
+
if (
|
|
77
|
+
JSON.stringify(this.mockApiDefinition.response.body.rawContent) !==
|
|
78
|
+
JSON.stringify(response.data)
|
|
79
|
+
) {
|
|
80
|
+
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
81
|
+
logger.error(
|
|
82
|
+
`Expected: ${this.mockApiDefinition.response.body["rawContent"]} - Actual: ${response.data}`,
|
|
83
|
+
);
|
|
84
|
+
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
85
|
+
}
|
|
86
|
+
} else if (Buffer.isBuffer(this.mockApiDefinition.response.body.rawContent)) {
|
|
87
|
+
if (
|
|
88
|
+
this.mockApiDefinition.request.headers &&
|
|
89
|
+
this.mockApiDefinition.request.headers["accept"] === "application/json"
|
|
90
|
+
) {
|
|
91
|
+
if (
|
|
92
|
+
response.data.content !==
|
|
93
|
+
this.mockApiDefinition.response.body.rawContent.toString("base64")
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
if (
|
|
99
|
+
uint8ArrayToString(response.data, "utf-8") !==
|
|
100
|
+
this.mockApiDefinition.response.body.rawContent.toString()
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (this.mockApiDefinition.response.body.contentType === "text/plain") {
|
|
106
|
+
if (this.mockApiDefinition.response.body.rawContent !== response.data) {
|
|
107
|
+
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
108
|
+
logger.error(
|
|
109
|
+
`Expected: ${this.mockApiDefinition.response.body} - Actual: ${response.data}`,
|
|
110
|
+
);
|
|
111
|
+
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const responseData = JSON.stringify(response.data);
|
|
115
|
+
if (
|
|
116
|
+
this.mockApiDefinition.response.body.rawContent !==
|
|
117
|
+
responseData.replace(this.serverBasePath, "")
|
|
118
|
+
) {
|
|
119
|
+
logger.error(`Response data mismatch for ${this.name} endpoint`);
|
|
120
|
+
logger.error(
|
|
121
|
+
`Expected: ${this.mockApiDefinition.response.body} - Actual: ${response.data}`,
|
|
122
|
+
);
|
|
123
|
+
throw new Error(`Response data mismatch for ${this.name} endpoint`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (this.mockApiDefinition.response.headers) {
|
|
128
|
+
for (const key in this.mockApiDefinition.response.headers) {
|
|
129
|
+
if (
|
|
130
|
+
this.mockApiDefinition.response.headers[key] !==
|
|
131
|
+
response.headers[key].replace(this.serverBasePath, "")
|
|
132
|
+
) {
|
|
133
|
+
logger.error(`Response headers mismatch for ${this.name} endpoint`);
|
|
134
|
+
logger.error(
|
|
135
|
+
`Expected: ${this.mockApiDefinition.response.headers[key]} - Actual: ${response.headers[key]}`,
|
|
136
|
+
);
|
|
137
|
+
throw new Error(`Response headers mismatch for ${this.name} endpoint`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ServerTestOptions {
|
|
145
|
+
baseUrl?: string;
|
|
146
|
+
runSingleScenario?: string;
|
|
147
|
+
runScenariosFromFile?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function delay(ms: number) {
|
|
151
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function waitForServer(baseUrl: string) {
|
|
155
|
+
logger.info(`Executing server tests with base URL: ${baseUrl}`);
|
|
156
|
+
let retry = 0;
|
|
157
|
+
|
|
158
|
+
while (retry < 3) {
|
|
159
|
+
try {
|
|
160
|
+
await fetch(baseUrl);
|
|
161
|
+
break;
|
|
162
|
+
} catch (e) {
|
|
163
|
+
retry++;
|
|
164
|
+
logger.info("Retrying...");
|
|
165
|
+
await delay(retry * 1000);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
logger.info(` ${baseUrl} is ready!`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function serverTest(scenariosPath: string, options: ServerTestOptions = {}) {
|
|
172
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
173
|
+
await waitForServer(baseUrl);
|
|
174
|
+
// 1. Get Testcases to run
|
|
175
|
+
const testCasesToRun: string[] = [];
|
|
176
|
+
if (options.runSingleScenario) {
|
|
177
|
+
testCasesToRun.push(options.runSingleScenario);
|
|
178
|
+
} else if (options.runScenariosFromFile) {
|
|
179
|
+
const data = fs.readFileSync(path.resolve(options.runScenariosFromFile), "utf8");
|
|
180
|
+
const lines = data.split("\n");
|
|
181
|
+
lines.forEach((line) => {
|
|
182
|
+
testCasesToRun.push(line.trim());
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// 2. Load all the scenarios
|
|
186
|
+
const scenarios = await loadScenarioMockApis(scenariosPath);
|
|
187
|
+
// 3. Execute each scenario
|
|
188
|
+
for (const [name, scenario] of Object.entries(scenarios)) {
|
|
189
|
+
if (!Array.isArray(scenario.apis)) continue;
|
|
190
|
+
for (const api of scenario.apis) {
|
|
191
|
+
if (api.kind !== "MockApiDefinition") continue;
|
|
192
|
+
if (testCasesToRun.length === 0 || testCasesToRun.includes(name)) {
|
|
193
|
+
const obj: ServerTestsGenerator = new ServerTestsGenerator(name, api, baseUrl);
|
|
194
|
+
await obj.executeScenario();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AzureCliCredential } from "@azure/identity";
|
|
2
|
+
import { CoverageReport, GeneratorMetadata, SpecCoverageClient } from "@typespec/spec-coverage-sdk";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
|
|
7
|
+
export interface UploadCoverageReportConfig {
|
|
8
|
+
coverageFile: string;
|
|
9
|
+
storageAccountName: string;
|
|
10
|
+
generatorName: string;
|
|
11
|
+
generatorVersion: string;
|
|
12
|
+
generatorCommit?: string;
|
|
13
|
+
generatorMode: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function uploadCoverageReport({
|
|
17
|
+
coverageFile,
|
|
18
|
+
storageAccountName,
|
|
19
|
+
generatorName,
|
|
20
|
+
generatorVersion,
|
|
21
|
+
generatorCommit: geenratorCommit,
|
|
22
|
+
generatorMode,
|
|
23
|
+
}: UploadCoverageReportConfig) {
|
|
24
|
+
const content = await readFile(coverageFile);
|
|
25
|
+
const coverage: CoverageReport = JSON.parse(content.toString());
|
|
26
|
+
|
|
27
|
+
const client = new SpecCoverageClient(storageAccountName, new AzureCliCredential());
|
|
28
|
+
const generatorMetadata: GeneratorMetadata = {
|
|
29
|
+
name: generatorName,
|
|
30
|
+
version: generatorVersion,
|
|
31
|
+
mode: generatorMode,
|
|
32
|
+
commit: geenratorCommit,
|
|
33
|
+
};
|
|
34
|
+
await client.coverage.upload(generatorMetadata, coverage);
|
|
35
|
+
|
|
36
|
+
logger.info(
|
|
37
|
+
`${pc.green(
|
|
38
|
+
"✓",
|
|
39
|
+
)} Scenario coverage file "${coverageFile}" uploaded to ${storageAccountName} storage account for ${generatorName}@${generatorVersion}.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AzureCliCredential } from "@azure/identity";
|
|
2
|
+
import { SpecCoverageClient } from "@typespec/spec-coverage-sdk";
|
|
3
|
+
import { writeFile } from "fs/promises";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { computeScenarioManifest } from "../coverage/scenario-manifest.js";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
7
|
+
|
|
8
|
+
export interface UploadScenarioManifestConfig {
|
|
9
|
+
scenariosPath: string;
|
|
10
|
+
storageAccountName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function uploadScenarioManifest({
|
|
14
|
+
scenariosPath,
|
|
15
|
+
storageAccountName,
|
|
16
|
+
}: UploadScenarioManifestConfig) {
|
|
17
|
+
const [manifest, diagnostics] = await computeScenarioManifest(scenariosPath);
|
|
18
|
+
if (manifest === undefined || diagnostics.length > 0) {
|
|
19
|
+
process.exit(-1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await writeFile("manifest.json", JSON.stringify(manifest, null, 2));
|
|
23
|
+
const client = new SpecCoverageClient(storageAccountName, new AzureCliCredential());
|
|
24
|
+
await client.createIfNotExists();
|
|
25
|
+
await client.manifest.upload(manifest);
|
|
26
|
+
|
|
27
|
+
logger.info(
|
|
28
|
+
`${pc.green("✓")} Scenario manifest uploaded to ${storageAccountName} storage account.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { findScenarioSpecFiles, loadScenarioMockApiFiles } from "../scenarios-resolver.js";
|
|
4
|
+
import { importSpecExpect, importTypeSpec } from "../spec-utils/import-spec.js";
|
|
5
|
+
import { createDiagnosticReporter } from "../utils/diagnostic-reporter.js";
|
|
6
|
+
|
|
7
|
+
export interface ValidateMockApisConfig {
|
|
8
|
+
scenariosPath: string;
|
|
9
|
+
exitDueToPreviousError?: boolean;
|
|
10
|
+
hasMoreScenarios?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function validateMockApis({
|
|
14
|
+
scenariosPath,
|
|
15
|
+
exitDueToPreviousError,
|
|
16
|
+
hasMoreScenarios,
|
|
17
|
+
}: ValidateMockApisConfig) {
|
|
18
|
+
const mockApis = await loadScenarioMockApiFiles(scenariosPath);
|
|
19
|
+
const scenarioFiles = await findScenarioSpecFiles(scenariosPath);
|
|
20
|
+
|
|
21
|
+
const specCompiler = await importTypeSpec(scenariosPath);
|
|
22
|
+
const specExpect = await importSpecExpect(scenariosPath);
|
|
23
|
+
const diagnostics = createDiagnosticReporter();
|
|
24
|
+
for (const { name, specFilePath } of scenarioFiles) {
|
|
25
|
+
logger.debug(`Found scenario "${specFilePath}"`);
|
|
26
|
+
const program = await specCompiler.compile(specCompiler.NodeHost, specFilePath, {
|
|
27
|
+
noEmit: true,
|
|
28
|
+
warningAsError: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Workaround https://github.com/Azure/cadl-azure/issues/2458
|
|
32
|
+
const programDiagnostics = program.diagnostics.filter(
|
|
33
|
+
(d) =>
|
|
34
|
+
!(
|
|
35
|
+
d.code === "@azure-tools/typespec-azure-core/casing-style" &&
|
|
36
|
+
typeof d.target === "object" &&
|
|
37
|
+
"kind" in d.target &&
|
|
38
|
+
d.target.kind === "Namespace" &&
|
|
39
|
+
d.target.name === "DPG"
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (programDiagnostics.length > 0) {
|
|
44
|
+
specCompiler.logDiagnostics(programDiagnostics, { log: logger.error });
|
|
45
|
+
diagnostics.reportDiagnostic({
|
|
46
|
+
message: `Scenario ${name} is invalid.`,
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mockApiFile = mockApis.find((x) => x.path.endsWith(`/${name}/mockapi.js`));
|
|
52
|
+
if (mockApiFile === undefined) {
|
|
53
|
+
diagnostics.reportDiagnostic({
|
|
54
|
+
message: `Scenario ${name} is missing a mockapi file. Make sure to have a mockapi.ts that is built.`,
|
|
55
|
+
});
|
|
56
|
+
logger.debug(`Expected mock api file at "${name}/mockapi.js"`);
|
|
57
|
+
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const scenarios = specExpect.listScenarios(program);
|
|
62
|
+
|
|
63
|
+
let foundFailure = false;
|
|
64
|
+
for (const scenario of scenarios) {
|
|
65
|
+
if (mockApiFile.scenarios[scenario.name] === undefined) {
|
|
66
|
+
foundFailure = true;
|
|
67
|
+
diagnostics.reportDiagnostic({
|
|
68
|
+
message: `Scenario ${scenario.name} is missing implementation in for ${name} scenario file.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!foundFailure) {
|
|
74
|
+
logger.info(`${pc.green("✓")} Scenario ${name} has all implemented mock apis.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (diagnostics.diagnostics.length === 0) {
|
|
79
|
+
if (exitDueToPreviousError && !hasMoreScenarios) {
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
if (exitDueToPreviousError) return exitDueToPreviousError;
|
|
83
|
+
else return false;
|
|
84
|
+
} else {
|
|
85
|
+
if (hasMoreScenarios) {
|
|
86
|
+
return true;
|
|
87
|
+
} else {
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { loadScenarios } from "../scenarios-resolver.js";
|
|
4
|
+
|
|
5
|
+
export interface ValidateScenarioConfig {
|
|
6
|
+
scenariosPath: string;
|
|
7
|
+
exitDueToPreviousError?: boolean;
|
|
8
|
+
hasMoreScenarios?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function validateScenarios({
|
|
12
|
+
scenariosPath,
|
|
13
|
+
exitDueToPreviousError,
|
|
14
|
+
hasMoreScenarios,
|
|
15
|
+
}: ValidateScenarioConfig) {
|
|
16
|
+
const [_, diagnostics] = await loadScenarios(scenariosPath);
|
|
17
|
+
|
|
18
|
+
if (diagnostics.length === 0) {
|
|
19
|
+
logger.info(`${pc.green("✓")} All scenarios are valid.`);
|
|
20
|
+
if (exitDueToPreviousError && !hasMoreScenarios) {
|
|
21
|
+
process.exit(-1);
|
|
22
|
+
}
|
|
23
|
+
if (exitDueToPreviousError) return exitDueToPreviousError;
|
|
24
|
+
else return false;
|
|
25
|
+
} else {
|
|
26
|
+
if (hasMoreScenarios) {
|
|
27
|
+
return true;
|
|
28
|
+
} else {
|
|
29
|
+
process.exit(-1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/app/app.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { MockApiDefinition, MockRequest, RequestExt, ScenarioMockApi } from "@typespec/spec-api";
|
|
2
|
+
import { ScenariosMetadata } from "@typespec/spec-coverage-sdk";
|
|
3
|
+
import { Response, Router } from "express";
|
|
4
|
+
import { getScenarioMetadata } from "../coverage/common.js";
|
|
5
|
+
import { CoverageTracker } from "../coverage/coverage-tracker.js";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
7
|
+
import { internalRouter } from "../routes/index.js";
|
|
8
|
+
import { loadScenarioMockApis } from "../scenarios-resolver.js";
|
|
9
|
+
import { MockApiServer } from "../server/index.js";
|
|
10
|
+
import { ApiMockAppConfig } from "./config.js";
|
|
11
|
+
import { processRequest } from "./request-processor.js";
|
|
12
|
+
|
|
13
|
+
export interface ScenariosAndScenariosMetadata {
|
|
14
|
+
scenarios: Record<string, ScenarioMockApi>;
|
|
15
|
+
scenariosMetadata: ScenariosMetadata;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MockApiApp {
|
|
19
|
+
private router = Router();
|
|
20
|
+
private server: MockApiServer;
|
|
21
|
+
private coverageTracker: CoverageTracker;
|
|
22
|
+
|
|
23
|
+
constructor(private config: ApiMockAppConfig) {
|
|
24
|
+
this.server = new MockApiServer({ port: config.port });
|
|
25
|
+
this.coverageTracker = new CoverageTracker(config.coverageFile);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async start(): Promise<void> {
|
|
29
|
+
this.server.use("/", internalRouter);
|
|
30
|
+
|
|
31
|
+
const ScenariosAndScenariosMetadata: ScenariosAndScenariosMetadata[] = [];
|
|
32
|
+
if (Array.isArray(this.config.scenarioPath)) {
|
|
33
|
+
for (let idx = 0; idx < this.config.scenarioPath.length; idx++) {
|
|
34
|
+
const scenarios = await loadScenarioMockApis(this.config.scenarioPath[idx]);
|
|
35
|
+
const scenariosMetadata = await getScenarioMetadata(this.config.scenarioPath[idx]);
|
|
36
|
+
ScenariosAndScenariosMetadata.push({ scenarios, scenariosMetadata });
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const scenarios = await loadScenarioMockApis(this.config.scenarioPath);
|
|
40
|
+
const scenariosMetadata = await getScenarioMetadata(this.config.scenarioPath);
|
|
41
|
+
ScenariosAndScenariosMetadata.push({ scenarios, scenariosMetadata });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.coverageTracker.setScenarios(ScenariosAndScenariosMetadata);
|
|
45
|
+
|
|
46
|
+
for (const { scenarios } of ScenariosAndScenariosMetadata) {
|
|
47
|
+
for (const [name, scenario] of Object.entries(scenarios)) {
|
|
48
|
+
this.registerScenario(name, scenario);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.router.get("/.coverage", (req, res) => {
|
|
53
|
+
res.json(this.coverageTracker.computeCoverage());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.server.use("/", this.router);
|
|
57
|
+
this.server.start();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private registerScenario(name: string, scenario: ScenarioMockApi) {
|
|
61
|
+
for (const endpoint of scenario.apis) {
|
|
62
|
+
if (endpoint.kind !== "MockApiDefinition") {
|
|
63
|
+
this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
|
|
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
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isObject(value: any): boolean {
|
|
99
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createHandler(apiDefinition: MockApiDefinition) {
|
|
103
|
+
return (req: MockRequest) => {
|
|
104
|
+
// Validate body if present in the request
|
|
105
|
+
if (apiDefinition.request.body) {
|
|
106
|
+
if (
|
|
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
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate headers if present in the request
|
|
128
|
+
if (apiDefinition.request.headers) {
|
|
129
|
+
Object.entries(apiDefinition.request.headers).forEach(([key, value]) => {
|
|
130
|
+
if (key.toLowerCase() !== "content-type") {
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
req.expect.deepEqual(req.headers[key], value);
|
|
133
|
+
} else {
|
|
134
|
+
req.expect.containsHeader(key.toLowerCase(), String(value));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate query params if present in the request
|
|
141
|
+
if (apiDefinition.request.params) {
|
|
142
|
+
Object.entries(apiDefinition.request.params).forEach(([key, value]) => {
|
|
143
|
+
if (!req.query[key]) {
|
|
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
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
req.expect.deepEqual(req.query[key], value);
|
|
152
|
+
} else {
|
|
153
|
+
req.expect.containsQueryParam(key, String(value));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validations are done. Now return the response
|
|
160
|
+
return {
|
|
161
|
+
status: apiDefinition.response.status,
|
|
162
|
+
body: apiDefinition.response.body,
|
|
163
|
+
headers: apiDefinition.response.headers,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
}
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MockRequest,
|
|
3
|
+
MockRequestHandler,
|
|
4
|
+
MockResponse,
|
|
5
|
+
RequestExt,
|
|
6
|
+
ValidationError,
|
|
7
|
+
} from "@typespec/spec-api";
|
|
8
|
+
import { Response } from "express";
|
|
9
|
+
import { inspect } from "util";
|
|
10
|
+
import { CoverageTracker } from "../coverage/coverage-tracker.js";
|
|
11
|
+
import { logger } from "../logger.js";
|
|
12
|
+
|
|
13
|
+
export async function processRequest(
|
|
14
|
+
coverageTracker: CoverageTracker,
|
|
15
|
+
scenarioName: string,
|
|
16
|
+
scenarioUri: string,
|
|
17
|
+
request: RequestExt,
|
|
18
|
+
response: Response,
|
|
19
|
+
func: MockRequestHandler,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const mockRequest = new MockRequest(request);
|
|
22
|
+
const mockResponse = await callHandler(mockRequest, response, func);
|
|
23
|
+
if (mockResponse === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await coverageTracker.trackEndpointResponse(scenarioName, scenarioUri, mockResponse);
|
|
28
|
+
processResponse(response, mockResponse);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const processResponse = (response: Response, mockResponse: MockResponse) => {
|
|
32
|
+
response.status(mockResponse.status);
|
|
33
|
+
|
|
34
|
+
if (mockResponse.headers) {
|
|
35
|
+
response.set(mockResponse.headers);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (mockResponse.body) {
|
|
39
|
+
response.contentType(mockResponse.body.contentType).send(mockResponse.body.rawContent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
response.end();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const callHandler = async (
|
|
46
|
+
mockRequest: MockRequest,
|
|
47
|
+
response: Response,
|
|
48
|
+
func: MockRequestHandler,
|
|
49
|
+
): Promise<MockResponse | undefined> => {
|
|
50
|
+
try {
|
|
51
|
+
return func(mockRequest);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (!(e instanceof ValidationError)) {
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
logger.warn(
|
|
58
|
+
[
|
|
59
|
+
`Request validation failed: ${e.message}:`,
|
|
60
|
+
` Expected:\n${inspect(e.expected)}`,
|
|
61
|
+
` Actual: \n${inspect(e.actual)}`,
|
|
62
|
+
].join("\n"),
|
|
63
|
+
);
|
|
64
|
+
response
|
|
65
|
+
.status(400)
|
|
66
|
+
.contentType("application/json")
|
|
67
|
+
.send(e.toJSON ? e.toJSON() : JSON.stringify(e.message))
|
|
68
|
+
.end();
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
};
|