1package desktop
2
3import (
4 "context"
5 "encoding/json"
6 "net"
7 "os"
8 "path/filepath"
9 "testing"
10 "time"
11
12 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17type browserWindowOpenerFunc func(context.Context, string) error
18
19func (f browserWindowOpenerFunc) OpenFreshWindow(ctx context.Context, url string) error {
20 return f(ctx, url)
21}
22
23func waitForSocket(t *testing.T, path string) {
24 t.Helper()
25
26 require.Eventually(t, func() bool {
27 _, err := os.Stat(path)
28 return err == nil
29 }, time.Second, 10*time.Millisecond)
30}
31
32func waitForSocketGone(t *testing.T, path string) {
33 t.Helper()
34
35 require.Eventually(t, func() bool {
36 _, err := os.Stat(path)
37 return os.IsNotExist(err)
38 }, time.Second, 10*time.Millisecond)
39}
40
41func shortTempDir(t *testing.T) string {
42 t.Helper()
43
44 dir, err := os.MkdirTemp("", "dbr-")
45 require.NoError(t, err)
46 t.Cleanup(func() {
47 _ = os.RemoveAll(dir)
48 })
49
50 return dir
51}
52
53func testIPC(root string) runtimeprofile.IPCPaths {
54 runtimeDir := filepath.Join(root, "runtime")
55 return runtimeprofile.IPCPaths{
56 RuntimeDir: runtimeDir,
57 BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
58 }
59}
60
61func testNamespacedIPC(root, engine string) runtimeprofile.IPCPaths {
62 runtimeDir := filepath.Join(root, "runtime", engine)
63 return runtimeprofile.IPCPaths{
64 RuntimeDir: runtimeDir,
65 BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
66 }
67}
68
69func TestBrowserLaunchRelay_DeliverOpenFreshWindow_MissingListenerReturnsFalseNil(t *testing.T) {
70 relay := NewBrowserLaunchRelay(testIPC(shortTempDir(t)))
71
72 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com")
73
74 require.NoError(t, err)
75 assert.False(t, delivered)
76}
77
78func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
79 ipc := testNamespacedIPC(shortTempDir(t), "cef")
80 listenerRelay := NewBrowserLaunchRelay(ipc)
81 clientRelay := NewBrowserLaunchRelay(ipc)
82
83 received := make(chan string, 1)
84 ctx, cancel := context.WithCancel(context.Background())
85 defer cancel()
86
87 closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
88 received <- url
89 return nil
90 }))
91 require.NoError(t, err)
92 defer closer.Close()
93
94 waitForSocket(t, ipc.BrowserLaunchSocket)
95
96 delivered, err := clientRelay.DeliverOpenFreshWindow(context.Background(), "https://example.com/same-namespace")
97 require.NoError(t, err)
98 require.True(t, delivered)
99 require.Equal(t, "https://example.com/same-namespace", <-received)
100}
101
102func TestBrowserLaunchRelay_DevCEFAndDevWebKit_UseDifferentSockets(t *testing.T) {
103 root := t.TempDir()
104 cefIPC := testNamespacedIPC(root, "cef")
105 wkIPC := testNamespacedIPC(root, "webkit")
106 require.NotEqual(t, cefIPC.BrowserLaunchSocket, wkIPC.BrowserLaunchSocket)
107}
108
109func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
110 ipc := testIPC(shortTempDir(t))
111 relay := NewBrowserLaunchRelay(ipc)
112
113 received := make(chan string, 1)
114 ctx, cancel := context.WithCancel(context.Background())
115 defer cancel()
116
117 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
118 received <- url
119 return nil
120 }))
121 require.NoError(t, err)
122 defer closer.Close()
123
124 waitForSocket(t, ipc.BrowserLaunchSocket)
125
126 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/new")
127
128 require.NoError(t, err)
129 assert.True(t, delivered)
130
131 select {
132 case got := <-received:
133 assert.Equal(t, "https://example.com/new", got)
134 case <-time.After(time.Second):
135 t.Fatal("expected opener to receive the URL")
136 }
137}
138
139func TestBrowserLaunchRelay_RequiresBrowserLaunchSocketPath(t *testing.T) {
140 relay := NewBrowserLaunchRelay(runtimeprofile.IPCPaths{})
141
142 _, err := relay.Listen(context.Background(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
143 require.Error(t, err)
144 require.Contains(t, err.Error(), "browser launch socket path")
145}
146
147func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t *testing.T) {
148 ipc := testIPC(shortTempDir(t))
149 relay := NewBrowserLaunchRelay(ipc)
150
151 firstReceived := make(chan string, 1)
152 ctx, cancel := context.WithCancel(context.Background())
153 defer cancel()
154
155 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
156 firstReceived <- url
157 return nil
158 }))
159 require.NoError(t, err)
160 defer closer.Close()
161
162 waitForSocket(t, ipc.BrowserLaunchSocket)
163
164 _, err = relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
165 require.Error(t, err)
166
167 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/live")
168 require.NoError(t, err)
169 require.True(t, delivered)
170
171 select {
172 case got := <-firstReceived:
173 assert.Equal(t, "https://example.com/live", got)
174 case <-time.After(time.Second):
175 t.Fatal("expected the original listener to remain active")
176 }
177}
178
179func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
180 ipc := testIPC(shortTempDir(t))
181 relay := NewBrowserLaunchRelay(ipc)
182
183 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
184 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
185
186 received := make(chan string, 1)
187 ctx, cancel := context.WithCancel(context.Background())
188 defer cancel()
189
190 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
191 received <- url
192 return nil
193 }))
194 require.NoError(t, err)
195 defer closer.Close()
196
197 waitForSocket(t, ipc.BrowserLaunchSocket)
198
199 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/stale")
200 require.NoError(t, err)
201 require.True(t, delivered)
202
203 select {
204 case got := <-received:
205 assert.Equal(t, "https://example.com/stale", got)
206 case <-time.After(time.Second):
207 t.Fatal("expected stale socket path to be rebound")
208 }
209}
210
211func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
212 ipc := testIPC(shortTempDir(t))
213 relay := NewBrowserLaunchRelay(ipc)
214
215 received := make(chan string, 1)
216 ctx, cancel := context.WithCancel(context.Background())
217 defer cancel()
218
219 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
220 received <- url
221 return nil
222 }))
223 require.NoError(t, err)
224 defer closer.Close()
225
226 waitForSocket(t, ipc.BrowserLaunchSocket)
227
228 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
229 require.NoError(t, err)
230
231 _, err = conn.Write([]byte("not-json"))
232 require.NoError(t, err)
233 require.NoError(t, conn.Close())
234
235 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/still-works")
236
237 require.NoError(t, err)
238 assert.True(t, delivered)
239
240 select {
241 case got := <-received:
242 assert.Equal(t, "https://example.com/still-works", got)
243 case <-time.After(time.Second):
244 t.Fatal("expected opener to receive the URL after malformed payload")
245 }
246}
247
248func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
249 ipc := testIPC(shortTempDir(t))
250 relay := NewBrowserLaunchRelay(ipc)
251
252 ctx, cancel := context.WithCancel(context.Background())
253 defer cancel()
254
255 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
256 require.NoError(t, err)
257
258 waitForSocket(t, ipc.BrowserLaunchSocket)
259
260 require.NoError(t, closer.Close())
261 waitForSocketGone(t, ipc.BrowserLaunchSocket)
262}
263
264func TestBrowserLaunchRelay_ContextCancelStopsServing(t *testing.T) {
265 ipc := testIPC(shortTempDir(t))
266 relay := NewBrowserLaunchRelay(ipc)
267
268 ctx, cancel := context.WithCancel(context.Background())
269 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
270 require.NoError(t, err)
271
272 waitForSocket(t, ipc.BrowserLaunchSocket)
273
274 cancel()
275 waitForSocketGone(t, ipc.BrowserLaunchSocket)
276 require.NoError(t, closer.Close())
277
278 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/after-cancel")
279 require.NoError(t, err)
280 assert.False(t, delivered)
281}
282
283func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RespectsResponseReadTimeout(t *testing.T) {
284 ipc := testIPC(shortTempDir(t))
285 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
286
287 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
288 require.NoError(t, err)
289 defer func() {
290 _ = listener.Close()
291 _ = os.Remove(ipc.BrowserLaunchSocket)
292 }()
293
294 ready := make(chan struct{})
295 go func() {
296 close(ready)
297 conn, acceptErr := listener.AcceptUnix()
298 if acceptErr != nil {
299 return
300 }
301 defer conn.Close()
302 <-time.After(time.Second)
303 }()
304 <-ready
305
306 relay := NewBrowserLaunchRelay(ipc)
307 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
308 defer cancel()
309
310 delivered, err := relay.DeliverOpenFreshWindow(ctx, "https://example.com/slow")
311 require.Error(t, err)
312 require.ErrorIs(t, err, context.DeadlineExceeded)
313 assert.False(t, delivered)
314}
315
316func TestBrowserLaunchRelay_DeliverOpenFreshWindow_ReturnsUnconfirmedErrorOnNoDeadlineTimeout(t *testing.T) {
317 ipc := testIPC(shortTempDir(t))
318 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
319
320 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
321 require.NoError(t, err)
322 defer func() {
323 _ = listener.Close()
324 _ = os.Remove(ipc.BrowserLaunchSocket)
325 }()
326
327 ready := make(chan struct{})
328 go func() {
329 close(ready)
330 conn, acceptErr := listener.AcceptUnix()
331 if acceptErr != nil {
332 return
333 }
334 defer conn.Close()
335 <-time.After(time.Second)
336 }()
337 <-ready
338
339 relay := NewBrowserLaunchRelay(ipc)
340
341 started := make(chan struct{})
342
343 type deliverResult struct {
344 delivered bool
345 err error
346 }
347 result := make(chan deliverResult, 1)
348 go func() {
349 close(started)
350 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/slow")
351 result <- deliverResult{delivered: delivered, err: err}
352 }()
353 <-started
354
355 select {
356 case got := <-result:
357 assert.True(t, got.delivered)
358 require.Error(t, got.err)
359 require.ErrorIs(t, got.err, ErrBrowserLaunchRelayUnconfirmed)
360 case <-time.After(browserLaunchIOTimeout * 3):
361 t.Fatal("DeliverOpenFreshWindow blocked without a caller deadline")
362 }
363}
364
365func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
366 ipc := testIPC(shortTempDir(t))
367 relay := NewBrowserLaunchRelay(ipc)
368
369 started := make(chan struct{})
370 release := make(chan struct{})
371 ctx, cancel := context.WithCancel(context.Background())
372 defer cancel()
373
374 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
375 close(started)
376 <-release
377 return nil
378 }))
379 require.NoError(t, err)
380 defer closer.Close()
381
382 waitForSocket(t, ipc.BrowserLaunchSocket)
383
384 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
385 require.NoError(t, err)
386 defer conn.Close()
387 require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
388 require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{URL: "https://example.com/slow-but-healthy"}))
389
390 var response browserLaunchResponse
391 require.NoError(t, json.NewDecoder(conn).Decode(&response))
392 assert.Empty(t, response.Error)
393
394 select {
395 case <-started:
396 case <-time.After(time.Second):
397 t.Fatal("expected opener to be invoked after acknowledgement")
398 }
399 close(release)
400}
401
402func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
403 ipc := testIPC(shortTempDir(t))
404 relay := NewBrowserLaunchRelay(ipc)
405
406 received := make(chan string, 1)
407 ctx, cancel := context.WithCancel(context.Background())
408 defer cancel()
409
410 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
411 received <- url
412 return nil
413 }))
414 require.NoError(t, err)
415 defer closer.Close()
416
417 waitForSocket(t, ipc.BrowserLaunchSocket)
418
419 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
420 require.NoError(t, err)
421 defer conn.Close()
422 time.Sleep(20 * time.Millisecond)
423
424 type deliverResult struct {
425 delivered bool
426 err error
427 }
428 result := make(chan deliverResult, 1)
429 go func() {
430 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/recovered")
431 result <- deliverResult{delivered: delivered, err: err}
432 }()
433
434 select {
435 case got := <-result:
436 require.NoError(t, got.err)
437 require.True(t, got.delivered)
438 case <-time.After(300 * time.Millisecond):
439 t.Fatal("silent client stalled the relay listener")
440 }
441
442 select {
443 case got := <-received:
444 assert.Equal(t, "https://example.com/recovered", got)
445 case <-time.After(time.Second):
446 t.Fatal("expected opener to receive the URL")
447 }
448}