import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("../lib/config.js", () => ({ getConfig: vi.fn(() => ({ apiUrl: "https://api.skills-hub.ai", accessToken: "tok", })), getAuthHeader: vi.fn(() => ({ Authorization: "Bearer tok" })), saveConfig: vi.fn(), ensureAuth: vi.fn(), })); vi.mock("../lib/api-client.js", () => ({ apiRequest: vi.fn(), })); import { orgTokensCommand } from "./org-tokens.js"; import { apiRequest } from "../lib/api-client.js"; import { ensureAuth } from "../lib/config.js"; const mockApiRequest = vi.mocked(apiRequest); const mockEnsureAuth = vi.mocked(ensureAuth); const mockTokenRow = { id: "tok-1", name: "ci", keyPrefix: "sho_abc1", scopes: ["registry:read", "audit:read"], createdById: "user-1", lastUsedAt: null, expiresAt: null, createdAt: "2026-07-01T00:00:00Z", }; beforeEach(() => { vi.clearAllMocks(); }); describe("org tokens list", () => { it("requires authentication", async () => { mockApiRequest.mockResolvedValue([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync(["node", "tokens", "list", "acme"], { from: "node", }); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("lists tokens for the org", async () => { mockApiRequest.mockResolvedValue([mockTokenRow]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync(["node", "tokens", "list", "acme"], { from: "node", }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/tokens"); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("ci"); expect(output).toContain("sho_abc1"); expect(output).toContain("registry:read, audit:read"); log.mockRestore(); }); it("shows message when there are no tokens", async () => { mockApiRequest.mockResolvedValue([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync(["node", "tokens", "list", "acme"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("No org tokens"); log.mockRestore(); }); it("URL-encodes the org slug", async () => { mockApiRequest.mockResolvedValue([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync(["node", "tokens", "list", "my org"], { from: "node", }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/my%20org/tokens"); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Forbidden")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgTokensCommand.parseAsync(["node", "tokens", "list", "acme"], { from: "node", }), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org tokens create", () => { const createdToken = { ...mockTokenRow, token: "sho_abc1_onetimesecretvalue", }; it("requires authentication", async () => { mockApiRequest.mockResolvedValue(createdToken); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( [ "node", "tokens", "create", "acme", "--name", "ci", "--scope", "registry:read", ], { from: "node" }, ); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("POSTs name and scopes, prints the one-time secret and a store-it-now warning", async () => { mockApiRequest.mockResolvedValue(createdToken); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( [ "node", "tokens", "create", "acme", "--name", "ci", "--scope", "registry:read", "audit:read", ], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/acme/tokens", expect.objectContaining({ method: "POST" }), ); const [, options] = mockApiRequest.mock.calls[0] as [ string, { body: string }, ]; const body = JSON.parse(options.body) as Record; expect(body).toEqual({ name: "ci", scopes: ["registry:read", "audit:read"], }); expect(body.expiresInDays).toBeUndefined(); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("sho_abc1_onetimesecretvalue"); expect(output).toContain("shown ONCE"); expect(output).toContain("store it in your secrets manager now"); log.mockRestore(); }); it("sends expiresInDays as a number when --expires-in-days is given", async () => { mockApiRequest.mockResolvedValue(createdToken); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( [ "node", "tokens", "create", "acme", "--name", "ci", "--scope", "registry:read", "--expires-in-days", "30", ], { from: "node" }, ); const [, options] = mockApiRequest.mock.calls[0] as [ string, { body: string }, ]; const body = JSON.parse(options.body) as Record; expect(body.expiresInDays).toBe(30); expect(typeof body.expiresInDays).toBe("number"); log.mockRestore(); }); it("URL-encodes the org slug", async () => { mockApiRequest.mockResolvedValue(createdToken); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( [ "node", "tokens", "create", "my org", "--name", "ci", "--scope", "registry:read", ], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/my%20org/tokens", expect.objectContaining({ method: "POST" }), ); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Admin role required")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgTokensCommand.parseAsync( [ "node", "tokens", "create", "acme", "--name", "ci", "--scope", "registry:read", ], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org tokens revoke", () => { it("requires authentication", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( ["node", "tokens", "revoke", "acme", "tok-1"], { from: "node" }, ); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("DELETEs the token and confirms", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( ["node", "tokens", "revoke", "acme", "tok-1"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/acme/tokens/tok-1", { method: "DELETE" }, ); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("Token revoked"); log.mockRestore(); }); it("URL-encodes the org slug and token id", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgTokensCommand.parseAsync( ["node", "tokens", "revoke", "my org", "tok 1"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/my%20org/tokens/tok%201", { method: "DELETE" }, ); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Token not found")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgTokensCommand.parseAsync( ["node", "tokens", "revoke", "acme", "tok-1"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); });