Skip to content

Commit 2529d6b

Browse files
committed
test: add tests for StaticSitesClient exit code and stderr handling
- stderr is captured and passed to logger.error - Non-zero exit code triggers spinner.fail and error message - process.exit(1) is called on binary failure - Success path (exit code 0) remains unchanged
1 parent 34939d6 commit 2529d6b

1 file changed

Lines changed: 97 additions & 14 deletions

File tree

src/cli/commands/deploy/deploy.spec.ts

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import "../../../../tests/_mocks/fs.js";
2-
import child_process from "node:child_process";
2+
import child_process, { spawn } from "node:child_process";
3+
import { EventEmitter } from "node:events";
4+
import path from "node:path";
35
import { logger } from "../../../core/utils/logger.js";
46
import { vol } from "memfs";
57
import * as accountModule from "../../../core/account.js";
@@ -10,6 +12,26 @@ import { loadPackageJson } from "../../../core/utils/json.js";
1012

1113
const pkg = loadPackageJson();
1214

15+
// Prevent transitive jsonwebtoken/buffer-equal-constant-time loading error
16+
// by fully mocking modules that pull in Azure SDK → jsonwebtoken chain
17+
vi.mock("../../../core/account.js", () => ({
18+
chooseOrCreateProjectDetails: vi.fn(() => Promise.resolve({ resourceGroup: "mock-rg", staticSiteName: "mock-site" })),
19+
getStaticSiteDeployment: vi.fn(() => Promise.resolve({})),
20+
authenticateWithAzureIdentity: vi.fn(),
21+
listSubscriptions: vi.fn(),
22+
listTenants: vi.fn(),
23+
}));
24+
25+
vi.mock("../login/login.js", () => ({
26+
login: vi.fn(() =>
27+
Promise.resolve({
28+
credentialChain: {},
29+
subscriptionId: "mock-subscription-id",
30+
}),
31+
),
32+
loginCommand: vi.fn(),
33+
}));
34+
1335
vi.mock("../../../core/utils/logger", () => {
1436
return {
1537
logger: {
@@ -22,8 +44,15 @@ vi.mock("../../../core/utils/logger", () => {
2244
};
2345
});
2446

25-
//vi.spyOn(process, "exit").mockImplementation(() => {});
26-
vi.spyOn(child_process, "spawn").mockImplementation(vi.fn());
47+
vi.mock("node:child_process", async (importOriginal) => {
48+
const actual: typeof child_process = await importOriginal();
49+
return {
50+
...actual,
51+
default: { ...actual, spawn: vi.fn() },
52+
spawn: vi.fn(),
53+
};
54+
});
55+
2756
vi.spyOn(deployClientModule, "getDeployClientPath").mockImplementation(() => {
2857
return Promise.resolve({
2958
binary: "mock-binary",
@@ -32,17 +61,6 @@ vi.spyOn(deployClientModule, "getDeployClientPath").mockImplementation(() => {
3261
});
3362
vi.spyOn(deployClientModule, "cleanUp").mockImplementation(() => {});
3463

35-
vi.spyOn(accountModule, "getStaticSiteDeployment").mockImplementation(() => Promise.resolve({}));
36-
37-
vi.spyOn(loginModule, "login").mockImplementation(() => {
38-
return Promise.resolve({
39-
credentialChain: {} as any,
40-
subscriptionId: "mock-subscription-id",
41-
resourceGroup: "mock-resource-group-name",
42-
staticSiteName: "mock-static-site-name",
43-
});
44-
});
45-
4664
describe("deploy", () => {
4765
const OLD_ENV = process.env;
4866

@@ -177,4 +195,69 @@ describe("deploy", () => {
177195
},
178196
});
179197
});
198+
199+
describe("StaticSitesClient process handling", () => {
200+
let mockChild: EventEmitter & { stdout: EventEmitter; stderr: EventEmitter };
201+
let exitSpy: ReturnType<typeof vi.spyOn>;
202+
203+
beforeEach(() => {
204+
// Create mock child process with stdout/stderr EventEmitters
205+
const stdout = new EventEmitter();
206+
const stderr = new EventEmitter();
207+
mockChild = Object.assign(new EventEmitter(), { stdout, stderr });
208+
209+
// Set up spawn mock to return the mock child process
210+
vi.mocked(spawn).mockReturnValue(mockChild as any);
211+
vi.spyOn(deployClientModule, "getDeployClientPath").mockResolvedValue({
212+
binary: "mock-binary",
213+
buildId: "0.0.0",
214+
});
215+
vi.spyOn(deployClientModule, "cleanUp").mockImplementation(() => {});
216+
217+
// Mock process.exit to prevent test runner from exiting
218+
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
219+
220+
// Provide deployment token via env to skip login flow
221+
process.env.SWA_CLI_DEPLOYMENT_TOKEN = "test-token";
222+
223+
// Create required filesystem structure in memfs
224+
const cwd = process.cwd();
225+
vol.fromJSON({
226+
[path.join("/test-output", "index.html")]: "hello",
227+
[path.join(cwd, "placeholder")]: "",
228+
});
229+
});
230+
231+
it("should capture stderr and pass to logger.error", async () => {
232+
await deploy({ outputLocation: "/test-output", dryRun: false });
233+
234+
mockChild.stderr.emit("data", Buffer.from("some error from binary"));
235+
236+
expect(logger.error).toHaveBeenCalledWith("some error from binary");
237+
});
238+
239+
it("should fail spinner and log error on non-zero exit code", async () => {
240+
await deploy({ outputLocation: "/test-output", dryRun: false });
241+
242+
mockChild.emit("close", 1);
243+
244+
expect(logger.error).toHaveBeenCalledWith("The deployment binary exited with code 1.");
245+
});
246+
247+
it("should call process.exit(1) on non-zero exit code", async () => {
248+
await deploy({ outputLocation: "/test-output", dryRun: false });
249+
250+
mockChild.emit("close", 127);
251+
252+
expect(exitSpy).toHaveBeenCalledWith(1);
253+
});
254+
255+
it("should succeed without calling process.exit on exit code 0", async () => {
256+
await deploy({ outputLocation: "/test-output", dryRun: false });
257+
258+
mockChild.emit("close", 0);
259+
260+
expect(exitSpy).not.toHaveBeenCalled();
261+
});
262+
});
180263
});

0 commit comments

Comments
 (0)