@typespec/spector 0.1.0-alpha.9 → 0.1.0-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 +127 -0
- package/README.md +37 -0
- package/dist/generated-defs/TypeSpec.Spector.d.ts +4 -4
- package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -1
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js +6 -3
- package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -1
- package/dist/src/actions/helper.d.ts +8 -15
- package/dist/src/actions/helper.d.ts.map +1 -1
- package/dist/src/actions/helper.js +61 -69
- package/dist/src/actions/helper.js.map +1 -1
- package/dist/src/actions/serve.d.ts.map +1 -1
- package/dist/src/actions/serve.js +0 -1
- package/dist/src/actions/serve.js.map +1 -1
- package/dist/src/actions/server-test.d.ts +3 -5
- package/dist/src/actions/server-test.d.ts.map +1 -1
- package/dist/src/actions/server-test.js +108 -132
- package/dist/src/actions/server-test.js.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.d.ts +4 -3
- package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
- package/dist/src/actions/upload-scenario-manifest.js +20 -13
- package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
- package/dist/src/actions/validate-mock-apis.js +1 -1
- package/dist/src/actions/validate-mock-apis.js.map +1 -1
- package/dist/src/app/app.d.ts +1 -0
- package/dist/src/app/app.d.ts.map +1 -1
- package/dist/src/app/app.js +48 -54
- package/dist/src/app/app.js.map +1 -1
- package/dist/src/app/request-processor.d.ts +2 -2
- package/dist/src/app/request-processor.d.ts.map +1 -1
- package/dist/src/app/request-processor.js +10 -6
- package/dist/src/app/request-processor.js.map +1 -1
- package/dist/src/cli/cli.js +20 -21
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/config/config.js +2 -2
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/coverage/coverage-tracker.d.ts.map +1 -1
- package/dist/src/coverage/coverage-tracker.js.map +1 -1
- package/dist/src/coverage/scenario-manifest.d.ts +3 -2
- package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
- package/dist/src/coverage/scenario-manifest.js +17 -5
- package/dist/src/coverage/scenario-manifest.js.map +1 -1
- package/dist/src/logger.d.ts +16 -2
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +27 -8
- package/dist/src/logger.js.map +1 -1
- package/dist/src/server/server.d.ts +1 -1
- package/dist/src/server/server.d.ts.map +1 -1
- package/dist/src/server/server.js +17 -4
- package/dist/src/server/server.js.map +1 -1
- package/dist/src/utils/misc-utils.d.ts +5 -3
- package/dist/src/utils/misc-utils.d.ts.map +1 -1
- package/dist/src/utils/misc-utils.js +1 -1
- package/dist/src/utils/misc-utils.js.map +1 -1
- package/generated-defs/TypeSpec.Spector.ts +4 -3
- package/generated-defs/TypeSpec.Spector.ts-test.ts +8 -3
- package/lib/main.tsp +1 -1
- package/package.json +25 -31
- package/src/actions/helper.ts +79 -95
- package/src/actions/serve.ts +0 -1
- package/src/actions/server-test.ts +132 -156
- package/src/actions/upload-scenario-manifest.ts +29 -18
- package/src/actions/validate-mock-apis.ts +1 -1
- package/src/app/app.ts +71 -72
- package/src/app/request-processor.ts +16 -4
- package/src/cli/cli.ts +21 -21
- package/src/config/config.ts +2 -2
- package/src/coverage/coverage-tracker.ts +2 -2
- package/src/coverage/scenario-manifest.ts +18 -8
- package/src/logger.ts +39 -8
- package/src/scenarios-resolver.ts +1 -1
- package/src/server/server.ts +20 -6
- package/src/utils/misc-utils.ts +6 -4
- package/temp/.tsbuildinfo +1 -1
- package/vitest.config.ts +11 -0
|
@@ -7,37 +7,48 @@ import { computeScenarioManifest } from "../coverage/scenario-manifest.js";
|
|
|
7
7
|
import { logger } from "../logger.js";
|
|
8
8
|
|
|
9
9
|
export interface UploadScenarioManifestConfig {
|
|
10
|
-
|
|
10
|
+
scenariosPath: string;
|
|
11
11
|
storageAccountName: string;
|
|
12
|
-
setNames: string[];
|
|
13
12
|
containerName: string;
|
|
13
|
+
manifestName: string;
|
|
14
|
+
override?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function uploadScenarioManifest({
|
|
17
|
-
|
|
18
|
+
scenariosPath,
|
|
18
19
|
storageAccountName,
|
|
19
|
-
setNames,
|
|
20
20
|
containerName,
|
|
21
|
+
manifestName,
|
|
22
|
+
override = false,
|
|
21
23
|
}: UploadScenarioManifestConfig) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (manifest === undefined || diagnostics.length > 0) {
|
|
28
|
-
process.exit(-1);
|
|
29
|
-
}
|
|
30
|
-
manifests.push(manifest);
|
|
24
|
+
const path = resolve(process.cwd(), scenariosPath);
|
|
25
|
+
logger.info(`Computing scenario manifest for ${path}`);
|
|
26
|
+
const [manifest, diagnostics] = await computeScenarioManifest(path);
|
|
27
|
+
if (manifest === undefined || diagnostics.length > 0) {
|
|
28
|
+
process.exit(-1);
|
|
31
29
|
}
|
|
32
|
-
await writeFile("manifest.json", JSON.stringify(
|
|
30
|
+
await writeFile("manifest.json", JSON.stringify(manifest, null, 2));
|
|
33
31
|
const client = new SpecCoverageClient(storageAccountName, {
|
|
34
32
|
credential: new AzureCliCredential(),
|
|
35
33
|
containerName,
|
|
36
34
|
});
|
|
37
35
|
await client.createIfNotExists();
|
|
38
|
-
|
|
36
|
+
if (override) {
|
|
37
|
+
await client.manifest.upload(manifestName, manifest);
|
|
38
|
+
logger.info(
|
|
39
|
+
`${pc.green("✓")} Scenario manifest uploaded to ${storageAccountName} storage account.`,
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
const result = await client.manifest.uploadIfVersionNew(manifestName, manifest);
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
if (result === "uploaded") {
|
|
45
|
+
logger.info(
|
|
46
|
+
`${pc.green("✓")} Scenario manifest new version uploaded to ${storageAccountName} storage account.`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
logger.info(
|
|
50
|
+
`${pc.white("-")} Existing scenario manifest in ${storageAccountName} storage account is up to date. No upload needed.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
43
54
|
}
|
|
@@ -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
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
expandDyns,
|
|
2
3
|
MockRequest,
|
|
3
4
|
MockRequestHandler,
|
|
4
5
|
MockResponse,
|
|
5
6
|
RequestExt,
|
|
7
|
+
ResolverConfig,
|
|
6
8
|
ValidationError,
|
|
7
9
|
} from "@typespec/spec-api";
|
|
8
10
|
import { Response } from "express";
|
|
@@ -17,6 +19,7 @@ export async function processRequest(
|
|
|
17
19
|
request: RequestExt,
|
|
18
20
|
response: Response,
|
|
19
21
|
func: MockRequestHandler,
|
|
22
|
+
resolverConfig: ResolverConfig,
|
|
20
23
|
): Promise<void> {
|
|
21
24
|
const mockRequest = new MockRequest(request);
|
|
22
25
|
const mockResponse = await callHandler(mockRequest, response, func);
|
|
@@ -25,18 +28,27 @@ export async function processRequest(
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
await coverageTracker.trackEndpointResponse(scenarioName, scenarioUri, mockResponse);
|
|
28
|
-
processResponse(response, mockResponse);
|
|
31
|
+
processResponse(response, mockResponse, resolverConfig);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
const processResponse = (
|
|
34
|
+
const processResponse = (
|
|
35
|
+
response: Response,
|
|
36
|
+
mockResponse: MockResponse,
|
|
37
|
+
resolverConfig: ResolverConfig,
|
|
38
|
+
) => {
|
|
32
39
|
response.status(mockResponse.status);
|
|
33
40
|
|
|
34
41
|
if (mockResponse.headers) {
|
|
35
|
-
response.set(mockResponse.headers);
|
|
42
|
+
response.set(expandDyns(mockResponse.headers, resolverConfig));
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
if (mockResponse.body) {
|
|
39
|
-
|
|
46
|
+
const raw =
|
|
47
|
+
typeof mockResponse.body.rawContent === "string" ||
|
|
48
|
+
Buffer.isBuffer(mockResponse.body.rawContent)
|
|
49
|
+
? mockResponse.body.rawContent
|
|
50
|
+
: mockResponse.body.rawContent?.serialize(resolverConfig);
|
|
51
|
+
response.contentType(mockResponse.body.contentType).send(raw);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
response.end();
|
package/src/cli/cli.ts
CHANGED
|
@@ -167,7 +167,7 @@ async function main() {
|
|
|
167
167
|
},
|
|
168
168
|
)
|
|
169
169
|
.command(
|
|
170
|
-
"
|
|
170
|
+
"knock <scenariosPaths..>",
|
|
171
171
|
"Executes the test cases against the service",
|
|
172
172
|
(cmd) => {
|
|
173
173
|
return cmd
|
|
@@ -181,23 +181,18 @@ async function main() {
|
|
|
181
181
|
description: "Path to the server",
|
|
182
182
|
type: "string",
|
|
183
183
|
})
|
|
184
|
-
.option("
|
|
185
|
-
description: "
|
|
184
|
+
.option("filter", {
|
|
185
|
+
description: "Glob filter of scenario to run",
|
|
186
186
|
type: "string",
|
|
187
187
|
})
|
|
188
|
-
.
|
|
189
|
-
description: "File that has the Scenarios to run",
|
|
190
|
-
type: "string",
|
|
191
|
-
})
|
|
192
|
-
.demandOption("scenariosPaths", "serverBasePath");
|
|
188
|
+
.demandOption("scenariosPaths");
|
|
193
189
|
},
|
|
194
190
|
async (args) => {
|
|
195
191
|
for (const scenariosPath of args.scenariosPaths) {
|
|
196
192
|
logger.info(`Executing server tests for scenarios at ${scenariosPath}`);
|
|
197
193
|
await serverTest(scenariosPath, {
|
|
198
194
|
baseUrl: args.baseUrl,
|
|
199
|
-
|
|
200
|
-
runScenariosFromFile: args.runScenariosFromFile,
|
|
195
|
+
filter: args.filter,
|
|
201
196
|
});
|
|
202
197
|
}
|
|
203
198
|
},
|
|
@@ -278,20 +273,13 @@ async function main() {
|
|
|
278
273
|
},
|
|
279
274
|
)
|
|
280
275
|
.command(
|
|
281
|
-
"upload-manifest <
|
|
276
|
+
"upload-manifest <scenariosPath>",
|
|
282
277
|
"Upload the scenario manifest. DO NOT CALL in generator.",
|
|
283
278
|
(cmd) => {
|
|
284
279
|
return cmd
|
|
285
|
-
.positional("
|
|
280
|
+
.positional("scenariosPath", {
|
|
286
281
|
description: "Path to the scenarios and mock apis",
|
|
287
282
|
type: "string",
|
|
288
|
-
array: true,
|
|
289
|
-
demandOption: true,
|
|
290
|
-
})
|
|
291
|
-
.option("setName", {
|
|
292
|
-
type: "string",
|
|
293
|
-
description: "Set used to generate the manifest.",
|
|
294
|
-
array: true,
|
|
295
283
|
demandOption: true,
|
|
296
284
|
})
|
|
297
285
|
.option("storageAccountName", {
|
|
@@ -303,14 +291,26 @@ async function main() {
|
|
|
303
291
|
description: "Name of the Container",
|
|
304
292
|
demandOption: true,
|
|
305
293
|
})
|
|
294
|
+
.option("manifestName", {
|
|
295
|
+
type: "string",
|
|
296
|
+
description:
|
|
297
|
+
"Name of the manifest(will be located at manifests/<manifestName>.json in the container).",
|
|
298
|
+
demandOption: true,
|
|
299
|
+
})
|
|
300
|
+
.option("override", {
|
|
301
|
+
type: "boolean",
|
|
302
|
+
description: "Override existing manifest with the same version.",
|
|
303
|
+
default: false,
|
|
304
|
+
})
|
|
306
305
|
.demandOption("storageAccountName");
|
|
307
306
|
},
|
|
308
307
|
async (args) => {
|
|
309
308
|
await uploadScenarioManifest({
|
|
310
|
-
|
|
309
|
+
scenariosPath: args.scenariosPath,
|
|
311
310
|
storageAccountName: args.storageAccountName,
|
|
312
|
-
setNames: args.setName,
|
|
313
311
|
containerName: args.containerName,
|
|
312
|
+
manifestName: args.manifestName,
|
|
313
|
+
override: args.override,
|
|
314
314
|
});
|
|
315
315
|
},
|
|
316
316
|
)
|
package/src/config/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
|
-
import yaml from "
|
|
2
|
+
import yaml from "yaml";
|
|
3
3
|
import { Diagnostic } from "../utils/diagnostic-reporter.js";
|
|
4
4
|
import { SpecConfigJsonSchema } from "./config-schema.js";
|
|
5
5
|
import { SchemaValidator } from "./schema-validator.js";
|
|
@@ -9,7 +9,7 @@ const validator = new SchemaValidator(SpecConfigJsonSchema);
|
|
|
9
9
|
|
|
10
10
|
export async function loadSpecConfig(path: string): Promise<[SpecConfig, Diagnostic[]]> {
|
|
11
11
|
const content = await readFile(path);
|
|
12
|
-
const config: any = yaml.
|
|
12
|
+
const config: any = yaml.parse(content.toString());
|
|
13
13
|
const diagnostics = validator.validate(config, path);
|
|
14
14
|
return [config, diagnostics];
|
|
15
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fail, KeyedMockResponse, MockResponse, PassByKeyScenario,
|
|
1
|
+
import { Fail, KeyedMockResponse, MockResponse, PassByKeyScenario, ScenarioMockApi } from "@typespec/spec-api";
|
|
2
2
|
import { logger } from "../logger.js";
|
|
3
3
|
import { CoverageReport, ScenariosMetadata, ScenarioStatus } from "@typespec/spec-coverage-sdk";
|
|
4
4
|
import { writeFileSync } from "fs";
|
|
@@ -106,7 +106,7 @@ export class CoverageTracker {
|
|
|
106
106
|
return "pass";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function checkByKeys(scenario: PassByKeyScenario
|
|
109
|
+
function checkByKeys(scenario: PassByKeyScenario) {
|
|
110
110
|
for (const endpoint of scenario.apis) {
|
|
111
111
|
const hits = scenarioHits?.get(endpoint.uri);
|
|
112
112
|
if (hits === undefined) {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { loadScenarios } from "../scenarios-resolver.js";
|
|
2
2
|
import { Diagnostic } from "../utils/diagnostic-reporter.js";
|
|
3
|
-
import { getCommit, getPackageJson } from "../utils/misc-utils.js";
|
|
3
|
+
import { getCommit, getPackageJson, type SpectorPackageJson } from "../utils/misc-utils.js";
|
|
4
4
|
import { ScenarioLocation, ScenarioManifest } from "@typespec/spec-coverage-sdk";
|
|
5
|
-
import { getSourceLocation, normalizePath } from "@typespec/compiler";
|
|
5
|
+
import { getSourceLocation, normalizePath, PackageJson } from "@typespec/compiler";
|
|
6
6
|
import { relative } from "path";
|
|
7
7
|
import type { Scenario } from "../lib/decorators.js";
|
|
8
8
|
|
|
9
9
|
export async function computeScenarioManifest(
|
|
10
10
|
scenariosPath: string,
|
|
11
|
-
setName: string
|
|
12
11
|
): Promise<[ScenarioManifest | undefined, readonly Diagnostic[]]> {
|
|
13
12
|
const [scenarios, diagnostics] = await loadScenarios(scenariosPath);
|
|
14
13
|
if (diagnostics.length > 0) {
|
|
@@ -17,19 +16,31 @@ export async function computeScenarioManifest(
|
|
|
17
16
|
|
|
18
17
|
const commit = getCommit(scenariosPath);
|
|
19
18
|
const pkg = await getPackageJson(scenariosPath);
|
|
20
|
-
return [createScenarioManifest(scenariosPath, pkg
|
|
19
|
+
return [createScenarioManifest(scenariosPath, pkg, commit, scenarios), []];
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
function getRepo(pkg: PackageJson): string | undefined {
|
|
23
|
+
const repository = pkg.repository;
|
|
24
|
+
const gitUrl = typeof repository === "string" ? repository : repository?.url;
|
|
25
|
+
if (!gitUrl) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
// Parse git+https://github.com/org/repo.git to https://github.com/org/repo
|
|
29
|
+
return gitUrl.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
30
|
+
}
|
|
23
31
|
export function createScenarioManifest(
|
|
24
32
|
scenariosPath: string,
|
|
25
|
-
|
|
33
|
+
pkg: SpectorPackageJson | undefined,
|
|
26
34
|
commit: string,
|
|
27
35
|
scenarios: Scenario[],
|
|
28
|
-
setName: string
|
|
29
36
|
): ScenarioManifest {
|
|
30
37
|
const sortedScenarios = [...scenarios].sort((a, b) => a.name.localeCompare(b.name));
|
|
31
38
|
return {
|
|
32
|
-
version,
|
|
39
|
+
version: pkg?.version ?? "?",
|
|
40
|
+
repo: pkg && getRepo(pkg),
|
|
41
|
+
sourceUrl: pkg?.spector?.sourceUrl,
|
|
42
|
+
packageName: pkg?.name,
|
|
43
|
+
displayName: pkg && ("displayName" in pkg ? pkg.displayName as string : undefined),
|
|
33
44
|
commit,
|
|
34
45
|
scenarios: sortedScenarios.map(({ name, scenarioDoc, target }) => {
|
|
35
46
|
const tspLocation = getSourceLocation(target);
|
|
@@ -40,6 +51,5 @@ export function createScenarioManifest(
|
|
|
40
51
|
};
|
|
41
52
|
return { name, scenarioDoc, location };
|
|
42
53
|
}),
|
|
43
|
-
setName
|
|
44
54
|
};
|
|
45
55
|
}
|
package/src/logger.ts
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import pc from "picocolors";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const levels = {
|
|
5
|
+
debug: 10,
|
|
6
|
+
info: 20,
|
|
7
|
+
warn: 30,
|
|
8
|
+
error: 30,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Level = keyof typeof levels;
|
|
12
|
+
const levelDisplay: Record<Level, string> = {
|
|
13
|
+
debug: pc.blue("debug"),
|
|
14
|
+
info: pc.green("info"),
|
|
15
|
+
warn: pc.yellow("warn"),
|
|
16
|
+
error: pc.red("error"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface Logger {
|
|
20
|
+
level: Level;
|
|
21
|
+
debug: (message: string, ...data: unknown[]) => void;
|
|
22
|
+
info: (message: string, ...data: unknown[]) => void;
|
|
23
|
+
warn: (message: string, ...data: unknown[]) => void;
|
|
24
|
+
error: (message: string, ...data: unknown[]) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const logger: Logger = {
|
|
4
28
|
level: "info",
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
29
|
+
debug: log("debug"),
|
|
30
|
+
info: log("info"),
|
|
31
|
+
warn: log("warn"),
|
|
32
|
+
error: log("error"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function log(level: Level) {
|
|
36
|
+
return (message: string, ...data: unknown[]) => {
|
|
37
|
+
if (levels[level] >= levels[logger.level]) {
|
|
38
|
+
console.log(levelDisplay[level], message, ...data);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -75,25 +75,39 @@ export class MockApiServer {
|
|
|
75
75
|
verify: rawBinaryBodySaver,
|
|
76
76
|
}),
|
|
77
77
|
);
|
|
78
|
-
this.app.use(multer().any());
|
|
78
|
+
this.app.use(multer().any() as any);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
public use(route: string, ...handlers: RequestHandler[]): void {
|
|
82
82
|
this.app.use(route, ...handlers);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
public start():
|
|
85
|
+
public start(): Promise<number> {
|
|
86
86
|
this.app.use(errorHandler);
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const server = this.app.listen(this.config.port, () => {
|
|
90
|
+
const resolvedPort = getPort(server);
|
|
91
|
+
if (!resolvedPort) {
|
|
92
|
+
logger.error("Failed to resolve port");
|
|
93
|
+
reject(new Error("Failed to resolve port"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
logger.info(`Started server on ${resolvedPort}`);
|
|
97
|
+
resolve(resolvedPort);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.on("error", (err) => {
|
|
101
|
+
logger.error("Error starting server", err);
|
|
102
|
+
reject(err);
|
|
103
|
+
});
|
|
90
104
|
});
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
export type ServerRequestHandler = (request: RequestExt, response: Response) => void;
|
|
95
109
|
|
|
96
|
-
const
|
|
110
|
+
const getPort = (server: Server): number | undefined | null => {
|
|
97
111
|
const address = server?.address();
|
|
98
|
-
return typeof address === "string" ?
|
|
112
|
+
return typeof address === "string" ? null : address?.port;
|
|
99
113
|
};
|
package/src/utils/misc-utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PackageJson } from "@typespec/compiler";
|
|
1
2
|
import { execSync } from "child_process";
|
|
2
3
|
import { readFile, stat } from "fs/promises";
|
|
3
4
|
import { dirname, join } from "path";
|
|
@@ -10,7 +11,7 @@ export async function ensureScenariosPathExists(scenariosPath: string) {
|
|
|
10
11
|
throw new Error(`Scenarios path ${scenariosPath} is not a directory.`);
|
|
11
12
|
}
|
|
12
13
|
} catch (e) {
|
|
13
|
-
throw new Error(`Scenarios path ${scenariosPath} doesn't
|
|
14
|
+
throw new Error(`Scenarios path ${scenariosPath} doesn't exist.`, { cause: e });
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -18,9 +19,10 @@ export function getCommit(path: string): string {
|
|
|
18
19
|
return execSync("git rev-parse HEAD", { cwd: path }).toString().trim();
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export interface PackageJson {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
export interface SpectorPackageJson extends PackageJson {
|
|
23
|
+
spector?: {
|
|
24
|
+
sourceUrl?: string;
|
|
25
|
+
};
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export async function getPackageJson(path: string): Promise<PackageJson | undefined> {
|