import { Command } from "commander"; import { ResourceInfo } from "../Types/CLITypes"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; // Mock the ApiClient module before it's imported by ResourceCommands const mockExecuteApiRequest: jest.Mock = jest.fn(); jest.mock("../Core/ApiClient", () => { return { ...jest.requireActual("../Core/ApiClient"), executeApiRequest: (...args: unknown[]): unknown => { return mockExecuteApiRequest(...args); }, }; }); // Import after mock setup import { discoverResources, registerResourceCommands, } from "../Commands/ResourceCommands"; const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime"); const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json"); describe("ResourceCommands", () => { let originalConfigContent: string | null = null; beforeAll(() => { if (fs.existsSync(CONFIG_FILE)) { originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8"); } }); afterAll(() => { if (originalConfigContent) { fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 }); } else if (fs.existsSync(CONFIG_FILE)) { fs.unlinkSync(CONFIG_FILE); } }); beforeEach(() => { if (fs.existsSync(CONFIG_FILE)) { fs.unlinkSync(CONFIG_FILE); } jest.spyOn(console, "log").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {}); jest.spyOn(process, "exit").mockImplementation((() => {}) as any); mockExecuteApiRequest.mockReset(); delete process.env["ONEUPTIME_API_KEY"]; delete process.env["ONEUPTIME_URL"]; }); afterEach(() => { jest.restoreAllMocks(); delete process.env["ONEUPTIME_API_KEY"]; delete process.env["ONEUPTIME_URL"]; }); describe("discoverResources", () => { let resources: ResourceInfo[]; beforeAll(() => { resources = discoverResources(); }); it("should discover at least one resource", () => { expect(resources.length).toBeGreaterThan(0); }); it("should discover the Incident resource", () => { const incident: ResourceInfo | undefined = resources.find( (r: ResourceInfo) => { return r.singularName === "Incident"; }, ); expect(incident).toBeDefined(); expect(incident!.modelType).toBe("database"); expect(incident!.apiPath).toBe("/incident"); }); it("should discover the Monitor resource", () => { const monitor: ResourceInfo | undefined = resources.find( (r: ResourceInfo) => { return r.singularName === "Monitor"; }, ); expect(monitor).toBeDefined(); expect(monitor!.modelType).toBe("database"); }); it("should discover the Alert resource", () => { const alert: ResourceInfo | undefined = resources.find( (r: ResourceInfo) => { return r.singularName === "Alert"; }, ); expect(alert).toBeDefined(); }); it("should have kebab-case names for all resources", () => { for (const r of resources) { expect(r.name).toMatch(/^[a-z][a-z0-9-]*$/); } }); it("should have apiPath for all resources", () => { for (const r of resources) { expect(r.apiPath).toBeTruthy(); expect(r.apiPath.startsWith("/")).toBe(true); } }); it("should have valid modelType for all resources", () => { for (const r of resources) { expect(["database", "analytics"]).toContain(r.modelType); } }); }); describe("registerResourceCommands", () => { it("should register commands for all discovered resources", () => { const program: Command = new Command(); program.exitOverride(); registerResourceCommands(program); const resources: ResourceInfo[] = discoverResources(); for (const resource of resources) { const cmd: Command | undefined = program.commands.find((c: Command) => { return c.name() === resource.name; }); expect(cmd).toBeDefined(); } }); it("should register list, get, create, update, delete, count subcommands for database resources", () => { const program: Command = new Command(); program.exitOverride(); registerResourceCommands(program); const incidentCmd: Command | undefined = program.commands.find( (c: Command) => { return c.name() === "incident"; }, ); expect(incidentCmd).toBeDefined(); const subcommands: string[] = incidentCmd!.commands.map((c: Command) => { return c.name(); }); expect(subcommands).toContain("list"); expect(subcommands).toContain("get"); expect(subcommands).toContain("create"); expect(subcommands).toContain("update"); expect(subcommands).toContain("delete"); expect(subcommands).toContain("count"); }); }); describe("resource command actions", () => { function createProgramWithResources(): Command { const program: Command = new Command(); program.exitOverride(); program.configureOutput({ writeOut: () => {}, writeErr: () => {}, }); program .option("--api-key ", "API key") .option("--url ", "URL") .option("--context ", "Context"); registerResourceCommands(program); return program; } beforeEach(() => { /* * Use env vars for credentials instead of config file to avoid * race conditions with other test files that share ~/.oneuptime/config.json */ process.env["ONEUPTIME_API_KEY"] = "test-key-12345"; process.env["ONEUPTIME_URL"] = "https://test.oneuptime.com"; mockExecuteApiRequest.mockResolvedValue({ data: [] }); }); describe("list subcommand", () => { it("should call API with list operation", async () => { const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "list"]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("list"); expect(mockExecuteApiRequest.mock.calls[0][0].apiPath).toBe( "/incident", ); }); it("should pass query, limit, skip, sort options", async () => { const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "list", "--query", '{"status":"active"}', "--limit", "20", "--skip", "5", "--sort", '{"createdAt":-1}', ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); const opts: Record = mockExecuteApiRequest.mock.calls[0][0]; expect(opts.query).toEqual({ status: "active" }); expect(opts.limit).toBe(20); expect(opts.skip).toBe(5); expect(opts.sort).toEqual({ createdAt: -1 }); }); it("should extract data array from response object", async () => { mockExecuteApiRequest.mockResolvedValue({ data: [{ _id: "1", name: "Test" }], }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "list", "-o", "json", ]); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalled(); }); it("should handle response that is already an array", async () => { mockExecuteApiRequest.mockResolvedValue([{ _id: "1" }]); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "list", "-o", "json", ]); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalled(); }); it("should handle API errors", async () => { mockExecuteApiRequest.mockRejectedValue( new Error("API error (500): Server Error"), ); const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "list"]); expect(process.exit).toHaveBeenCalled(); }); }); describe("get subcommand", () => { it("should call API with read operation and id", async () => { mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123", name: "Test", }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "get", "abc-123", ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); const opts: Record = mockExecuteApiRequest.mock.calls[0][0]; expect(opts.operation).toBe("read"); expect(opts.id).toBe("abc-123"); }); it("should support output format flag", async () => { mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "get", "abc-123", "-o", "json", ]); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalled(); }); it("should handle get errors", async () => { mockExecuteApiRequest.mockRejectedValue(new Error("not found 404")); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "get", "abc-123", ]); expect(process.exit).toHaveBeenCalled(); }); }); describe("create subcommand", () => { it("should call API with create operation and data", async () => { mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "create", "--data", '{"name":"New Incident"}', ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); const opts: Record = mockExecuteApiRequest.mock.calls[0][0]; expect(opts.operation).toBe("create"); expect(opts.data).toEqual({ name: "New Incident" }); }); it("should support reading data from a file", async () => { mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" }); const tmpFile: string = path.join( os.tmpdir(), "cli-test-" + Date.now() + ".json", ); fs.writeFileSync(tmpFile, '{"name":"From File"}'); try { const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "create", "--file", tmpFile, ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); expect(mockExecuteApiRequest.mock.calls[0][0].data).toEqual({ name: "From File", }); } finally { fs.unlinkSync(tmpFile); } }); it("should error when neither --data nor --file is provided", async () => { const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "create"]); expect(process.exit).toHaveBeenCalled(); }); it("should error on invalid JSON in --data", async () => { const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "create", "--data", "not-json", ]); expect(process.exit).toHaveBeenCalled(); }); }); describe("update subcommand", () => { it("should call API with update operation, id, and data", async () => { mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "update", "abc-123", "--data", '{"name":"Updated"}', ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); const opts: Record = mockExecuteApiRequest.mock.calls[0][0]; expect(opts.operation).toBe("update"); expect(opts.id).toBe("abc-123"); expect(opts.data).toEqual({ name: "Updated" }); }); it("should handle update errors", async () => { mockExecuteApiRequest.mockRejectedValue(new Error("API error")); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "update", "abc-123", "--data", '{"name":"x"}', ]); expect(process.exit).toHaveBeenCalled(); }); }); describe("delete subcommand", () => { it("should call API with delete operation and id", async () => { mockExecuteApiRequest.mockResolvedValue({}); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "delete", "abc-123", ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); const opts: Record = mockExecuteApiRequest.mock.calls[0][0]; expect(opts.operation).toBe("delete"); expect(opts.id).toBe("abc-123"); }); it("should handle API errors", async () => { mockExecuteApiRequest.mockRejectedValue(new Error("not found 404")); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "delete", "abc-123", ]); expect(process.exit).toHaveBeenCalled(); }); }); describe("count subcommand", () => { it("should call API with count operation", async () => { mockExecuteApiRequest.mockResolvedValue({ count: 42 }); const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "count"]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("count"); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalledWith(42); }); it("should pass query filter", async () => { mockExecuteApiRequest.mockResolvedValue({ count: 5 }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "incident", "count", "--query", '{"status":"active"}', ]); expect(mockExecuteApiRequest.mock.calls[0][0].query).toEqual({ status: "active", }); }); it("should handle response without count field", async () => { mockExecuteApiRequest.mockResolvedValue(99); const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "count"]); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalledWith(99); }); it("should handle non-object response in count", async () => { mockExecuteApiRequest.mockResolvedValue("some-string"); const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "count"]); // eslint-disable-next-line no-console expect(console.log).toHaveBeenCalledWith("some-string"); }); it("should handle count errors", async () => { mockExecuteApiRequest.mockRejectedValue(new Error("API error")); const program: Command = createProgramWithResources(); await program.parseAsync(["node", "test", "incident", "count"]); expect(process.exit).toHaveBeenCalled(); }); }); describe("credential resolution in commands", () => { it("should use global --api-key and --url flags", async () => { delete process.env["ONEUPTIME_API_KEY"]; delete process.env["ONEUPTIME_URL"]; mockExecuteApiRequest.mockResolvedValue({ data: [] }); const program: Command = createProgramWithResources(); await program.parseAsync([ "node", "test", "--api-key", "global-key", "--url", "https://global.com", "incident", "list", ]); expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1); expect(mockExecuteApiRequest.mock.calls[0][0].apiKey).toBe( "global-key", ); expect(mockExecuteApiRequest.mock.calls[0][0].apiUrl).toBe( "https://global.com", ); }); }); }); });