@typespec/http-server-js 0.58.0-alpha.12-dev.1 → 0.58.0-alpha.12-dev.2
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/.testignore +26 -0
- package/eng/scripts/emit-e2e.js +315 -0
- package/eng/scripts/tspconfig.yaml +6 -0
- package/package.json +17 -3
- package/test/e2e/helpers.ts +59 -0
- package/test/e2e/http/parameters/basic/main.test.e2e.ts +36 -0
- package/test/e2e/http/parameters/body-optionality/main.test.e2e.ts +45 -0
- package/test/e2e/http/parameters/spread/main.test.e2e.ts +92 -0
- package/test/e2e/http/type/model/empty/main.test.e2e.ts +35 -0
- package/test/e2e/spector.ts +33 -0
- package/vitest.config.e2e.ts +20 -0
- package/vitest.config.ts +8 -1
package/.testignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Failed on initial run
|
|
2
|
+
special-words
|
|
3
|
+
authentication/oauth2
|
|
4
|
+
authentication/union
|
|
5
|
+
encode/bytes
|
|
6
|
+
encode/datetime
|
|
7
|
+
encode/duration
|
|
8
|
+
encode/numeric
|
|
9
|
+
payload/content-negotiation
|
|
10
|
+
payload/media-type
|
|
11
|
+
payload/multipart
|
|
12
|
+
payload/xml
|
|
13
|
+
response/status-code-range
|
|
14
|
+
streaming/jsonl
|
|
15
|
+
type/array
|
|
16
|
+
type/dictionary
|
|
17
|
+
type/enum/extensible
|
|
18
|
+
type/enum/fixed
|
|
19
|
+
type/model/inheritance/enum-discriminator
|
|
20
|
+
type/property/additional-properties
|
|
21
|
+
type/property/nullable
|
|
22
|
+
type/property/optionality
|
|
23
|
+
type/property/value-types
|
|
24
|
+
type/scalar
|
|
25
|
+
type/union
|
|
26
|
+
versioning/returnTypeChangedFrom
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { run } from "@typespec/internal-build-utils";
|
|
4
|
+
import pkg from "fs-extra";
|
|
5
|
+
import { copyFile, mkdir, rm } from "fs/promises";
|
|
6
|
+
import { globby } from "globby";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import pLimit from "p-limit";
|
|
10
|
+
import { basename, dirname, join, resolve } from "path";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { hideBin } from "yargs/helpers";
|
|
14
|
+
import yargs from "yargs/yargs";
|
|
15
|
+
|
|
16
|
+
const { pathExists, stat, readFile, writeFile } = pkg;
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
const projectRoot = join(__dirname, "../..");
|
|
22
|
+
const tspConfig = join(__dirname, "tspconfig.yaml");
|
|
23
|
+
|
|
24
|
+
const basePath = join(projectRoot, "node_modules", "@typespec", "http-specs", "specs");
|
|
25
|
+
const ignoreFilePath = join(projectRoot, ".testignore");
|
|
26
|
+
const logDirRoot = join(projectRoot, "temp", "emit-e2e-logs");
|
|
27
|
+
const reportFilePath = join(logDirRoot, "report.txt");
|
|
28
|
+
|
|
29
|
+
// Remove the log directory if it exists.
|
|
30
|
+
async function clearLogDirectory() {
|
|
31
|
+
if (await pathExists(logDirRoot)) {
|
|
32
|
+
await rm(logDirRoot, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse command-line arguments.
|
|
37
|
+
const argv = yargs(hideBin(process.argv))
|
|
38
|
+
.option("interactive", {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
describe: "Enable interactive mode",
|
|
41
|
+
default: false,
|
|
42
|
+
})
|
|
43
|
+
.positional("paths", {
|
|
44
|
+
describe: "Optional list of specific file or directory paths to process (relative to basePath)",
|
|
45
|
+
type: "string",
|
|
46
|
+
array: true,
|
|
47
|
+
default: [],
|
|
48
|
+
})
|
|
49
|
+
.option("build", {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
describe: "Build the generated projects",
|
|
52
|
+
default: false,
|
|
53
|
+
})
|
|
54
|
+
.help().argv;
|
|
55
|
+
|
|
56
|
+
// Read and parse the ignore file.
|
|
57
|
+
async function getIgnoreList() {
|
|
58
|
+
try {
|
|
59
|
+
const content = await readFile(ignoreFilePath, "utf8");
|
|
60
|
+
return content
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.filter((line) => line.trim() && !line.startsWith("#"))
|
|
63
|
+
.map((line) => line.trim());
|
|
64
|
+
} catch {
|
|
65
|
+
console.warn(pc.yellow("No ignore file found."));
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Recursively process paths (files or directories relative to basePath).
|
|
71
|
+
async function processPaths(paths, ignoreList) {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const relativePath of paths) {
|
|
74
|
+
const fullPath = resolve(basePath, relativePath);
|
|
75
|
+
|
|
76
|
+
if (!(await pathExists(fullPath))) {
|
|
77
|
+
console.warn(pc.yellow(`Path not found: ${relativePath}`));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stats = await stat(fullPath);
|
|
82
|
+
if (stats.isFile() && fullPath.endsWith("main.tsp")) {
|
|
83
|
+
if (ignoreList.some((ignore) => relativePath.startsWith(ignore))) continue;
|
|
84
|
+
results.push({ fullPath, relativePath });
|
|
85
|
+
} else if (stats.isDirectory()) {
|
|
86
|
+
const patterns = ["**/main.tsp"];
|
|
87
|
+
const discoveredPaths = await globby(patterns, { cwd: fullPath });
|
|
88
|
+
const validFiles = discoveredPaths
|
|
89
|
+
.map((p) => ({
|
|
90
|
+
fullPath: join(fullPath, p),
|
|
91
|
+
relativePath: join(relativePath, p),
|
|
92
|
+
}))
|
|
93
|
+
.filter((file) => !ignoreList.some((ignore) => file.relativePath.startsWith(ignore)));
|
|
94
|
+
results.push(...validFiles);
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(pc.yellow(`Skipping unsupported path: ${relativePath}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Deduplicate.
|
|
101
|
+
const filesByDir = new Map();
|
|
102
|
+
for (const file of results) {
|
|
103
|
+
const dir = dirname(file.relativePath);
|
|
104
|
+
const existing = filesByDir.get(dir);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
filesByDir.set(dir, file);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Array.from(filesByDir.values());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Run a shell command silently.
|
|
113
|
+
async function runCommand(command, args, options = {}) {
|
|
114
|
+
// Remove clutter by not printing anything; capture output by setting stdio to 'pipe'.
|
|
115
|
+
return await run(command, args, {
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
env: { NODE_ENV: "test", ...process.env },
|
|
118
|
+
silent: true,
|
|
119
|
+
...options,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process a single file.
|
|
124
|
+
async function processFile(file, options) {
|
|
125
|
+
const { fullPath, relativePath } = file;
|
|
126
|
+
const { build, interactive } = options;
|
|
127
|
+
const outputDir = join("test", "e2e", "generated", dirname(relativePath));
|
|
128
|
+
const specCopyPath = join(outputDir, "spec.tsp");
|
|
129
|
+
const logDir = join(projectRoot, "temp", "emit-e2e-logs", dirname(relativePath));
|
|
130
|
+
|
|
131
|
+
let spinner;
|
|
132
|
+
if (interactive) {
|
|
133
|
+
spinner = ora({ text: `Processing: ${relativePath}`, color: "cyan" }).start();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
if (await pathExists(outputDir)) {
|
|
138
|
+
if (spinner) spinner.text = `Clearing directory: ${outputDir}`;
|
|
139
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
if (spinner) spinner.text = `Creating directory: ${outputDir}`;
|
|
142
|
+
await mkdir(outputDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
if (spinner) spinner.text = `Copying spec to: ${specCopyPath}`;
|
|
145
|
+
await copyFile(fullPath, specCopyPath);
|
|
146
|
+
|
|
147
|
+
if (spinner) spinner.text = `Compiling: ${relativePath}`;
|
|
148
|
+
await runCommand("npx", [
|
|
149
|
+
"tsp",
|
|
150
|
+
"compile",
|
|
151
|
+
fullPath,
|
|
152
|
+
"--emit",
|
|
153
|
+
resolve(import.meta.dirname, "../.."),
|
|
154
|
+
"--config",
|
|
155
|
+
tspConfig,
|
|
156
|
+
"--output-dir",
|
|
157
|
+
outputDir,
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
if (spinner) spinner.text = `Formatting with Prettier: ${relativePath}`;
|
|
161
|
+
await runCommand("npx", ["prettier", outputDir, "--write"]);
|
|
162
|
+
|
|
163
|
+
if (build) {
|
|
164
|
+
if (spinner) spinner.text = `Building project: ${relativePath}`;
|
|
165
|
+
await runCommand("npm", ["run", "build"], { cwd: outputDir });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (spinner) {
|
|
169
|
+
spinner.succeed(`Finished processing: ${relativePath}`);
|
|
170
|
+
}
|
|
171
|
+
return { status: "succeeded", relativePath };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (spinner) {
|
|
174
|
+
spinner.fail(`Failed processing: ${relativePath}`);
|
|
175
|
+
}
|
|
176
|
+
const errorDetails = error.stdout || error.stderr || error.message;
|
|
177
|
+
|
|
178
|
+
// Write error details to a log file.
|
|
179
|
+
await mkdir(logDir, { recursive: true });
|
|
180
|
+
const logFilePath = join(logDir, `${basename(relativePath, ".tsp")}-error.log`);
|
|
181
|
+
await writeFile(logFilePath, errorDetails, "utf8");
|
|
182
|
+
|
|
183
|
+
if (interactive) {
|
|
184
|
+
const { action } = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: "list",
|
|
187
|
+
name: "action",
|
|
188
|
+
message: `Processing failed for ${relativePath}. What would you like to do?`,
|
|
189
|
+
choices: [
|
|
190
|
+
{ name: "Retry", value: "retry" },
|
|
191
|
+
{ name: "Skip to next file", value: "next" },
|
|
192
|
+
{ name: "Abort processing", value: "abort" },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if (action === "retry") {
|
|
198
|
+
if (spinner) spinner.start(`Retrying: ${relativePath}`);
|
|
199
|
+
return await processFile(file, options);
|
|
200
|
+
} else if (action === "next") {
|
|
201
|
+
console.log(pc.yellow(`Skipping: ${relativePath}`));
|
|
202
|
+
} else if (action === "abort") {
|
|
203
|
+
console.log(pc.red("Aborting processing."));
|
|
204
|
+
throw new Error("Processing aborted by user");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { status: "failed", relativePath, errorDetails };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process all files.
|
|
212
|
+
async function processFiles(files, options) {
|
|
213
|
+
const { interactive } = options;
|
|
214
|
+
const succeeded = [];
|
|
215
|
+
const failed = [];
|
|
216
|
+
|
|
217
|
+
if (interactive) {
|
|
218
|
+
// Sequential processing so each spinner is visible.
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await processFile(file, options);
|
|
222
|
+
if (result.status === "succeeded") {
|
|
223
|
+
succeeded.push(result.relativePath);
|
|
224
|
+
} else {
|
|
225
|
+
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Global progress spinner.
|
|
233
|
+
const total = files.length;
|
|
234
|
+
let completed = 0;
|
|
235
|
+
const globalSpinner = ora({ text: `Processing 0/${total} files...`, color: "cyan" }).start();
|
|
236
|
+
const limit = pLimit(4);
|
|
237
|
+
const tasks = files.map((file) =>
|
|
238
|
+
limit(() =>
|
|
239
|
+
processFile(file, options).then((result) => {
|
|
240
|
+
completed++;
|
|
241
|
+
globalSpinner.text = `Processing ${completed}/${total} files...`;
|
|
242
|
+
return result;
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
const results = await Promise.all(tasks);
|
|
247
|
+
globalSpinner.succeed(`Processed ${total} files`);
|
|
248
|
+
for (const result of results) {
|
|
249
|
+
if (result.status === "succeeded") {
|
|
250
|
+
succeeded.push(result.relativePath);
|
|
251
|
+
} else {
|
|
252
|
+
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(pc.bold(pc.green("\nProcessing Complete:")));
|
|
258
|
+
console.log(pc.green(`Succeeded: ${succeeded.length}`));
|
|
259
|
+
console.log(pc.red(`Failed: ${failed.length}`));
|
|
260
|
+
|
|
261
|
+
if (failed.length > 0) {
|
|
262
|
+
console.log(pc.red("\nFailed Specs:"));
|
|
263
|
+
failed.forEach((f) => {
|
|
264
|
+
console.log(pc.red(` - ${f.relativePath}`));
|
|
265
|
+
});
|
|
266
|
+
console.log(pc.blue(`\nLogs available at: ${logDirRoot}`));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Ensure the log directory exists before writing the report.
|
|
270
|
+
await mkdir(logDirRoot, { recursive: true });
|
|
271
|
+
const report = [
|
|
272
|
+
"Succeeded Files:",
|
|
273
|
+
...succeeded.map((f) => ` - ${f}`),
|
|
274
|
+
"Failed Files:",
|
|
275
|
+
...failed.map((f) => ` - ${f.relativePath}\n Error: ${f.errorDetails}`),
|
|
276
|
+
].join("\n");
|
|
277
|
+
await writeFile(reportFilePath, report, "utf8");
|
|
278
|
+
console.log(pc.blue(`Report written to: ${reportFilePath}`));
|
|
279
|
+
}
|
|
280
|
+
// Main execution function
|
|
281
|
+
async function main() {
|
|
282
|
+
const startTime = process.hrtime.bigint(); // ✅ High precision time tracking
|
|
283
|
+
let exitCode = 0; // ✅ Track success/failure
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await clearLogDirectory(); // ✅ Clear logs at the start
|
|
287
|
+
|
|
288
|
+
const ignoreList = await getIgnoreList();
|
|
289
|
+
const paths = argv._.length
|
|
290
|
+
? await processPaths(argv._, ignoreList)
|
|
291
|
+
: await processPaths(["."], ignoreList);
|
|
292
|
+
|
|
293
|
+
if (paths.length === 0) {
|
|
294
|
+
console.log(pc.yellow("⚠️ No files to process."));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await processFiles(paths, {
|
|
299
|
+
interactive: argv.interactive,
|
|
300
|
+
build: argv.build,
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(pc.red(`❌ Fatal Error: ${error.message}`));
|
|
304
|
+
exitCode = 1; // ✅ Ensure graceful failure handling
|
|
305
|
+
} finally {
|
|
306
|
+
// ✅ Always log execution time before exit
|
|
307
|
+
const endTime = process.hrtime.bigint();
|
|
308
|
+
const duration = Number(endTime - startTime) / 1e9; // Convert nanoseconds to seconds
|
|
309
|
+
console.log(pc.blue(`⏱️ Total execution time: ${duration.toFixed(2)} seconds`));
|
|
310
|
+
|
|
311
|
+
process.exit(exitCode); // ✅ Ensures proper exit handling
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typespec/http-server-js",
|
|
3
|
-
"version": "0.58.0-alpha.12-dev.
|
|
3
|
+
"version": "0.58.0-alpha.12-dev.2",
|
|
4
4
|
"author": "Microsoft Corporation",
|
|
5
5
|
"description": "TypeSpec HTTP server code generator for JavaScript",
|
|
6
6
|
"homepage": "https://github.com/microsoft/typespec",
|
|
@@ -47,12 +47,23 @@
|
|
|
47
47
|
"@types/node": "~22.13.9",
|
|
48
48
|
"@typespec/compiler": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
49
49
|
"@typespec/http": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
50
|
+
"@typespec/http-specs": "^0.1.0-alpha.15 || >=0.1.0-alpha.16-dev <0.1.0-alpha.16",
|
|
51
|
+
"@typespec/internal-build-utils": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
50
52
|
"@typespec/openapi3": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
53
|
+
"@typespec/spector": "^0.1.0-alpha.9 || >=0.1.0-alpha.10-dev <0.1.0-alpha.10",
|
|
51
54
|
"@vitest/coverage-v8": "^3.0.7",
|
|
52
55
|
"@vitest/ui": "^3.0.7",
|
|
56
|
+
"fs-extra": "^11.2.0",
|
|
57
|
+
"globby": "~14.1.0",
|
|
58
|
+
"inquirer": "^12.2.0",
|
|
59
|
+
"ora": "^8.1.1",
|
|
60
|
+
"p-limit": "^6.2.0",
|
|
61
|
+
"pathe": "^2.0.3",
|
|
62
|
+
"picocolors": "~1.1.1",
|
|
53
63
|
"tsx": "^4.19.3",
|
|
54
64
|
"typescript": "~5.8.2",
|
|
55
|
-
"vitest": "^3.0.7"
|
|
65
|
+
"vitest": "^3.0.7",
|
|
66
|
+
"yargs": "~17.7.2"
|
|
56
67
|
},
|
|
57
68
|
"scripts": {
|
|
58
69
|
"clean": "rimraf ./dist ./temp",
|
|
@@ -66,6 +77,9 @@
|
|
|
66
77
|
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
|
|
67
78
|
"lint": "eslint . --max-warnings=0",
|
|
68
79
|
"lint:fix": "eslint . --fix",
|
|
69
|
-
"regen-docs": "echo Doc generation disabled for this package."
|
|
80
|
+
"regen-docs": "echo Doc generation disabled for this package.",
|
|
81
|
+
"test:e2e": "npm run emit:e2e && npm run run:e2e",
|
|
82
|
+
"emit:e2e": "node eng/scripts/emit-e2e.js",
|
|
83
|
+
"run:e2e": "vitest run --config ./vitest.config.e2e.js"
|
|
70
84
|
}
|
|
71
85
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, Server, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
interface BasicRouter {
|
|
4
|
+
/**
|
|
5
|
+
* Dispatches the request to the appropriate service based on the request path.
|
|
6
|
+
*
|
|
7
|
+
* This member function may be used directly as a handler for a Node HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* @param request - The incoming HTTP request.
|
|
10
|
+
* @param response - The outgoing HTTP response.
|
|
11
|
+
*/
|
|
12
|
+
dispatch(request: IncomingMessage, response: ServerResponse): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function startServer(router: BasicRouter, abortSignal: AbortSignal): Promise<string> {
|
|
16
|
+
return new Promise<string>((resolve, reject) => {
|
|
17
|
+
if (abortSignal.aborted) {
|
|
18
|
+
return reject(new Error("Server start cancelled"));
|
|
19
|
+
}
|
|
20
|
+
const server = createServer((req, res) => router.dispatch(req, res));
|
|
21
|
+
const stop = () => {
|
|
22
|
+
return new Promise<void>((r) => {
|
|
23
|
+
server.close(() => r);
|
|
24
|
+
server.closeAllConnections();
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
abortSignal.addEventListener("abort", () => {
|
|
28
|
+
stop().catch(() => {});
|
|
29
|
+
});
|
|
30
|
+
server.listen(function (this: Server) {
|
|
31
|
+
const address = this.address();
|
|
32
|
+
if (!address) {
|
|
33
|
+
reject(new Error("Server address not available"));
|
|
34
|
+
stop().catch(() => {});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resolve(typeof address === "string" ? address : `http://localhost:${address.port}`);
|
|
39
|
+
});
|
|
40
|
+
server.on("error", reject);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Meant to be used for the `onInternalError` handler in the router
|
|
46
|
+
* in order to log any failed test assertions from within service handlers.
|
|
47
|
+
* Purely informational as the test will fail regardless.
|
|
48
|
+
* @param ctx The HttpContext from the http-server-js router
|
|
49
|
+
*/
|
|
50
|
+
function logAssertionErrors(ctx: any, error: Error): void {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(error);
|
|
53
|
+
ctx.response.statusCode = 599;
|
|
54
|
+
ctx.response.end();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const testRouterOptions = {
|
|
58
|
+
onInternalError: logAssertionErrors,
|
|
59
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createBasicRouter } from "../../../generated/parameters/basic/src/generated/http/router.js";
|
|
4
|
+
import { startServer, testRouterOptions } from "../../../helpers.js";
|
|
5
|
+
import { runScenario } from "../../../spector.js";
|
|
6
|
+
|
|
7
|
+
describe("Parameters.Basic", () => {
|
|
8
|
+
let serverAbortController: AbortController;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
serverAbortController = new AbortController();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
serverAbortController.abort();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes all scenarios", async () => {
|
|
17
|
+
const router = createBasicRouter(
|
|
18
|
+
{
|
|
19
|
+
async simple(ctx, body) {
|
|
20
|
+
deepStrictEqual(body, { name: "foo" });
|
|
21
|
+
return { statusCode: 204 };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
async simple(ctx, name) {
|
|
26
|
+
deepStrictEqual(name, "foo");
|
|
27
|
+
return { statusCode: 204 };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
testRouterOptions,
|
|
31
|
+
);
|
|
32
|
+
const baseUrl = await startServer(router, serverAbortController.signal);
|
|
33
|
+
const { status } = await runScenario("parameters/basic/**/*", baseUrl);
|
|
34
|
+
expect(status).toBe("pass");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { afterEach, assert, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createBodyOptionalityRouter } from "../../../generated/parameters/body-optionality/src/generated/http/router.js";
|
|
4
|
+
import { startServer, testRouterOptions } from "../../../helpers.js";
|
|
5
|
+
import { runScenario } from "../../../spector.js";
|
|
6
|
+
|
|
7
|
+
describe("Parameters.BodyOptionality", () => {
|
|
8
|
+
let serverAbortController: AbortController;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
serverAbortController = new AbortController();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
serverAbortController.abort();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes all scenarios", async () => {
|
|
17
|
+
const router = createBodyOptionalityRouter(
|
|
18
|
+
{
|
|
19
|
+
async requiredExplicit(ctx, body) {
|
|
20
|
+
deepStrictEqual(body, { name: "foo" });
|
|
21
|
+
return { statusCode: 204 };
|
|
22
|
+
},
|
|
23
|
+
async requiredImplicit(ctx, name) {
|
|
24
|
+
deepStrictEqual(name, "foo");
|
|
25
|
+
return { statusCode: 204 };
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
async omit(ctx, options) {
|
|
30
|
+
assert.isUndefined(options?.body);
|
|
31
|
+
return { statusCode: 204 };
|
|
32
|
+
},
|
|
33
|
+
async set(ctx, options) {
|
|
34
|
+
assert(options?.body);
|
|
35
|
+
assert.deepStrictEqual(options.body, { name: "foo" });
|
|
36
|
+
return { statusCode: 204 };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
testRouterOptions,
|
|
40
|
+
);
|
|
41
|
+
const baseUrl = await startServer(router, serverAbortController.signal);
|
|
42
|
+
const { status } = await runScenario("parameters/body-optionality/**/*", baseUrl);
|
|
43
|
+
expect(status).toBe("pass");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createSpreadRouter } from "../../../generated/parameters/spread/src/generated/http/router.js";
|
|
4
|
+
import { startServer, testRouterOptions } from "../../../helpers.js";
|
|
5
|
+
import { runScenario } from "../../../spector.js";
|
|
6
|
+
|
|
7
|
+
describe("Parameters.Spread", () => {
|
|
8
|
+
let serverAbortController: AbortController;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
serverAbortController = new AbortController();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
serverAbortController.abort();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes all scenarios", async () => {
|
|
17
|
+
const router = createSpreadRouter(
|
|
18
|
+
{
|
|
19
|
+
async spreadAsRequestBody(ctx, name) {
|
|
20
|
+
deepStrictEqual(name, "foo");
|
|
21
|
+
return { statusCode: 204 };
|
|
22
|
+
},
|
|
23
|
+
async spreadCompositeRequest(ctx, name, testHeader, body) {
|
|
24
|
+
deepStrictEqual(name, "foo");
|
|
25
|
+
deepStrictEqual(testHeader, "bar");
|
|
26
|
+
deepStrictEqual(body, { name: "foo" });
|
|
27
|
+
return { statusCode: 204 };
|
|
28
|
+
},
|
|
29
|
+
async spreadCompositeRequestMix(ctx, name, testHeader, prop) {
|
|
30
|
+
deepStrictEqual(name, "foo");
|
|
31
|
+
deepStrictEqual(testHeader, "bar");
|
|
32
|
+
deepStrictEqual(prop, "foo");
|
|
33
|
+
return { statusCode: 204 };
|
|
34
|
+
},
|
|
35
|
+
async spreadCompositeRequestOnlyWithBody(ctx, body) {
|
|
36
|
+
deepStrictEqual(body, { name: "foo" });
|
|
37
|
+
return { statusCode: 204 };
|
|
38
|
+
},
|
|
39
|
+
async spreadCompositeRequestWithoutBody(ctx, name, testHeader) {
|
|
40
|
+
deepStrictEqual(name, "foo");
|
|
41
|
+
deepStrictEqual(testHeader, "bar");
|
|
42
|
+
return { statusCode: 204 };
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
async spreadAsRequestBody(ctx, name) {
|
|
47
|
+
deepStrictEqual(name, "foo");
|
|
48
|
+
return { statusCode: 204 };
|
|
49
|
+
},
|
|
50
|
+
async spreadAsRequestParameter(ctx, id, xMsTestHeader, name) {
|
|
51
|
+
deepStrictEqual(id, "1");
|
|
52
|
+
deepStrictEqual(xMsTestHeader, "bar");
|
|
53
|
+
deepStrictEqual(name, "foo");
|
|
54
|
+
return { statusCode: 204 };
|
|
55
|
+
},
|
|
56
|
+
async spreadParameterWithInnerAlias(ctx, id, name, age, xMsTestHeader) {
|
|
57
|
+
deepStrictEqual(id, "1");
|
|
58
|
+
deepStrictEqual(name, "foo");
|
|
59
|
+
deepStrictEqual(age, 1);
|
|
60
|
+
deepStrictEqual(xMsTestHeader, "bar");
|
|
61
|
+
return { statusCode: 204 };
|
|
62
|
+
},
|
|
63
|
+
async spreadParameterWithInnerModel(ctx, id, name, xMsTestHeader) {
|
|
64
|
+
deepStrictEqual(id, "1");
|
|
65
|
+
deepStrictEqual(name, "foo");
|
|
66
|
+
deepStrictEqual(xMsTestHeader, "bar");
|
|
67
|
+
return { statusCode: 204 };
|
|
68
|
+
},
|
|
69
|
+
async spreadWithMultipleParameters(
|
|
70
|
+
ctx,
|
|
71
|
+
id,
|
|
72
|
+
xMsTestHeader,
|
|
73
|
+
requiredString,
|
|
74
|
+
requiredIntList,
|
|
75
|
+
options,
|
|
76
|
+
) {
|
|
77
|
+
deepStrictEqual(id, "1");
|
|
78
|
+
deepStrictEqual(xMsTestHeader, "bar");
|
|
79
|
+
deepStrictEqual(requiredString, "foo");
|
|
80
|
+
deepStrictEqual(requiredIntList, [1, 2]);
|
|
81
|
+
deepStrictEqual(options?.optionalInt, 1);
|
|
82
|
+
deepStrictEqual(options?.optionalStringList, ["foo", "bar"]);
|
|
83
|
+
return { statusCode: 204 };
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
testRouterOptions,
|
|
87
|
+
);
|
|
88
|
+
const baseUrl = await startServer(router, serverAbortController.signal);
|
|
89
|
+
const { status } = await runScenario("parameters/spread/**/*", baseUrl);
|
|
90
|
+
expect(status).toBe("pass");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createEmptyRouter } from "../../../../generated/type/model/empty/src/generated/http/router.js";
|
|
4
|
+
import { startServer, testRouterOptions } from "../../../../helpers.js";
|
|
5
|
+
import { runScenario } from "../../../../spector.js";
|
|
6
|
+
|
|
7
|
+
describe("Type.Model.Empty", () => {
|
|
8
|
+
let serverAbortController: AbortController;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
serverAbortController = new AbortController();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
serverAbortController.abort();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes all scenarios", async () => {
|
|
17
|
+
const router = createEmptyRouter(
|
|
18
|
+
{
|
|
19
|
+
async getEmpty(ctx) {
|
|
20
|
+
return { body: {} };
|
|
21
|
+
},
|
|
22
|
+
async postRoundTripEmpty(ctx, body) {
|
|
23
|
+
return { body };
|
|
24
|
+
},
|
|
25
|
+
async putEmpty(ctx, input) {
|
|
26
|
+
deepStrictEqual(input, {});
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
testRouterOptions,
|
|
30
|
+
);
|
|
31
|
+
const baseUrl = await startServer(router, serverAbortController.signal);
|
|
32
|
+
const { status } = await runScenario("type/model/empty/**/*", baseUrl);
|
|
33
|
+
expect(status).toBe("pass");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { run } from "@typespec/internal-build-utils";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "pathe";
|
|
4
|
+
|
|
5
|
+
// Root of `http-server-js` package so vscode test integration runs from the correct directory
|
|
6
|
+
const CWD = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
|
+
|
|
8
|
+
export async function runScenario(
|
|
9
|
+
scenario: string,
|
|
10
|
+
baseUrl: string,
|
|
11
|
+
): Promise<{ status: "pass" | "fail" }> {
|
|
12
|
+
try {
|
|
13
|
+
await run(
|
|
14
|
+
"npx",
|
|
15
|
+
[
|
|
16
|
+
"tsp-spector",
|
|
17
|
+
"knock",
|
|
18
|
+
"./node_modules/@typespec/http-specs/specs",
|
|
19
|
+
"--filter",
|
|
20
|
+
scenario,
|
|
21
|
+
"--baseUrl",
|
|
22
|
+
baseUrl,
|
|
23
|
+
],
|
|
24
|
+
{
|
|
25
|
+
shell: true,
|
|
26
|
+
cwd: CWD,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
return { status: "pass" };
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return { status: "fail" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
2
|
+
import { defaultTypeSpecVitestConfig } from "../../vitest.workspace.js";
|
|
3
|
+
|
|
4
|
+
export default mergeConfig(
|
|
5
|
+
defaultTypeSpecVitestConfig,
|
|
6
|
+
defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
environment: "node",
|
|
9
|
+
testTimeout: 10_000,
|
|
10
|
+
isolate: false,
|
|
11
|
+
coverage: {
|
|
12
|
+
reporter: ["cobertura", "json", "text"],
|
|
13
|
+
},
|
|
14
|
+
outputFile: {
|
|
15
|
+
junit: "./test-results.xml",
|
|
16
|
+
},
|
|
17
|
+
include: ["test/**/*.e2e.ts"],
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
package/vitest.config.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
2
2
|
import { defaultTypeSpecVitestConfig } from "../../vitest.workspace.js";
|
|
3
3
|
|
|
4
|
-
export default mergeConfig(
|
|
4
|
+
export default mergeConfig(
|
|
5
|
+
defaultTypeSpecVitestConfig,
|
|
6
|
+
defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
include: ["test/**/*.test.ts"],
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
);
|