import { beforeEach, describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; // oxlint-disable-next-line executor/no-vitest-import -- boundary: vi.mock/vi.hoisted must come from vitest itself for mock hoisting to resolve import { vi } from "vitest"; import { OnePasswordError } from "./errors"; import { makeOnePasswordService } from "./service"; const opMocks = vi.hoisted(() => ({ setGlobalFlags: vi.fn(), setServiceAccount: vi.fn(), vaultList: vi.fn(), itemList: vi.fn(), readParse: vi.fn(), })); const sdkMocks = vi.hoisted(() => ({ createClient: vi.fn(), DesktopAuth: vi.fn((accountName: string) => ({ accountName })), exports: {} as { createClient?: unknown; DesktopAuth?: unknown; }, })); vi.mock("@1password/op-js", () => ({ setGlobalFlags: opMocks.setGlobalFlags, setServiceAccount: opMocks.setServiceAccount, vault: { list: opMocks.vaultList }, item: { list: opMocks.itemList }, read: { parse: opMocks.readParse }, })); vi.mock("@1password/sdk", () => sdkMocks.exports); describe("makeOnePasswordService", () => { beforeEach(() => { vi.clearAllMocks(); opMocks.vaultList.mockReturnValue([]); opMocks.itemList.mockReturnValue([]); opMocks.readParse.mockReturnValue("secret"); sdkMocks.exports.createClient = sdkMocks.createClient; sdkMocks.exports.DesktopAuth = sdkMocks.DesktopAuth; sdkMocks.createClient.mockResolvedValue({ secrets: { resolve: vi.fn(async () => "secret") }, vaults: { list: vi.fn(async () => []) }, items: { list: vi.fn(async () => []) }, }); }); it.effect("falls back to the SDK when the CLI throws while listing vaults", () => Effect.gen(function* () { const sdkVaultsList = vi.fn(async () => [{ id: "sdk-vault", title: "SDK Vault" }]); opMocks.vaultList.mockImplementation(() => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: simulates the untyped op-js CLI wrapper throwing throw new Error("spawn op ENOENT"); }); sdkMocks.createClient.mockResolvedValue({ secrets: { resolve: vi.fn(async () => "secret") }, vaults: { list: sdkVaultsList }, items: { list: vi.fn(async () => []) }, }); const service = yield* makeOnePasswordService( { kind: "service-account", token: "ops_test_token" }, { timeoutMs: 1_000 }, ); const vaults = yield* service.listVaults(); expect(vaults).toEqual([{ id: "sdk-vault", title: "SDK Vault" }]); expect(sdkMocks.createClient).toHaveBeenCalledTimes(1); expect(sdkVaultsList).toHaveBeenCalledTimes(1); }), ); it.effect("includes the backend cause when both vault listing backends fail", () => Effect.gen(function* () { opMocks.vaultList.mockImplementation(() => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: simulates the untyped op-js CLI wrapper throwing throw new Error("spawn op ENOENT"); }); sdkMocks.createClient.mockResolvedValue({ secrets: { resolve: vi.fn(async () => "secret") }, vaults: { list: vi.fn(async () => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: simulates the untyped 1Password SDK rejecting throw new Error("desktop approval refused for account"); }), }, items: { list: vi.fn(async () => []) }, }); const error = yield* makeOnePasswordService( { kind: "service-account", token: "ops_test_token" }, { timeoutMs: 1_000 }, ).pipe( Effect.flatMap((service) => service.listVaults()), Effect.flip, ); expect(error).toBeInstanceOf(OnePasswordError); // oxlint-disable executor/no-unknown-error-message -- boundary: OnePasswordError carries a typed message; asserting its contents expect(error.message).toContain("1Password SDK vault listing failed:"); expect(error.message).toContain("desktop approval refused for account"); expect(error.message).not.toBe("1Password CLI vault listing failed"); // oxlint-enable executor/no-unknown-error-message }), ); it.effect("reports a clear SDK load error when the compiled namespace is empty", () => Effect.gen(function* () { opMocks.vaultList.mockImplementation(() => { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: simulates the untyped op-js CLI wrapper throwing throw new Error("spawn op ENOENT"); }); sdkMocks.exports.createClient = undefined; sdkMocks.exports.DesktopAuth = undefined; const error = yield* makeOnePasswordService( { kind: "service-account", token: "ops_test_token" }, { timeoutMs: 1_000 }, ).pipe( Effect.flatMap((service) => service.listVaults()), Effect.flip, ); expect(error).toBeInstanceOf(OnePasswordError); expect(error.operation).toBe("sdk module load"); // oxlint-disable executor/no-unknown-error-message -- boundary: OnePasswordError carries a typed message; asserting its contents expect(error.message).toContain("did not expose createClient and DesktopAuth"); expect(error.message).toContain("/opt/homebrew/bin"); // oxlint-enable executor/no-unknown-error-message }), ); });