| ... | @@ -7,6 +7,7 @@ import * as Sink from "effect/Sink"; | ... | @@ -7,6 +7,7 @@ import * as Sink from "effect/Sink"; |
| 7 | import * as Stream from "effect/Stream"; | 7 | import * as Stream from "effect/Stream"; |
| 8 | import * as PlatformError from "effect/PlatformError"; | 8 | import * as PlatformError from "effect/PlatformError"; |
| 9 | import { ChildProcessSpawner } from "effect/unstable/process"; | 9 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| | 10 | import { beforeEach, vi } from "vite-plus/test"; |
| 10 | | 11 | |
| 11 | import { | 12 | import { |
| 12 | ChromiumKeyError, | 13 | ChromiumKeyError, |
| ... | @@ -17,6 +18,18 @@ import { | ... | @@ -17,6 +18,18 @@ import { |
| 17 | } from "./ChromiumKeys.ts"; | 18 | } from "./ChromiumKeys.ts"; |
| 18 | import { LinuxBrowserSecretPath } from "./LinuxBrowserSecret.ts"; | 19 | import { LinuxBrowserSecretPath } from "./LinuxBrowserSecret.ts"; |
| 19 | | 20 | |
| | 21 | const { getPassword } = vi.hoisted(() => ({ getPassword: vi.fn<() => string | null>() })); |
| | 22 | |
| | 23 | vi.mock("@napi-rs/keyring", () => ({ |
| | 24 | Entry: class { |
| | 25 | getPassword = getPassword; |
| | 26 | }, |
| | 27 | })); |
| | 28 | |
| | 29 | beforeEach(() => { |
| | 30 | getPassword.mockReset(); |
| | 31 | }); |
| | 32 | |
| 20 | type CapturedCommand = { | 33 | type CapturedCommand = { |
| 21 | readonly command: string; | 34 | readonly command: string; |
| 22 | readonly args: ReadonlyArray<string>; | 35 | readonly args: ReadonlyArray<string>; |
| ... | @@ -63,6 +76,47 @@ const helperLayer = (input: { | ... | @@ -63,6 +76,47 @@ const helperLayer = (input: { |
| 63 | ), | 76 | ), |
| 64 | ); | 77 | ); |
| 65 | | 78 | |
| | 79 | describe("macOS Chromium secrets", () => { |
| | 80 | const request = { |
| | 81 | platform: "darwin", |
| | 82 | keychainService: "Chrome Safe Storage", |
| | 83 | keychainAccount: "Chrome", |
| | 84 | linuxSecretApplication: undefined, |
| | 85 | } as const; |
| | 86 | const noProcesses = Layer.succeed( |
| | 87 | ChildProcessSpawner.ChildProcessSpawner, |
| | 88 | ChildProcessSpawner.make(() => Effect.die("must not spawn")), |
| | 89 | ); |
| | 90 | |
| | 91 | it.effect("derives the cookie key from the keychain secret", () => |
| | 92 | Effect.gen(function* () { |
| | 93 | getPassword.mockReturnValue("macos-secret"); |
| | 94 | const keys = yield* resolveChromiumKeys(request); |
| | 95 | expect(keys.cbcV10?.toString("hex")).toBe("3df7306fb1eac353289565a2f6b64f74"); |
| | 96 | }).pipe(Effect.provide(noProcesses)), |
| | 97 | ); |
| | 98 | |
| | 99 | it.effect("reports a missing keychain entry", () => |
| | 100 | Effect.gen(function* () { |
| | 101 | getPassword.mockReturnValue(null); |
| | 102 | const error = yield* resolveChromiumKeys(request).pipe(Effect.flip); |
| | 103 | expect(error.reason).toBe("keychainItemMissing"); |
| | 104 | }).pipe(Effect.provide(noProcesses)), |
| | 105 | ); |
| | 106 | |
| | 107 | it.effect("preserves a denied keychain approval", () => |
| | 108 | Effect.gen(function* () { |
| | 109 | const denied = new Error("User denied access"); |
| | 110 | getPassword.mockImplementation(() => { |
| | 111 | throw denied; |
| | 112 | }); |
| | 113 | const error = yield* resolveChromiumKeys(request).pipe(Effect.flip); |
| | 114 | expect(error.reason).toBe("needsKeychainApproval"); |
| | 115 | expect(error.cause).toBe(denied); |
| | 116 | }).pipe(Effect.provide(noProcesses)), |
| | 117 | ); |
| | 118 | }); |
| | 119 | |
| 66 | describe("Linux Chromium secrets", () => { | 120 | describe("Linux Chromium secrets", () => { |
| 67 | it.effect("retains a missing helper failure alongside the keyring-free fallback", () => | 121 | it.effect("retains a missing helper failure alongside the keyring-free fallback", () => |
| 68 | Effect.gen(function* () { | 122 | Effect.gen(function* () { |