Open History to see the last commit
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7
8import * as BrowserSession from "../BrowserSession.ts";
9import { readManagedPolicyFile } from "@t3tools/shared/managedPolicyFile";
10import * as DesktopManagedPolicy from "../../app/DesktopManagedPolicy.ts";
11import * as BrowserImport from "./BrowserImport.ts";
12import { BROWSER_IMPORT_SOURCES, sourcePathContext } from "./Sources.ts";
13import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
14
15const helium = BROWSER_IMPORT_SOURCES.find((source) => source.id === "helium")!;
16
17const cookie = {
18 url: "https://rejected.example/path",
19 name: "session",
20 value: "value",
21 domain: undefined,
22 path: "/",
23 secure: true,
24 httpOnly: true,
25 expirationDate: undefined,
26 sameSite: "lax" as const,
27};
28
29const layerRejectedBeforeSession = Layer.succeed(
30 BrowserSession.BrowserSession,
31 BrowserSession.BrowserSession.of({
32 getPartition: () => Effect.die("getPartition must not be reached"),
33 isPartition: () => false,
34 getSession: () => Effect.die("getSession must not be reached"),
35 clearCookies: () => Effect.die("clearCookies must not be reached"),
36 cookiesCleared: () => Effect.die("cookiesCleared must not be reached"),
37 clearCache: () => Effect.die("clearCache must not be reached"),
38 }),
39);
40
41const withImporter = Effect.fnUntraced(function* () {
42 const fileSystem = yield* FileSystem.FileSystem;
43 const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-import-" });
44 const layerEnvironment = Layer.succeed(HostProcess.Environment, { HOME: home });
45 const context = yield* sourcePathContext.pipe(
46 Effect.provideService(HostProcess.Environment, { HOME: home }),
47 Effect.provideService(HostProcess.Platform, "darwin"),
48 );
49 const root = helium.userDataDirectory(context);
50 if (root === undefined) throw new Error("Helium has no macOS user-data directory");
51 yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true });
52 yield* fileSystem.writeFileString(`${root}/Default/Cookies`, "db");
53
54 const importer = yield* BrowserImport.BrowserImport.pipe(
55 Effect.provide(
56 BrowserImport.layer.pipe(
57 Layer.provide(layerRejectedBeforeSession),
58 Layer.provide(DesktopManagedPolicy.layerTest()),
59 Layer.provide(layerEnvironment),
60 Layer.provide(Layer.succeed(HostProcess.Platform, "darwin")),
61 Layer.provide(Layer.succeed(HostProcess.ExecutablePath, "/Applications/T3 Code.app")),
62 Layer.provide(NodeServices.layer),
63 ),
64 ),
65 );
66 return { importer, home, root };
67});
68
69describe("BrowserImport.importCookies", () => {
70 it.effect("rejects a source profile the browser never reported", () =>
71 Effect.gen(function* () {
72 const fileSystem = yield* FileSystem.FileSystem;
73 const { importer, home } = yield* withImporter();
74
75 yield* fileSystem.makeDirectory(`${home}/secrets`, { recursive: true });
76 yield* fileSystem.writeFileString(`${home}/secrets/Cookies`, "not-a-db");
77
78 const error = yield* importer
79 .importCookies({
80 input: {
81 sourceId: "helium",
82 sourceProfileDirectory: "../../../../secrets",
83 targetProfileId: "default",
84 },
85 scope: "persist:t3code-preview-test",
86 persistent: true,
87 })
88 .pipe(Effect.flip);
89
90 assert.instanceOf(error, BrowserImport.BrowserImportFailedError);
91 assert.equal(error.reason, "unknownSourceProfile");
92 }).pipe(Effect.provide(NodeServices.layer)),
93 );
94
95 it.effect.skipIf(!symlinksSupported)(
96 "refuses to import while the source browser holds its profile",
97 () =>
98 Effect.gen(function* () {
99 const fileSystem = yield* FileSystem.FileSystem;
100 const { importer, root } = yield* withImporter();
101 yield* fileSystem.symlink("host-that-does-not-exist-1234", `${root}/SingletonLock`);
102
103 const error = yield* importer
104 .importCookies({
105 input: {
106 sourceId: "helium",
107 sourceProfileDirectory: "Default",
108 targetProfileId: "default",
109 },
110 scope: "persist:t3code-preview-test",
111 persistent: true,
112 })
113 .pipe(Effect.flip);
114
115 assert.instanceOf(error, BrowserImport.BrowserImportFailedError);
116 assert.equal(error.reason, "browserRunning");
117 }).pipe(Effect.provide(NodeServices.layer)),
118 );
119});
120
121describe("BrowserImport.checkBrowserImportPolicy", () => {
122 it.effect("allows imports unless a readable policy turns them off, and fails closed", () =>
123 Effect.gen(function* () {
124 const fileSystem = yield* FileSystem.FileSystem;
125 const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-import-policy-" });
126 const outcome = (contents: string | undefined) =>
127 Effect.gen(function* () {
128 const policyPath = `${dir}/policy.json`;
129 yield* fileSystem.remove(policyPath, { force: true });
130 if (contents !== undefined) yield* fileSystem.writeFileString(policyPath, contents);
131 const readPolicy = readManagedPolicyFile(policyPath).pipe(
132 Effect.map((document) => document && { source: policyPath, document }),
133 );
134 return yield* BrowserImport.checkBrowserImportPolicy(readPolicy).pipe(
135 Effect.match({ onFailure: (error) => error.message, onSuccess: () => "allowed" }),
136 );
137 });
138 const policy = (limits?: object) =>
139 JSON.stringify({ schemaVersion: 1, organization: "Example Corp", limits });
140
141 assert.equal(yield* outcome(undefined), "allowed");
142 assert.equal(yield* outcome(policy()), "allowed");
143 assert.equal(yield* outcome(policy({ browserCookieImport: true })), "allowed");
144 assert.equal(
145 yield* outcome(policy({ browserCookieImport: false })),
146 "Example Corp's policy does not allow importing browser cookies.",
147 );
148 for (const unreadable of ["{ not json", policy({ notALimit: false })]) {
149 assert.include(yield* outcome(unreadable), "could not be read");
150 }
151 }).pipe(Effect.provide(NodeServices.layer)),
152 );
153});
154
155describe("BrowserImport.writeCookies", () => {
156 it.effect("flushes the store after writing, and reports success if the flush fails", () =>
157 Effect.gen(function* () {
158 const events: Array<string> = [];
159 const result = yield* BrowserImport.writeCookies(
160 {
161 cookies: {
162 set: () => {
163 events.push("set");
164 return Promise.resolve();
165 },
166 flushStore: () => {
167 events.push("flush");
168 return Promise.reject(new Error("fixture flush failure"));
169 },
170 },
171 },
172 { cookies: [cookie, cookie], undecryptable: 0, undecryptableHosts: [] },
173 );
174
175 assert.deepEqual(events, ["set", "set", "flush"]);
176 assert.deepEqual(result, { imported: 2, skipped: 0, skippedDomains: [] });
177 }),
178 );
179});