| 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 3 | |
| 4 | import { |
| 5 | browserProfileRemovalAvailable, |
| 6 | clearBrowserProfileData, |
| 7 | importFailureReason, |
| 8 | } from "./IntegrationsSettings"; |
| 9 | |
| 10 | const environmentId = "environment-a" as EnvironmentId; |
| 11 | const secondEnvironmentId = "environment-b" as EnvironmentId; |
| 12 | |
| 13 | describe("clearBrowserProfileData", () => { |
| 14 | it("waits for cookie and cache cleanup", async () => { |
| 15 | const clearCookies = vi.fn().mockResolvedValue(undefined); |
| 16 | const clearCache = vi.fn().mockResolvedValue(undefined); |
| 17 | |
| 18 | await clearBrowserProfileData({ clearCookies, clearCache }, [environmentId], "profile-a"); |
| 19 | |
| 20 | expect(clearCookies).toHaveBeenCalledWith(environmentId, "profile-a"); |
| 21 | expect(clearCache).toHaveBeenCalledWith(environmentId, "profile-a"); |
| 22 | }); |
| 23 | |
| 24 | it("clears every known environment before succeeding", async () => { |
| 25 | const clearCookies = vi.fn().mockResolvedValue(undefined); |
| 26 | const clearCache = vi.fn().mockResolvedValue(undefined); |
| 27 | |
| 28 | await clearBrowserProfileData( |
| 29 | { clearCookies, clearCache }, |
| 30 | [environmentId, secondEnvironmentId], |
| 31 | "profile-a", |
| 32 | ); |
| 33 | |
| 34 | expect(clearCookies.mock.calls).toEqual([ |
| 35 | [environmentId, "profile-a"], |
| 36 | [secondEnvironmentId, "profile-a"], |
| 37 | ]); |
| 38 | expect(clearCache.mock.calls).toEqual([ |
| 39 | [environmentId, "profile-a"], |
| 40 | [secondEnvironmentId, "profile-a"], |
| 41 | ]); |
| 42 | }); |
| 43 | |
| 44 | it("propagates cleanup failures", async () => { |
| 45 | const failure = new Error("clear failed"); |
| 46 | await expect( |
| 47 | clearBrowserProfileData( |
| 48 | { |
| 49 | clearCookies: vi.fn().mockRejectedValue(failure), |
| 50 | clearCache: vi.fn().mockResolvedValue(undefined), |
| 51 | }, |
| 52 | [environmentId], |
| 53 | "profile-a", |
| 54 | ), |
| 55 | ).rejects.toBe(failure); |
| 56 | }); |
| 57 | |
| 58 | it("does not report success without an environment or bridge", async () => { |
| 59 | const bridge = { |
| 60 | clearCookies: vi.fn().mockResolvedValue(undefined), |
| 61 | clearCache: vi.fn().mockResolvedValue(undefined), |
| 62 | }; |
| 63 | |
| 64 | await expect(clearBrowserProfileData(bridge, [], "profile-a")).rejects.toThrow(); |
| 65 | await expect(clearBrowserProfileData(null, [environmentId], "profile-a")).rejects.toThrow(); |
| 66 | expect(bridge.clearCookies).not.toHaveBeenCalled(); |
| 67 | expect(bridge.clearCache).not.toHaveBeenCalled(); |
| 68 | }); |
| 69 | }); |
| 70 | |
| 71 | describe("clearBrowserProfileData with server browsers", () => { |
| 72 | it("clears server-hosted profiles without a desktop bridge", async () => { |
| 73 | const clear = vi.fn().mockResolvedValue(undefined); |
| 74 | await clearBrowserProfileData(null, [environmentId, secondEnvironmentId], "profile-a", { |
| 75 | environmentIds: [secondEnvironmentId], |
| 76 | clear, |
| 77 | }); |
| 78 | expect(clear).toHaveBeenCalledExactlyOnceWith(secondEnvironmentId, "profile-a"); |
| 79 | }); |
| 80 | |
| 81 | it("clears both copies and fails when the server copy fails", async () => { |
| 82 | const bridge = { |
| 83 | clearCookies: vi.fn().mockResolvedValue(undefined), |
| 84 | clearCache: vi.fn().mockResolvedValue(undefined), |
| 85 | }; |
| 86 | const failure = new Error("server failed"); |
| 87 | await expect( |
| 88 | clearBrowserProfileData(bridge, [environmentId], "profile-a", { |
| 89 | environmentIds: [environmentId], |
| 90 | clear: vi.fn().mockRejectedValue(failure), |
| 91 | }), |
| 92 | ).rejects.toBe(failure); |
| 93 | expect(bridge.clearCookies).toHaveBeenCalledWith(environmentId, "profile-a"); |
| 94 | }); |
| 95 | }); |
| 96 | |
| 97 | describe("browserProfileRemovalAvailable", () => { |
| 98 | it("accepts a server browser in place of the desktop bridge", () => { |
| 99 | expect(browserProfileRemovalAvailable(false, true, 1, 1)).toBe(true); |
| 100 | }); |
| 101 | |
| 102 | it("requires a ready non-empty catalog and desktop bridge", () => { |
| 103 | expect(browserProfileRemovalAvailable(true, true, 1)).toBe(true); |
| 104 | expect(browserProfileRemovalAvailable(true, true, 0)).toBe(false); |
| 105 | expect(browserProfileRemovalAvailable(true, false, 1)).toBe(false); |
| 106 | expect(browserProfileRemovalAvailable(false, true, 1)).toBe(false); |
| 107 | }); |
| 108 | }); |
| 109 | |
| 110 | // Mirrors `BrowserImportFailedError.message`, which IPC flattens to a string |
| 111 | // before the renderer sees it. |
| 112 | const failure = (reason: string) => ({ |
| 113 | message: `Importing cookies from safari failed: ${reason}.`, |
| 114 | }); |
| 115 | |
| 116 | describe("importFailureReason", () => { |
| 117 | it("recovers the reason token from the flattened message", () => { |
| 118 | expect(importFailureReason(failure("needsFullDiskAccess"))).toBe("needsFullDiskAccess"); |
| 119 | expect(importFailureReason(failure("browserRunning"))).toBe("browserRunning"); |
| 120 | expect(importFailureReason(failure("readFailed"))).toBe("readFailed"); |
| 121 | expect(importFailureReason(failure("keychainUnavailable"))).toBe("keychainUnavailable"); |
| 122 | // A settings write that fails after the cookies landed is its own case, |
| 123 | // not a read failure over a database that was in fact read. |
| 124 | expect(importFailureReason(failure("profileNotSaved"))).toBe("profileNotSaved"); |
| 125 | expect(importFailureReason(failure("profileLimitReached"))).toBe("profileLimitReached"); |
| 126 | }); |
| 127 | |
| 128 | it("falls back to readFailed for anything it cannot classify", () => { |
| 129 | expect(importFailureReason(new Error("something else entirely"))).toBe("readFailed"); |
| 130 | expect(importFailureReason(undefined)).toBe("readFailed"); |
| 131 | }); |
| 132 | }); |