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 resolveimport { 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 }), );});
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
}),
);
});