Skip to content

Commit 727e227

Browse files
committed
test(c00010 S3): cover remaining CLI command branches
1 parent 20b702f commit 727e227

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* Additional in-process command branches for the c00010 S3 coverage pass.
3+
*
4+
* These tests intentionally exercise the command entry points rather than the
5+
* lower-level services so Vitest's V8 report includes the CLI surface. They
6+
* keep production code untouched: all network calls are faked and all
7+
* filesystem work uses temporary projects.
8+
*/
9+
import { afterEach, describe, expect, test, vi } from "vitest";
10+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
11+
import { join } from "node:path";
12+
import { tmpdir } from "node:os";
13+
import { spawn } from "node:child_process";
14+
15+
import { copyExampleClean } from "../helpers/fixtures";
16+
import { exampleDir } from "../../scripts/helpers/root.helper";
17+
import { OUTPUT_DIR_NAME } from "../../packages/contracts/constants/core/postman.constant";
18+
import { resolveProjectContext } from "../../packages/core/discovery/project-context.service";
19+
import { runInit } from "../../packages/cli/commands/init.script";
20+
import { runList } from "../../packages/cli/commands/list-endpoints.script";
21+
import { runPush } from "../../packages/cli/commands/push.script";
22+
import { main as validateJson } from "../../packages/cli/commands/validate-json.script";
23+
import { runGenerate } from "../../packages/cli/commands/generate.script";
24+
25+
let work = "";
26+
let previousKey: string | undefined;
27+
let previousWorkspace: string | undefined;
28+
let previousPostmanId: string | undefined;
29+
30+
afterEach(async () => {
31+
vi.unstubAllGlobals();
32+
if (previousKey === undefined) delete process.env["POSTMAN_API_KEY"];
33+
else process.env["POSTMAN_API_KEY"] = previousKey;
34+
if (previousWorkspace === undefined) delete process.env["POSTMAN_WORKSPACE"];
35+
else process.env["POSTMAN_WORKSPACE"] = previousWorkspace;
36+
if (previousPostmanId === undefined) delete process.env["POSTMAN_PROJECT_ROOT"];
37+
else process.env["POSTMAN_PROJECT_ROOT"] = previousPostmanId;
38+
if (work) await rm(work, { recursive: true, force: true });
39+
work = "";
40+
});
41+
42+
async function project(name: string): Promise<string> {
43+
if (!work) work = await mkdtemp(join(tmpdir(), "cli-coverage-"));
44+
const root = join(work, name);
45+
await copyExampleClean(exampleDir("express"), root);
46+
return root;
47+
}
48+
49+
async function generated(name: string): Promise<string> {
50+
const root = await project(name);
51+
const outcome = await runGenerate(["--project-root", root]);
52+
expect(outcome.code, "runGenerate must succeed").toBe(0);
53+
return root;
54+
}
55+
56+
function findRequest(item: unknown): { request?: { body?: { mode?: string; raw?: string } } } | null {
57+
if (!item || typeof item !== "object") return null;
58+
const value = item as {
59+
request?: { body?: { mode?: string; raw?: string } };
60+
item?: unknown[];
61+
};
62+
if (value.request) return value;
63+
for (const child of value.item ?? []) {
64+
const found = findRequest(child);
65+
if (found) return found;
66+
}
67+
return null;
68+
}
69+
70+
async function withArgv<T>(args: ReadonlyArray<string>, fn: () => Promise<T>): Promise<T> {
71+
const saved = [...process.argv];
72+
try {
73+
process.argv = ["node", "coverage-branches.spec.ts", ...args];
74+
return await fn();
75+
} finally {
76+
process.argv = saved;
77+
}
78+
}
79+
80+
describe("CLI command branches", () => {
81+
test("validate-json reports a warning when the collection has no auth", async () => {
82+
const root = await generated("validate-no-auth");
83+
const file = join(root, OUTPUT_DIR_NAME, "sample-express.postman_collection.json");
84+
const doc = JSON.parse(await readFile(file, "utf8")) as { auth?: unknown };
85+
delete doc.auth;
86+
await writeFile(file, JSON.stringify(doc));
87+
88+
const code = await validateJson(["--project-root", root]);
89+
expect(code).toBe(0);
90+
});
91+
92+
test("validate-json reports an invalid raw body as a warning", async () => {
93+
const root = await generated("validate-body");
94+
const file = join(root, OUTPUT_DIR_NAME, "sample-express.postman_collection.json");
95+
const doc = JSON.parse(await readFile(file, "utf8")) as { item: unknown[] };
96+
const requestItem = findRequest(doc.item[0]);
97+
expect(requestItem?.request).toBeDefined();
98+
requestItem!.request!.body = { mode: "raw", raw: "not-json" };
99+
await writeFile(file, JSON.stringify(doc));
100+
101+
const code = await validateJson(["--project-root", root]);
102+
expect(code).toBe(0);
103+
});
104+
105+
test("list-endpoints uses the default zone when zoneOrder is empty", async () => {
106+
const root = await generated("list-empty-zones");
107+
const configPath = join(root, "empty-zones.constant.ts");
108+
await writeFile(
109+
configPath,
110+
[
111+
"export const config = {",
112+
" name: 'sample-express',",
113+
" baseUrl: 'http://localhost',",
114+
" variables: [],",
115+
" filePrefixes: {},",
116+
" zones: [],",
117+
" zoneOrder: [],",
118+
" defaultZone: 'Empty zone',",
119+
" authDescriptions: {},",
120+
" loginEndpointName: 'Login',",
121+
" environments: [],",
122+
"};",
123+
].join("\n"),
124+
);
125+
126+
const outcome = await runList(["--project-root", root, "--config", configPath]);
127+
expect(outcome.code).toBe(0);
128+
expect(outcome.endpoints.length).toBeGreaterThan(0);
129+
expect(outcome.endpoints.every((endpoint) => endpoint.zone === "Empty zone")).toBe(true);
130+
});
131+
132+
test("push converts a 401 response into a redacted actionable failure", async () => {
133+
const root = await generated("push-401");
134+
previousKey = process.env["POSTMAN_API_KEY"];
135+
previousWorkspace = process.env["POSTMAN_WORKSPACE"];
136+
process.env["POSTMAN_API_KEY"] = "pmak-FAKE-401";
137+
delete process.env["POSTMAN_WORKSPACE"];
138+
vi.stubGlobal(
139+
"fetch",
140+
(async () => ({
141+
ok: false,
142+
status: 401,
143+
text: async () => "private server detail containing pmak-FAKE-401",
144+
json: async () => ({}),
145+
})) as unknown as typeof fetch,
146+
);
147+
148+
const outcome = await runPush(["--project-root", root, "--no-environments"]);
149+
expect(outcome.code).toBe(1);
150+
expect(outcome.user).toBeNull();
151+
expect(outcome.error?.reason).toContain("Invalid Postman API key (401)");
152+
expect(outcome.error?.nextAction).toContain("key");
153+
expect(JSON.stringify(outcome)).not.toContain("pmak-FAKE-401");
154+
});
155+
156+
test("init writes the non-interactive empty-manifest scaffold", async () => {
157+
if (!work) work = await mkdtemp(join(tmpdir(), "cli-coverage-"));
158+
const root = join(work, "init-no-manifest");
159+
await mkdir(join(root, "routes"), { recursive: true });
160+
await writeFile(
161+
join(root, "routes", "api.php"),
162+
"<?php // Route::get('/health', fn () => response('ok'));",
163+
);
164+
const context = resolveProjectContext({ projectRoot: root });
165+
const outcome = await runInit([], context);
166+
167+
expect(outcome.code).toBe(0);
168+
expect(outcome.projectName).toBe("init-no-manifest");
169+
expect(outcome.authGuards).toEqual(["token"]);
170+
expect(outcome.routeFiles).toEqual(["routes/api.php"]);
171+
await expect(readFile(outcome.configPath ?? "", "utf8")).resolves.toContain("routeFiles");
172+
});
173+
174+
test("summary uses its default text and history path", async () => {
175+
const root = await project("summary-history");
176+
const savedLog = console.log;
177+
const logs: string[] = [];
178+
console.log = (...values: ReadonlyArray<unknown>) => {
179+
logs.push(values.map(String).join(" "));
180+
};
181+
try {
182+
const code = await withArgv(["--project-root", root], async () => {
183+
const { main } = await import("../../packages/cli/commands/summary.script");
184+
return main();
185+
});
186+
expect(code).toBe(0);
187+
expect(logs.join("\n")).toContain("Framework");
188+
} finally {
189+
console.log = savedLog;
190+
}
191+
});
192+
193+
test("watch exits cleanly after SIGINT", async () => {
194+
const root = await project("watch-sigint");
195+
const script = join(process.cwd(), "packages", "cli", "commands", "watch.script.ts");
196+
const child = spawn("bun", [script, "--project-root", root], {
197+
env: { ...process.env },
198+
stdio: ["ignore", "pipe", "pipe"],
199+
});
200+
let output = "";
201+
const stopped = new Promise<void>((resolve, reject) => {
202+
const timer = setTimeout(() => {
203+
child.kill("SIGKILL");
204+
reject(new Error("watch did not reach its running state before timeout"));
205+
}, 90_000);
206+
child.stdout?.on("data", (chunk: Buffer) => {
207+
output += chunk.toString();
208+
if (output.includes("watching") || output.includes("Watching")) {
209+
clearTimeout(timer);
210+
child.kill("SIGINT");
211+
}
212+
});
213+
child.on("error", reject);
214+
child.on("close", (code) => {
215+
clearTimeout(timer);
216+
if (code !== 0 && code !== null) reject(new Error(`watch exited with ${code}: ${output}`));
217+
else resolve();
218+
});
219+
});
220+
await expect(stopped).resolves.toBeUndefined();
221+
}, 120_000);
222+
});

0 commit comments

Comments
 (0)