Skip to content
Open
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
121 changes: 107 additions & 14 deletions src/cli/commands/deploy/deploy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "../../../../tests/_mocks/fs.js";
import child_process from "node:child_process";
import child_process, { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import path from "node:path";
import { logger } from "../../../core/utils/logger.js";
import { vol } from "memfs";
import * as accountModule from "../../../core/account.js";
Expand All @@ -10,6 +12,26 @@ import { loadPackageJson } from "../../../core/utils/json.js";

const pkg = loadPackageJson();

// Prevent transitive jsonwebtoken/buffer-equal-constant-time loading error
// by fully mocking modules that pull in Azure SDK → jsonwebtoken chain
vi.mock("../../../core/account.js", () => ({
chooseOrCreateProjectDetails: vi.fn(() => Promise.resolve({ resourceGroup: "mock-rg", staticSiteName: "mock-site" })),
getStaticSiteDeployment: vi.fn(() => Promise.resolve({})),
authenticateWithAzureIdentity: vi.fn(),
listSubscriptions: vi.fn(),
listTenants: vi.fn(),
}));

vi.mock("../login/login.js", () => ({
login: vi.fn(() =>
Promise.resolve({
credentialChain: {},
subscriptionId: "mock-subscription-id",
}),
),
loginCommand: vi.fn(),
}));

vi.mock("../../../core/utils/logger", () => {
return {
logger: {
Expand All @@ -22,8 +44,15 @@ vi.mock("../../../core/utils/logger", () => {
};
});

//vi.spyOn(process, "exit").mockImplementation(() => {});
vi.spyOn(child_process, "spawn").mockImplementation(vi.fn());
vi.mock("node:child_process", async (importOriginal) => {
const actual: typeof child_process = await importOriginal();
return {
...actual,
default: { ...actual, spawn: vi.fn() },
spawn: vi.fn(),
};
});

vi.spyOn(deployClientModule, "getDeployClientPath").mockImplementation(() => {
return Promise.resolve({
binary: "mock-binary",
Expand All @@ -32,17 +61,6 @@ vi.spyOn(deployClientModule, "getDeployClientPath").mockImplementation(() => {
});
vi.spyOn(deployClientModule, "cleanUp").mockImplementation(() => {});

vi.spyOn(accountModule, "getStaticSiteDeployment").mockImplementation(() => Promise.resolve({}));

vi.spyOn(loginModule, "login").mockImplementation(() => {
return Promise.resolve({
credentialChain: {} as any,
subscriptionId: "mock-subscription-id",
resourceGroup: "mock-resource-group-name",
staticSiteName: "mock-static-site-name",
});
});

describe("deploy", () => {
const OLD_ENV = process.env;

Expand Down Expand Up @@ -177,4 +195,79 @@ describe("deploy", () => {
},
});
});

describe("StaticSitesClient process handling", () => {
let mockChild: EventEmitter & { stdout: EventEmitter; stderr: EventEmitter };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let exitSpy: any;

beforeEach(() => {
// Create mock child process with stdout/stderr EventEmitters
const stdout = new EventEmitter();
const stderr = new EventEmitter();
mockChild = Object.assign(new EventEmitter(), { stdout, stderr });

// Set up spawn mock to return the mock child process
vi.mocked(spawn).mockReturnValue(mockChild as any);
vi.spyOn(deployClientModule, "getDeployClientPath").mockResolvedValue({
binary: "mock-binary",
buildId: "0.0.0",
});
vi.spyOn(deployClientModule, "cleanUp").mockImplementation(() => {});

// Mock process.exit to prevent test runner from exiting
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => undefined) as unknown as () => never);

// Provide deployment token via env to skip login flow
process.env.SWA_CLI_DEPLOYMENT_TOKEN = "test-token";

// Create required filesystem structure in memfs
const cwd = process.cwd();
vol.fromJSON({
[path.join("/test-output", "index.html")]: "hello",
[path.join(cwd, "placeholder")]: "",
});
});

it("should capture stderr and pass to logger.error", async () => {
await deploy({ outputLocation: "/test-output", dryRun: false });

mockChild.stderr.emit("data", Buffer.from("some error from binary"));

expect(logger.error).toHaveBeenCalledWith("some error from binary");
});

it("should fail spinner and log error on non-zero exit code", async () => {
await deploy({ outputLocation: "/test-output", dryRun: false });

mockChild.emit("close", 1);

expect(logger.error).toHaveBeenCalledWith("The deployment binary exited with code 1.");
});

it("should call process.exit(1) on non-zero exit code", async () => {
await deploy({ outputLocation: "/test-output", dryRun: false });

mockChild.emit("close", 127);

expect(exitSpy).toHaveBeenCalledWith(1);
});

it("should succeed without calling process.exit on exit code 0", async () => {
await deploy({ outputLocation: "/test-output", dryRun: false });

mockChild.emit("close", 0);

expect(exitSpy).not.toHaveBeenCalled();
});

it("should not call process.exit(1) on non-zero exit code in dry-run mode", async () => {
await deploy({ outputLocation: "/test-output", dryRun: true });

mockChild.emit("close", 1);

expect(logger.error).toHaveBeenCalledWith("The deployment binary exited with code 1.");
expect(exitSpy).not.toHaveBeenCalled();
});
});
});
15 changes: 15 additions & 0 deletions src/cli/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,13 @@ export async function deploy(options: SWACLIConfig) {
});
});

child.stderr!.on("data", (data) => {
const stderrOutput = data.toString().trim();
if (stderrOutput) {
logger.error(stderrOutput);
}
});

child.on("error", (error) => {
logger.error(error.toString());
});
Expand All @@ -325,6 +332,14 @@ export async function deploy(options: SWACLIConfig) {
if (code === 0) {
spinner.succeed(chalk.green(`Project deployed to ${chalk.underline(projectUrl)} 🚀`));
logger.log(``);
} else {
spinner.fail(chalk.red(`Deployment failed with exit code ${code}`));
logger.error(`The deployment binary exited with code ${code}.`);
logger.error(`If you are running in a minimal container image, ensure native dependencies are installed.`);
logger.error(`Run ${chalk.cyan(`ldd ${binary}`)} to check for missing shared libraries.`);
if (!dryRun) {
process.exit(1);
}
}
});
}
Expand Down
Loading