Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/thirty-clowns-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@pactflow/openapi-pact-comparator": minor
---

Support URLs for OAS and Pact file arguments

- CLI now accepts http:// and https:// URLs for both OAS and Pact files
- Fixed exit code overflow by capping at 255
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ npm run test

Then, when you're ready for a PR, use
[changesets](https://github.com/changesets/changesets) to describe your PR.
Simply `npm run changesets:add` and follow the prompts.
Simply `npm run changeset:add` and follow the prompts.
93 changes: 93 additions & 0 deletions src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it, beforeAll, afterAll } from "vitest";
import { spawn } from "node:child_process";
import { createServer, type Server } from "node:http";
import { readFileSync } from "node:fs";
import path from "node:path";

const fixturesDir = path.join(__dirname, "fixtures");
const cliPath = path.join(__dirname, "..", "cli.ts");
const tsxCli = require.resolve("tsx/cli");

let server: Server;
let baseUrl: string;

beforeAll(async () => {
server = createServer((req, res) => {
const filePath = path.join(fixturesDir, req.url!);
try {
const content = readFileSync(filePath);
res.writeHead(200);
res.end(content);
} catch {
res.writeHead(404);
res.end("Not Found");
}
});

await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const addr = server.address() as { port: number };
baseUrl = `http://127.0.0.1:${addr.port}`;
});

afterAll(() => server.close());

const runCli = (
oasPath: string,
pactPath: string,
): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
return new Promise((resolve) => {
const child = spawn(
process.execPath,
[tsxCli, cliPath, oasPath, pactPath],
{
cwd: fixturesDir,
},
);

let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});

child.on("close", (exitCode) => {
resolve({ exitCode: exitCode ?? 0, stdout, stderr });
});
});
};

describe("CLI integration", () => {
it("should exit with code 0 when no errors are found", async () => {
const { exitCode, stdout } = await runCli(
path.join("example-petstore-valid", "oas.yaml"),
path.join("example-petstore-valid", "pact.json"),
);

expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("[]");
});

it("should exit with non-zero code when errors are found", async () => {
const { exitCode, stdout } = await runCli(
path.join("example-petstore-invalid", "oas.yaml"),
path.join("example-petstore-invalid", "pact.json"),
);

expect(exitCode).toBe(1);
expect(stdout).toContain('"type":"error"');
expect(stdout).toContain('"code":"response.status.unknown"');
});

it("should fetch OAS and Pact from URLs", async () => {
const oasUrl = `${baseUrl}/example-petstore-valid/oas.yaml`;
const pactUrl = `${baseUrl}/example-petstore-valid/pact.json`;

const { exitCode, stdout } = await runCli(oasUrl, pactUrl);

expect(exitCode).toBe(0);
expect(stdout.trim()).toBe("[]");
});
});
43 changes: 7 additions & 36 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
#!/usr/bin/env node

import { program } from "commander";
import yaml from "js-yaml";
import fs from "node:fs";
import { Comparator } from "./index";
import packageJson from "../package.json";

const readAndParse = async (filename: string) => {
const file = await fs.promises.readFile(filename, { encoding: "utf-8" });
try {
return JSON.parse(file);
} catch (error) {
try {
return yaml.load(file);
} catch (_err) {
throw error;
}
}
};
import { Runner } from "./cli/runner";

program
.version(packageJson.version)
Expand All @@ -29,25 +14,11 @@ Comparison output is presented as ND-JSON, with one line per Pact file.

The exit code equals the number of Pact files with errors (not the number of errors in one comparison).`,
)
.argument("<oas>", "path to OAS file")
.argument("<pact...>", "path(s) to Pact file(s)")
.action(async (oasPath, pactPaths) => {
const oas = await readAndParse(oasPath);
const comparator = new Comparator(oas);

let errors = 0;
for (const pactPath of pactPaths) {
const pact = await readAndParse(pactPath);

const results = [];
for await (const result of comparator.compare(pact)) {
results.push(result);
}

errors += results.some((r) => r.type === "error") ? 1 : 0;
console.log(JSON.stringify(results));
}

process.exit(errors);
.argument("<oas>", "path or URL to OAS file")
.argument("<pact...>", "path(s) or URL(s) to Pact file(s)")
.action(async (oasPath: string, pactPaths: string[]) => {
const runner = new Runner();
const exitCode = await runner.run(oasPath, pactPaths);
process.exit(exitCode);
})
.parse(process.argv);
Loading