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
230 changes: 230 additions & 0 deletions packages/cli/src/commands/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { envCommand, SECRETS_BY_ALIAS } from "../env.js";
import { CloudflareClient } from "../../lib/cloudflare.js";
import { getServerPkgDir } from "../../lib/config.js";
import { select, isCancel, cancel } from "@clack/prompts";
import {
promptApiToken,
promptTrackerScriptName,
getScriptSnippet,
} from "../../lib/ui.js";

// Mock dependencies
vi.mock("../../lib/cloudflare.js", () => ({
CloudflareClient: vi.fn(),
}));

vi.mock("../../lib/config.js");
vi.mock("@clack/prompts");
vi.mock("../../lib/ui.js");
vi.mock("path");

describe("env.ts", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getServerPkgDir).mockReturnValue("/mock/server/dir");
});

describe("SECRETS_BY_ALIAS", () => {
it("should contain token secret configuration", () => {
const tokenSecret = SECRETS_BY_ALIAS.get("token");
expect(tokenSecret).toBeDefined();
expect(tokenSecret?.key).toBe("CF_BEARER_TOKEN");
expect(tokenSecret?.name).toBe("Cloudflare API Token");
expect(tokenSecret?.description).toBe(
"Token used to authenticate with Cloudflare API",
);
expect(tokenSecret?.prompt).toBe(promptApiToken);
});

it("should contain tracker-script secret configuration", () => {
const trackerSecret = SECRETS_BY_ALIAS.get("tracker-script");
expect(trackerSecret).toBeDefined();
expect(trackerSecret?.key).toBe("CF_TRACKER_SCRIPT_NAME");
expect(trackerSecret?.name).toBe("Tracker Script Name");
expect(trackerSecret?.description).toBe(
"Custom name for the tracker.js file served from the server",
);
expect(trackerSecret?.prompt).toBe(promptTrackerScriptName);
});
});

describe("envCommand", () => {
it("should update secret when valid secret key is provided", async () => {
vi.mocked(promptApiToken).mockResolvedValue("mock-api-token");
const mockSetSecrets = vi.fn().mockResolvedValue(true);

vi.mocked(CloudflareClient).mockImplementation(function () {
return {
setCloudflareSecrets: mockSetSecrets,
} as any;
});

await envCommand("token");

expect(mockSetSecrets).toHaveBeenCalledWith({
CF_BEARER_TOKEN: "mock-api-token",
});
});

it("should show script snippet when updating tracker script", async () => {
vi.mocked(promptTrackerScriptName).mockResolvedValue(
"custom-tracker",
);
vi.mocked(getScriptSnippet).mockReturnValue("mock-snippet");
const mockSetSecrets = vi.fn().mockResolvedValue(true);

vi.mocked(CloudflareClient).mockImplementation(function () {
return {
setCloudflareSecrets: mockSetSecrets,
} as any;
});

const consoleSpy = vi
.spyOn(console, "log")
.mockImplementation(() => {});

await envCommand("tracker-script");

expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Use this HTML snippet"),
);
expect(consoleSpy).toHaveBeenCalledWith("mock-snippet");

consoleSpy.mockRestore();
});

it("should exit with error when invalid secret key is provided", async () => {
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const processSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => {
throw new Error("process.exit");
});

await expect(envCommand("invalid-secret")).rejects.toThrow(
"process.exit",
);

expect(consoleSpy).toHaveBeenCalledWith(
"❌ Unknown secret: invalid-secret",
);
expect(processSpy).toHaveBeenCalledWith(1);

consoleSpy.mockRestore();
processSpy.mockRestore();
});

it("should prompt for secret selection when no secret key provided", async () => {
vi.mocked(select).mockResolvedValue("token");
vi.mocked(promptApiToken).mockResolvedValue("mock-api-token");
const mockSetSecrets = vi.fn().mockResolvedValue(true);

vi.mocked(CloudflareClient).mockImplementation(function () {
return {
setCloudflareSecrets: mockSetSecrets,
} as any;
});

await envCommand();

expect(select).toHaveBeenCalledWith({
message: "Which secret would you like to update?",
options: expect.arrayContaining([
expect.objectContaining({
value: "token",
label: "Cloudflare API Token (token)",
}),
expect.objectContaining({
value: "tracker-script",
label: "Tracker Script Name (tracker-script)",
}),
]),
});
});

it("should cancel operation when user cancels selection", async () => {
vi.mocked(select).mockResolvedValue(undefined);
vi.mocked(isCancel).mockReturnValue(true);
const processSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => {
throw new Error("process.exit");
});

await expect(envCommand()).rejects.toThrow("process.exit");

expect(cancel).toHaveBeenCalledWith("Operation canceled.");
expect(processSpy).toHaveBeenCalledWith(0);

processSpy.mockRestore();
});

