1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import { beforeEach, vi } from "vite-plus/test";
4
5const { openExternalMock, writeTextMock } = vi.hoisted(() => ({
6 openExternalMock: vi.fn(),
7 writeTextMock: vi.fn(),
8}));
9
10vi.mock("electron", () => ({
11 shell: {
12 openExternal: openExternalMock,
13 },
14 clipboard: {
15 writeText: writeTextMock,
16 },
17}));
18
19import * as ElectronShell from "./ElectronShell.ts";
20
21describe("ElectronShell", () => {
22 beforeEach(() => {
23 openExternalMock.mockReset();
24 writeTextMock.mockReset();
25 });
26
27 it.effect("opens safe external URLs", () =>
28 Effect.gen(function* () {
29 openExternalMock.mockResolvedValue(undefined);
30
31 const electronShell = yield* ElectronShell.ElectronShell;
32 const result = yield* electronShell.openExternal("https://example.com/path");
33
34 assert.equal(result, true);
35 assert.deepEqual(openExternalMock.mock.calls, [["https://example.com/path"]]);
36 }).pipe(Effect.provide(ElectronShell.layer)),
37 );
38
39 it.effect("copies text to the system clipboard", () =>
40 Effect.gen(function* () {
41 writeTextMock.mockResolvedValue(undefined);
42
43 const electronShell = yield* ElectronShell.ElectronShell;
44 yield* electronShell.copyText("https://example.com/path");
45
46 assert.deepEqual(writeTextMock.mock.calls, [["https://example.com/path"]]);
47 }).pipe(Effect.provide(ElectronShell.layer)),
48 );
49
50 it.effect("does not fail when the clipboard write rejects", () =>
51 Effect.gen(function* () {
52 writeTextMock.mockRejectedValue(new Error("write failed"));
53
54 const electronShell = yield* ElectronShell.ElectronShell;
55 yield* electronShell.copyText("https://example.com/path");
56
57 assert.deepEqual(writeTextMock.mock.calls, [["https://example.com/path"]]);
58 }).pipe(Effect.provide(ElectronShell.layer)),
59 );
60
61 it.effect("opens the Full Disk Access settings anchor", () =>
62 Effect.gen(function* () {
63 openExternalMock.mockResolvedValue(undefined);
64
65 const electronShell = yield* ElectronShell.ElectronShell;
66 const result = yield* electronShell.openSystemSettings("full-disk-access");
67
68 assert.equal(result, true);
69 assert.deepEqual(openExternalMock.mock.calls, [
70 ["x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles"],
71 ]);
72 }).pipe(Effect.provide(ElectronShell.layer)),
73 );
74
75 it.effect("opens remote SSH editor URLs", () =>
76 Effect.gen(function* () {
77 openExternalMock.mockResolvedValue(undefined);
78
79 const electronShell = yield* ElectronShell.ElectronShell;
80 const result = yield* electronShell.openExternal(
81 "vscode://vscode-remote/ssh-remote+example.com/home/user/project",
82 );
83
84 assert.equal(result, true);
85 assert.deepEqual(openExternalMock.mock.calls, [
86 ["vscode://vscode-remote/ssh-remote+example.com/home/user/project"],
87 ]);
88 }).pipe(Effect.provide(ElectronShell.layer)),
89 );
90
91 it.effect("opens Zed's ssh deep link", () =>
92 Effect.gen(function* () {
93 openExternalMock.mockResolvedValue(undefined);
94
95 const electronShell = yield* ElectronShell.ElectronShell;
96 const results = yield* Effect.all([
97 electronShell.openExternal("zed://ssh/example.com/home/user/project"),
98 electronShell.openExternal("zed://ssh/example.com/"),
99 ]);
100
101 assert.deepEqual(results, [true, true]);
102 assert.deepEqual(openExternalMock.mock.calls, [
103 ["zed://ssh/example.com/home/user/project"],
104 ["zed://ssh/example.com/"],
105 ]);
106 }).pipe(Effect.provide(ElectronShell.layer)),
107 );
108
109 it.effect("opens the JetBrains Toolbox ssh deep link", () =>
110 Effect.gen(function* () {
111 openExternalMock.mockResolvedValue(undefined);
112
113 const url =
114 "jetbrains://gateway/ssh/environment?h=example.com&launchIde=true&ideHint=IU&projectHint=%2Fhome%2Fuser%2Fproject";
115 const electronShell = yield* ElectronShell.ElectronShell;
116 const result = yield* electronShell.openExternal(url);
117
118 assert.equal(result, true);
119 assert.deepEqual(openExternalMock.mock.calls, [[url]]);
120 }).pipe(Effect.provide(ElectronShell.layer)),
121 );
122
123 it.effect("does not open editor URLs that mix up link shapes", () =>
124 Effect.gen(function* () {
125 openExternalMock.mockResolvedValue(undefined);
126
127 const electronShell = yield* ElectronShell.ElectronShell;
128 const results = yield* Effect.all([
129 electronShell.openExternal("zed://extension/attacker"),
130 electronShell.openExternal("vscode://ssh/example.com/home/user/project"),
131 electronShell.openExternal("jetbrains://idea/navigate/reference?project=x&path=y"),
132 electronShell.openExternal("jetbrains://gateway/ssh/environment?launchIde=true"),
133 electronShell.openExternal("jetbrains://gateway/com.example.provider/connect?h=x"),
134 ]);
135
136 assert.deepEqual(results, [false, false, false, false, false]);
137 assert.equal(openExternalMock.mock.calls.length, 0);
138 }).pipe(Effect.provide(ElectronShell.layer)),
139 );
140
141 it.effect("does not open remote editor URLs with userinfo", () =>
142 Effect.gen(function* () {
143 openExternalMock.mockResolvedValue(undefined);
144
145 const electronShell = yield* ElectronShell.ElectronShell;
146 const results = yield* Effect.all([
147 electronShell.openExternal(
148 "vscode://user@vscode-remote/ssh-remote+example.com/home/user/project",
149 ),
150 electronShell.openExternal(
151 "vscode://:secret@vscode-remote/ssh-remote+example.com/home/user/project",
152 ),
153 electronShell.openExternal("zed://ssh/user@example.com/home/user/project"),
154 electronShell.openExternal("jetbrains://user@gateway/ssh/environment?h=example.com"),
155 ]);
156
157 assert.deepEqual(results, [false, false, false, false]);
158 assert.equal(openExternalMock.mock.calls.length, 0);
159 }).pipe(Effect.provide(ElectronShell.layer)),
160 );
161
162 it.effect("does not open unsafe external URLs", () =>
163 Effect.gen(function* () {
164 const electronShell = yield* ElectronShell.ElectronShell;
165 const result = yield* electronShell.openExternal("file:///etc/passwd");
166
167 assert.equal(result, false);
168 assert.equal(openExternalMock.mock.calls.length, 0);
169 }).pipe(Effect.provide(ElectronShell.layer)),
170 );
171
172 it.effect("does not open non-remote editor URLs", () =>
173 Effect.gen(function* () {
174 openExternalMock.mockResolvedValue(undefined);
175
176 const electronShell = yield* ElectronShell.ElectronShell;
177 const result = yield* electronShell.openExternal(
178 "vscode://ms-python.python/some-command?argument=attacker",
179 );
180
181 assert.equal(result, false);
182 assert.equal(openExternalMock.mock.calls.length, 0);
183 }).pipe(Effect.provide(ElectronShell.layer)),
184 );
185
186 it.effect("returns false when Electron rejects openExternal", () =>
187 Effect.gen(function* () {
188 openExternalMock.mockRejectedValue(new Error("open failed"));
189
190 const electronShell = yield* ElectronShell.ElectronShell;
191 const result = yield* electronShell.openExternal("https://example.com/path");
192
193 assert.equal(result, false);
194 }).pipe(Effect.provide(ElectronShell.layer)),
195 );
196});