297 lines · 12.2 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as PlatformError from "effect/PlatformError";import { beforeEach, vi } from "vite-plus/test"; const { fromPartition, sessions } = vi.hoisted(() => ({  fromPartition: vi.fn(),  sessions: new Map<    string,    {      readonly clearCache: ReturnType<typeof vi.fn>;      readonly clearStorageData: ReturnType<typeof vi.fn>;      readonly getUserAgent: ReturnType<typeof vi.fn>;      readonly setPermissionRequestHandler: ReturnType<typeof vi.fn>;      readonly setPermissionCheckHandler: ReturnType<typeof vi.fn>;      readonly setUserAgent: ReturnType<typeof vi.fn>;    }  >(),})); vi.mock("electron", () => ({  session: {    fromPartition,  },})); import * as BrowserSession from "./BrowserSession.ts"; const layer = BrowserSession.layer.pipe(Layer.provide(NodeServices.layer)); describe("BrowserSession", () => {  beforeEach(() => {    sessions.clear();    fromPartition.mockReset();    fromPartition.mockImplementation((partition: string) => {      const browserSession = {        clearCache: vi.fn(() => Promise.resolve()),        clearStorageData: vi.fn(() => Promise.resolve()),        getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/0.0.27"),        setPermissionRequestHandler: vi.fn(),        setPermissionCheckHandler: vi.fn(),        setUserAgent: vi.fn(),      };      sessions.set(partition, browserSession);      return browserSession;    });  });   it.effect("derives deterministic partitions and memoizes sessions", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;       const partition = yield* browserSessions.getPartition("scope-a");      const first = yield* browserSessions.getSession("scope-a");      const second = yield* browserSessions.getSession("scope-a");       assert.strictEqual(partition, "persist:t3code-preview-f051bb2c68cb7b2fe969");      assert.strictEqual(first, second);      assert.strictEqual(fromPartition.mock.calls.length, 1);    }).pipe(Effect.provide(layer)),  );   it.effect("keeps scopes that differ only by a lone surrogate in separate partitions", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;       // TextEncoder folds a lone surrogate to U+FFFD, so without escaping these      // two supported ids would hash to one partition and share every cookie.      const loneSurrogate = yield* browserSessions.getPartition("p\ud800");      const replacementChar = yield* browserSessions.getPartition("p\ufffd");      assert.notStrictEqual(loneSurrogate, replacementChar);       // The escape can't be forged with a literal backslash either.      const literal = yield* browserSessions.getPartition("p\\ud800");      assert.notStrictEqual(literal, loneSurrogate);       // And a well-formed scope still lands on its historical partition.      assert.strictEqual(        yield* browserSessions.getPartition("scope-a"),        "persist:t3code-preview-f051bb2c68cb7b2fe969",      );    }).pipe(Effect.provide(layer)),  );   it.effect("keeps legacy defaults disjoint from nondefault profile partitions", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;       // These share the same scope string: default environment `a::b`, and      // environment `a` with nondefault profile `b`.      const legacyDefault = yield* browserSessions.getPartition("a::b");      const nondefaultProfile = yield* browserSessions.getPartition("a::b", true, "profile");       assert.strictEqual(legacyDefault, "persist:t3code-preview-78f0be89237d77f7a70e");      assert.strictEqual(nondefaultProfile, "persist:t3code-preview-profile-78f0be89237d77f7a70e");      assert.notStrictEqual(nondefaultProfile, legacyDefault);      assert.isTrue(browserSessions.isPartition(legacyDefault));      assert.isTrue(browserSessions.isPartition(nondefaultProfile));    }).pipe(Effect.provide(layer)),  );   it.effect("grants clipboard-sanitized-write through both the request and check handlers", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;      const partition = yield* browserSessions.getPartition("scope-a");      yield* browserSessions.getSession("scope-a");       const browserSession = sessions.get(partition);      assert.isDefined(browserSession);       const requestHandler = browserSession.setPermissionRequestHandler.mock.calls[0]?.[0];      const checkHandler = browserSession.setPermissionCheckHandler.mock.calls[0]?.[0];      assert.isFunction(requestHandler);      assert.isFunction(checkHandler);       const requestAllows = (permission: string): boolean => {        let granted: boolean | undefined;        requestHandler(null, permission, (value: boolean) => {          granted = value;        });        assert.isDefined(granted);        return granted;      };       for (const permission of [        "clipboard-read",        "clipboard-sanitized-write",        "notifications",        "geolocation",      ]) {        assert.isTrue(requestAllows(permission), `request handler should allow ${permission}`);        assert.isTrue(          checkHandler(null, permission) as boolean,          `check handler should allow ${permission}`,        );      }       // `clipboard-write` is not a real Electron permission — the async write API      // uses `clipboard-sanitized-write` — so the stale name must not be granted,      // and unrelated permissions stay denied.      for (const permission of ["clipboard-write", "midi"]) {        assert.isFalse(requestAllows(permission), `request handler should deny ${permission}`);        assert.isFalse(          checkHandler(null, permission) as boolean,          `check handler should deny ${permission}`,        );      }    }).pipe(Effect.provide(layer)),  );   it.effect("preserves partition scope and the platform failure chain", () => {    const nativeCause = new Error("native digest failed");    const platformCause = PlatformError.systemError({      _tag: "Unknown",      module: "Crypto",      method: "digest",      cause: nativeCause,    });    const failingCryptoLayer = Layer.succeed(      Crypto.Crypto,      Crypto.make({        randomBytes: (size) => new Uint8Array(size),        digest: () => Effect.fail(platformCause),      }),    );     return Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;      const error = yield* browserSessions.getPartition("environment-a").pipe(Effect.flip);       assert.instanceOf(error, BrowserSession.BrowserSessionPartitionDerivationError);      assert.equal(error.scope, "environment-a");      assert.strictEqual(error.cause, platformCause);      assert.strictEqual(error.cause.reason.cause, nativeCause);      assert.equal(        error.message,        "Failed to derive a desktop preview browser partition for scope environment-a.",      );      assert.notInclude(error.message, nativeCause.message);    }).pipe(Effect.provide(BrowserSession.layer.pipe(Layer.provide(failingCryptoLayer))));  });   it.effect("preserves session scope, partition, and the Electron failure", () =>    Effect.gen(function* () {      const cause = new Error("Electron session failed");      fromPartition.mockImplementationOnce(() => {        throw cause;      });      const browserSessions = yield* BrowserSession.BrowserSession;      const partition = yield* browserSessions.getPartition("environment-b");      const error = yield* browserSessions.getSession("environment-b").pipe(Effect.flip);       assert.instanceOf(error, BrowserSession.BrowserSessionCreationError);      assert.equal(error.scope, "environment-b");      assert.equal(error.partition, partition);      assert.strictEqual(error.cause, cause);      assert.equal(        error.message,        `Failed to create a desktop preview browser session for scope environment-b (partition ${partition}).`,      );      assert.notInclude(error.message, cause.message);    }).pipe(Effect.provide(layer)),  );   it.effect("clears storage and cache for every created session", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;      yield* browserSessions.getSession("scope-a");      yield* browserSessions.getSession("scope-b");       yield* browserSessions.clearCookies();      yield* browserSessions.clearCache();       assert.strictEqual(sessions.size, 2);      for (const browserSession of sessions.values()) {        assert.strictEqual(browserSession.clearStorageData.mock.calls.length, 1);        assert.deepEqual(browserSession.clearStorageData.mock.calls[0], [          {            storages: ["cookies", "localstorage", "indexdb", "serviceworkers"],          },        ]);        assert.strictEqual(browserSession.clearCache.mock.calls.length, 1);      }    }).pipe(Effect.provide(layer)),  );   it.effect("clears a partition whose session has not been opened yet", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;      const partition = yield* browserSessions.getPartition("scope-untouched");       // Deriving the partition string does not create the session, and the      // clear only walks sessions it already holds. Without loading it first      // this reports success and deletes nothing — which is what a user      // clearing a profile after a restart would get.      assert.isUndefined(sessions.get(partition));      yield* browserSessions.clearCookies([partition]);      assert.isUndefined(sessions.get(partition));       yield* browserSessions.getSession("scope-untouched");      yield* browserSessions.clearCookies([partition]);       const created = sessions.get(partition);      assert.isDefined(created);      assert.strictEqual(created.clearStorageData.mock.calls.length, 1);    }).pipe(Effect.provide(layer)),  );   it.effect("correlates clear failures while still attempting every session", () =>    Effect.gen(function* () {      const browserSessions = yield* BrowserSession.BrowserSession;      yield* browserSessions.getSession("scope-a");      yield* browserSessions.getSession("scope-b");      const firstPartition = yield* browserSessions.getPartition("scope-a");      const secondPartition = yield* browserSessions.getPartition("scope-b");      const firstSession = sessions.get(firstPartition);      const secondSession = sessions.get(secondPartition);      assert.isDefined(firstSession);      assert.isDefined(secondSession);       const storageCause = new Error("storage clear failed");      secondSession.clearStorageData.mockImplementationOnce(() => Promise.reject(storageCause));      const storageError = yield* browserSessions.clearCookies().pipe(Effect.flip);       assert.instanceOf(storageError, BrowserSession.BrowserSessionStorageClearError);      assert.equal(storageError.partition, secondPartition);      assert.strictEqual(storageError.cause, storageCause);      assert.equal(        storageError.message,        `Failed to clear desktop preview browser storage for partition ${secondPartition}.`,      );      assert.notInclude(storageError.message, storageCause.message);      for (const browserSession of sessions.values()) {        assert.strictEqual(browserSession.clearStorageData.mock.calls.length, 1);      }       const cacheCause = new Error("cache clear failed");      firstSession.clearCache.mockImplementationOnce(() => Promise.reject(cacheCause));      const cacheError = yield* browserSessions.clearCache().pipe(Effect.flip);       assert.instanceOf(cacheError, BrowserSession.BrowserSessionCacheClearError);      assert.equal(cacheError.partition, firstPartition);      assert.strictEqual(cacheError.cause, cacheCause);      assert.equal(        cacheError.message,        `Failed to clear the desktop preview browser cache for partition ${firstPartition}.`,      );      assert.notInclude(cacheError.message, cacheCause.message);      for (const browserSession of sessions.values()) {        assert.strictEqual(browserSession.clearCache.mock.calls.length, 1);      }    }).pipe(Effect.provide(layer)),  );});