it("should handle secret update failure", async () => {
vi.mocked(promptApiToken).mockResolvedValue("mock-api-token");
const mockSetSecrets = vi.fn().mockResolvedValue(false);

vi.mocked(CloudflareClient).mockImplementation(function () {
return {
setCloudflareSecrets: mockSetSecrets,
} as any;
});

const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const processSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => {
throw new Error("process.exit");
});

await expect(envCommand("token")).rejects.toThrow("process.exit");

expect(consoleSpy).toHaveBeenCalledWith(
"❌ Failed to update Cloudflare API Token",
);
expect(processSpy).toHaveBeenCalledWith(1);

consoleSpy.mockRestore();
processSpy.mockRestore();
});

it("should handle errors during secret update", async () => {
vi.mocked(promptApiToken).mockRejectedValue(
new Error("Prompt error"),
);
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const processSpy = vi
.spyOn(process, "exit")
.mockImplementation(() => {
throw new Error("process.exit");
});

await expect(envCommand("token")).rejects.toThrow("process.exit");

expect(consoleSpy).toHaveBeenCalledWith(
"❌ Error updating secret:",
expect.any(Error),
);
expect(processSpy).toHaveBeenCalledWith(1);

consoleSpy.mockRestore();
processSpy.mockRestore();
});

it("should throw error when secret configuration not found", async () => {
vi.mocked(select).mockResolvedValue("nonexistent");
vi.mocked(isCancel).mockReturnValue(false);
vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});

await expect(envCommand()).rejects.toThrow("process.exit");
});
});
});
113 changes: 0 additions & 113 deletions packages/cli/src/commands/__tests__/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import { isCancel } from "@clack/prompts";

// Import after mocks are set up
import {
promptApiToken,
promptDeploy,
promptProjectConfig,
promptAccountSelection,
Expand All @@ -65,118 +64,6 @@ describe("install prompts", () => {
mockExit.mockRestore();
});

describe("promptApiToken", () => {
let mockSpinner: {
start: ReturnType<typeof vi.fn>;
stop: ReturnType<typeof vi.fn>;
};

beforeEach(async () => {
mockSpinner = {
start: vi.fn(),
stop: vi.fn(),
};
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.spinner as unknown as ReturnType<typeof vi.fn>
).mockReturnValue(mockSpinner);
});

it("should return valid API token when validation passes", async () => {
const mockToken = "a".repeat(40);
(isCancel as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.password as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue(mockToken);

const mockCloudflare = await import("../../lib/cloudflare.js");
vi.mocked(
mockCloudflare.CloudflareClient.validateToken,
).mockResolvedValue({ valid: true });

const result = await promptApiToken();
expect(result).toBe(mockToken);
});

it("should throw error if user cancels", async () => {
(isCancel as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
true,
);
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.password as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue("token");

await expect(promptApiToken()).rejects.toThrow(
"Operation canceled",
);
});

it("should throw error if token validation fails", async () => {
const mockToken = "a".repeat(40);
(isCancel as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.password as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue(mockToken);

const mockCloudflare = await import("../../lib/cloudflare.js");
vi.mocked(
mockCloudflare.CloudflareClient.validateToken,
).mockResolvedValue({
valid: false,
error: "Invalid token or insufficient permissions",
});

await expect(promptApiToken()).rejects.toThrow(
"Invalid token or insufficient permissions",
);
});

it("should throw error if validation throws", async () => {
const mockToken = "a".repeat(40);
(isCancel as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.password as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue(mockToken);

const mockCloudflare = await import("../../lib/cloudflare.js");
vi.mocked(
mockCloudflare.CloudflareClient.validateToken,
).mockRejectedValue(new Error("Network error"));

await expect(promptApiToken()).rejects.toThrow("Network error");
});

it("should throw generic error if validation throws non-Error", async () => {
const mockToken = "a".repeat(40);
(isCancel as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
const mockPrompts = await import("@clack/prompts");
(
mockPrompts.password as unknown as ReturnType<typeof vi.fn>
).mockResolvedValue(mockToken);

const mockCloudflare = await import("../../lib/cloudflare.js");
vi.mocked(
mockCloudflare.CloudflareClient.validateToken,
).mockRejectedValue("string error");

await expect(promptApiToken()).rejects.toThrow(
"Failed to validate token. Please check your internet connection.",
);
});
});

describe("promptDeploy", () => {
it("should return true when user confirms", async () => {
const mockPrompts = await import("@clack/prompts");
Expand Down
Loading
Loading