Skip to content

Commit e9b06dc

Browse files
CLI improvements: support URL arguments, add tests, and fix minor bug (#480)
* test: add integration tests for cli prior to refactoring * refactor(cli): separate command parsing logic from executin details The existing implementation was not unit-tested (and hard to test). Following the single responsibility principle, and separating the command parsing logic from its execution allows the implementation to be fully tested with unit-tests. * fix: cap exit code at 255 to avoid overflowing back to 0 * feat(cli): support URLs for cli input arguments --------- Co-authored-by: sebastian-tello <[email protected]>
1 parent dd8b80d commit e9b06dc

File tree

6 files changed

+531
-37
lines changed

6 files changed

+531
-37
lines changed

.changeset/thirty-clowns-call.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@pactflow/openapi-pact-comparator": minor
3+
---
4+
5+
Support URLs for OAS and Pact file arguments
6+
7+
- CLI now accepts http:// and https:// URLs for both OAS and Pact files
8+
- Fixed exit code overflow by capping at 255

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ npm run test
1212

1313
Then, when you're ready for a PR, use
1414
[changesets](https://github.com/changesets/changesets) to describe your PR.
15-
Simply `npm run changesets:add` and follow the prompts.
15+
Simply `npm run changeset:add` and follow the prompts.

src/__tests__/cli.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, expect, it, beforeAll, afterAll } from "vitest";
2+
import { spawn } from "node:child_process";
3+
import { createServer, type Server } from "node:http";
4+
import { readFileSync } from "node:fs";
5+
import path from "node:path";
6+
7+
const fixturesDir = path.join(__dirname, "fixtures");
8+
const cliPath = path.join(__dirname, "..", "cli.ts");
9+
const tsxCli = require.resolve("tsx/cli");
10+
11+
let server: Server;
12+
let baseUrl: string;
13+
14+
beforeAll(async () => {
15+
server = createServer((req, res) => {
16+
const filePath = path.join(fixturesDir, req.url!);
17+
try {
18+
const content = readFileSync(filePath);
19+
res.writeHead(200);
20+
res.end(content);
21+
} catch {
22+
res.writeHead(404);
23+
res.end("Not Found");
24+
}
25+
});
26+
27+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
28+
const addr = server.address() as { port: number };
29+
baseUrl = `http://127.0.0.1:${addr.port}`;
30+
});
31+
32+
afterAll(() => server.close());
33+
34+
const runCli = (
35+
oasPath: string,
36+
pactPath: string,
37+
): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
38+
return new Promise((resolve) => {
39+
const child = spawn(
40+
process.execPath,
41+
[tsxCli, cliPath, oasPath, pactPath],
42+
{
43+
cwd: fixturesDir,
44+
},
45+
);
46+
47+
let stdout = "";
48+
let stderr = "";
49+
child.stdout.on("data", (data) => {
50+
stdout += data.toString();
51+
});
52+
child.stderr.on("data", (data) => {
53+
stderr += data.toString();
54+
});
55+
56+
child.on("close", (exitCode) => {
57+
resolve({ exitCode: exitCode ?? 0, stdout, stderr });
58+
});
59+
});
60+
};
61+
62+
describe("CLI integration", () => {
63+
it("should exit with code 0 when no errors are found", async () => {
64+
const { exitCode, stdout } = await runCli(
65+
path.join("example-petstore-valid", "oas.yaml"),
66+
path.join("example-petstore-valid", "pact.json"),
67+
);
68+
69+
expect(exitCode).toBe(0);
70+
expect(stdout.trim()).toBe("[]");
71+
});
72+
73+
it("should exit with non-zero code when errors are found", async () => {
74+
const { exitCode, stdout } = await runCli(
75+
path.join("example-petstore-invalid", "oas.yaml"),
76+
path.join("example-petstore-invalid", "pact.json"),
77+
);
78+
79+
expect(exitCode).toBe(1);
80+
expect(stdout).toContain('"type":"error"');
81+
expect(stdout).toContain('"code":"response.status.unknown"');
82+
});
83+
84+
it("should fetch OAS and Pact from URLs", async () => {
85+
const oasUrl = `${baseUrl}/example-petstore-valid/oas.yaml`;
86+
const pactUrl = `${baseUrl}/example-petstore-valid/pact.json`;
87+
88+
const { exitCode, stdout } = await runCli(oasUrl, pactUrl);
89+
90+
expect(exitCode).toBe(0);
91+
expect(stdout.trim()).toBe("[]");
92+
});
93+
});

src/cli.ts

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
11
#!/usr/bin/env node
22

33
import { program } from "commander";
4-
import yaml from "js-yaml";
5-
import fs from "node:fs";
6-
import { Comparator } from "./index";
74
import packageJson from "../package.json";
8-
9-
const readAndParse = async (filename: string) => {
10-
const file = await fs.promises.readFile(filename, { encoding: "utf-8" });
11-
try {
12-
return JSON.parse(file);
13-
} catch (error) {
14-
try {
15-
return yaml.load(file);
16-
} catch (_err) {
17-
throw error;
18-
}
19-
}
20-
};
5+
import { Runner } from "./cli/runner";
216

227
program
238
.version(packageJson.version)
@@ -29,25 +14,11 @@ Comparison output is presented as ND-JSON, with one line per Pact file.
2914
3015
The exit code equals the number of Pact files with errors (not the number of errors in one comparison).`,
3116
)
32-
.argument("<oas>", "path to OAS file")
33-
.argument("<pact...>", "path(s) to Pact file(s)")
34-
.action(async (oasPath, pactPaths) => {
35-
const oas = await readAndParse(oasPath);
36-
const comparator = new Comparator(oas);
37-
38-
let errors = 0;
39-
for (const pactPath of pactPaths) {
40-
const pact = await readAndParse(pactPath);
41-
42-
const results = [];
43-
for await (const result of comparator.compare(pact)) {
44-
results.push(result);
45-
}
46-
47-
errors += results.some((r) => r.type === "error") ? 1 : 0;
48-
console.log(JSON.stringify(results));
49-
}
50-
51-
process.exit(errors);
17+
.argument("<oas>", "path or URL to OAS file")
18+
.argument("<pact...>", "path(s) or URL(s) to Pact file(s)")
19+
.action(async (oasPath: string, pactPaths: string[]) => {
20+
const runner = new Runner();
21+
const exitCode = await runner.run(oasPath, pactPaths);
22+
process.exit(exitCode);
5223
})
5324
.parse(process.argv);

0 commit comments

Comments
 (0)