1package desktop
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12
13 "github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18type browserWindowOpenerFunc func(context.Context, string) error
19
20func (f browserWindowOpenerFunc) OpenFreshWindow(ctx context.Context, url string) error {
21 return f(ctx, url)
22}
23
24func waitForSocket(t *testing.T, path string) {
25 t.Helper()
26
27 require.Eventually(t, func() bool {
28 _, err := os.Stat(path)
29 return err == nil
30 }, time.Second, 10*time.Millisecond)
31}
32
33func waitForSocketGone(t *testing.T, path string) {
34 t.Helper()
35
36 require.Eventually(t, func() bool {
37 _, err := os.Stat(path)
38 return os.IsNotExist(err)
39 }, time.Second, 10*time.Millisecond)
40}
41
42func shortTempDir(t *testing.T) string {
43 t.Helper()
44
45 dir, err := os.MkdirTemp("", "dbr-")
46 require.NoError(t, err)
47 t.Cleanup(func() {
48 _ = os.RemoveAll(dir)
49 })
50
51 return dir
52}
53
54func TestValidateBrowserLaunchSocketDirOwnedAcceptsCurrentUser(t *testing.T) {
55 ipc := testIPC(shortTempDir(t))
56 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
57
58 require.NoError(t, validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid())))
59}
60
61func TestValidateBrowserLaunchSocketDirOwnedRejectsUnexpectedOwner(t *testing.T) {
62 ipc := testIPC(shortTempDir(t))
63 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
64
65 unexpectedUID := uint32(os.Geteuid() + 1)
66 if unexpectedUID == uint32(os.Geteuid()) {
67 unexpectedUID++
68 }
69
70 err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, unexpectedUID)
71 require.Error(t, err)
72 require.Contains(t, err.Error(), "browser launch dir owner")
73}
74
75func TestValidateBrowserLaunchSocketDirOwnedRejectsWritableUserParent(t *testing.T) {
76 root := shortTempDir(t)
77 ipc := runtimeprofile.IPCPaths{
78 RuntimeDir: filepath.Join(root, "dumber", "dev-abc123", "cef"),
79 BrowserLaunchSocket: filepath.Join(root, "dumber", "dev-abc123", "cef", browserLaunchSocketName),
80 }
81 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
82 require.NoError(t, os.Chmod(filepath.Join(root, "dumber"), 0o777))
83 t.Cleanup(func() {
84 _ = os.Chmod(filepath.Join(root, "dumber"), 0o700)
85 })
86
87 err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid()))
88 require.Error(t, err)
89 require.Contains(t, err.Error(), "browser launch dir permissions")
90}
91
92func testIPC(root string) runtimeprofile.IPCPaths {
93 runtimeDir := filepath.Join(root, "runtime")
94 return runtimeprofile.IPCPaths{
95 RuntimeDir: runtimeDir,
96 BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
97 }
98}
99
100func testNamespacedIPC(root, engine string) runtimeprofile.IPCPaths {
101 runtimeDir := filepath.Join(root, "runtime", engine)
102 return runtimeprofile.IPCPaths{
103 RuntimeDir: runtimeDir,
104 BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
105 }
106}
107
108func overrideBrowserLaunchRequestID(t *testing.T, id string) func() {
109 t.Helper()
110 previous := newBrowserLaunchRequestID
111 newBrowserLaunchRequestID = func() string { return id }
112 return func() { newBrowserLaunchRequestID = previous }
113}
114
115func TestBrowserLaunchRelay_DeliverOpenFreshWindow_MissingListenerReturnsFalseNil(t *testing.T) {
116 relay := NewBrowserLaunchRelay(testIPC(shortTempDir(t)))
117
118 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com")
119
120 require.NoError(t, err)
121 assert.False(t, delivered)
122}
123
124func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
125 ipc := testNamespacedIPC(shortTempDir(t), "cef")
126 listenerRelay := NewBrowserLaunchRelay(ipc)
127 clientRelay := NewBrowserLaunchRelay(ipc)
128
129 received := make(chan string, 1)
130 ctx, cancel := context.WithCancel(context.Background())
131 defer cancel()
132
133 closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
134 received <- url
135 return nil
136 }))
137 require.NoError(t, err)
138 defer closer.Close()
139
140 waitForSocket(t, ipc.BrowserLaunchSocket)
141
142 delivered, err := clientRelay.DeliverOpenFreshWindow(context.Background(), "https://example.com/same-namespace")
143 require.NoError(t, err)
144 require.True(t, delivered)
145 require.Equal(t, "https://example.com/same-namespace", <-received)
146}
147
148func TestBrowserLaunchRelay_DevCEFAndDevWebKit_UseDifferentSockets(t *testing.T) {
149 root := t.TempDir()
150 cefIPC := testNamespacedIPC(root, "cef")
151 wkIPC := testNamespacedIPC(root, "webkit")
152 require.NotEqual(t, cefIPC.BrowserLaunchSocket, wkIPC.BrowserLaunchSocket)
153}
154
155func TestBrowserLaunchRelay_DeliverOpenFreshWindow_SendsRequestIDAndAcceptsMatchingResponse(t *testing.T) {
156 ipc := testIPC(shortTempDir(t))
157 relay := NewBrowserLaunchRelay(ipc)
158 restore := overrideBrowserLaunchRequestID(t, "request-test-1")
159 defer restore()
160
161 receivedRequest := make(chan browserLaunchRequest, 1)
162 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
163 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
164 require.NoError(t, err)
165 defer func() {
166 _ = listener.Close()
167 _ = os.Remove(ipc.BrowserLaunchSocket)
168 }()
169
170 go func() {
171 conn, acceptErr := listener.AcceptUnix()
172 if acceptErr != nil {
173 return
174 }
175 defer conn.Close()
176 var request browserLaunchRequest
177 if decodeErr := json.NewDecoder(conn).Decode(&request); decodeErr != nil {
178 return
179 }
180 receivedRequest <- request
181 _ = json.NewEncoder(conn).Encode(browserLaunchResponse{RequestID: request.RequestID, Accepted: true})
182 }()
183
184 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/request-id")
185
186 require.NoError(t, err)
187 require.True(t, delivered)
188 request := <-receivedRequest
189 require.Equal(t, "request-test-1", request.RequestID)
190 require.Equal(t, "https://example.com/request-id", request.URL)
191}
192
193func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RejectsMismatchedResponseRequestID(t *testing.T) {
194 ipc := testIPC(shortTempDir(t))
195 relay := NewBrowserLaunchRelay(ipc)
196 restore := overrideBrowserLaunchRequestID(t, "request-test-2")
197 defer restore()
198
199 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
200 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
201 require.NoError(t, err)
202 defer func() {
203 _ = listener.Close()
204 _ = os.Remove(ipc.BrowserLaunchSocket)
205 }()
206
207 go func() {
208 conn, acceptErr := listener.AcceptUnix()
209 if acceptErr != nil {
210 return
211 }
212 defer conn.Close()
213 var request browserLaunchRequest
214 if decodeErr := json.NewDecoder(conn).Decode(&request); decodeErr != nil {
215 return
216 }
217 _ = json.NewEncoder(conn).Encode(browserLaunchResponse{RequestID: "different-request", Accepted: true})
218 }()
219
220 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/request-id-mismatch")
221
222 require.Error(t, err)
223 require.True(t, delivered)
224 require.Contains(t, err.Error(), "mismatched browser launch relay response")
225}
226
227func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
228 ipc := testIPC(shortTempDir(t))
229 relay := NewBrowserLaunchRelay(ipc)
230
231 received := make(chan string, 1)
232 ctx, cancel := context.WithCancel(context.Background())
233 defer cancel()
234
235 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
236 received <- url
237 return nil
238 }))
239 require.NoError(t, err)
240 defer closer.Close()
241
242 waitForSocket(t, ipc.BrowserLaunchSocket)
243
244 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/new")
245
246 require.NoError(t, err)
247 assert.True(t, delivered)
248
249 select {
250 case got := <-received:
251 assert.Equal(t, "https://example.com/new", got)
252 case <-time.After(time.Second):
253 t.Fatal("expected opener to receive the URL")
254 }
255}
256
257func TestBrowserLaunchRelay_RequiresBrowserLaunchSocketPath(t *testing.T) {
258 relay := NewBrowserLaunchRelay(runtimeprofile.IPCPaths{})
259
260 _, err := relay.Listen(context.Background(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
261 require.Error(t, err)
262 require.Contains(t, err.Error(), "browser launch socket path")
263}
264
265func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t *testing.T) {
266 ipc := testIPC(shortTempDir(t))
267 relay := NewBrowserLaunchRelay(ipc)
268
269 firstReceived := make(chan string, 1)
270 ctx, cancel := context.WithCancel(context.Background())
271 defer cancel()
272
273 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
274 firstReceived <- url
275 return nil
276 }))
277 require.NoError(t, err)
278 defer closer.Close()
279
280 waitForSocket(t, ipc.BrowserLaunchSocket)
281
282 _, err = relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
283 require.Error(t, err)
284
285 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/live")
286 require.NoError(t, err)
287 require.True(t, delivered)
288
289 select {
290 case got := <-firstReceived:
291 assert.Equal(t, "https://example.com/live", got)
292 case <-time.After(time.Second):
293 t.Fatal("expected the original listener to remain active")
294 }
295}
296
297func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
298 ipc := testIPC(shortTempDir(t))
299 relay := NewBrowserLaunchRelay(ipc)
300
301 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
302 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
303
304 received := make(chan string, 1)
305 ctx, cancel := context.WithCancel(context.Background())
306 defer cancel()
307
308 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
309 received <- url
310 return nil
311 }))
312 require.NoError(t, err)
313 defer closer.Close()
314
315 waitForSocket(t, ipc.BrowserLaunchSocket)
316
317 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/stale")
318 require.NoError(t, err)
319 require.True(t, delivered)
320
321 select {
322 case got := <-received:
323 assert.Equal(t, "https://example.com/stale", got)
324 case <-time.After(time.Second):
325 t.Fatal("expected stale socket path to be rebound")
326 }
327}
328
329func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
330 ipc := testIPC(shortTempDir(t))
331 relay := NewBrowserLaunchRelay(ipc)
332
333 received := make(chan string, 1)
334 ctx, cancel := context.WithCancel(context.Background())
335 defer cancel()
336
337 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
338 received <- url
339 return nil
340 }))
341 require.NoError(t, err)
342 defer closer.Close()
343
344 waitForSocket(t, ipc.BrowserLaunchSocket)
345
346 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
347 require.NoError(t, err)
348
349 _, err = conn.Write([]byte("not-json"))
350 require.NoError(t, err)
351 require.NoError(t, conn.Close())
352
353 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/still-works")
354
355 require.NoError(t, err)
356 assert.True(t, delivered)
357
358 select {
359 case got := <-received:
360 assert.Equal(t, "https://example.com/still-works", got)
361 case <-time.After(time.Second):
362 t.Fatal("expected opener to receive the URL after malformed payload")
363 }
364}
365
366func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
367 ipc := testIPC(shortTempDir(t))
368 relay := NewBrowserLaunchRelay(ipc)
369
370 ctx, cancel := context.WithCancel(context.Background())
371 defer cancel()
372
373 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
374 require.NoError(t, err)
375
376 waitForSocket(t, ipc.BrowserLaunchSocket)
377
378 require.NoError(t, closer.Close())
379 waitForSocketGone(t, ipc.BrowserLaunchSocket)
380}
381
382func TestBrowserLaunchRelay_ContextCancelStopsServing(t *testing.T) {
383 ipc := testIPC(shortTempDir(t))
384 relay := NewBrowserLaunchRelay(ipc)
385
386 ctx, cancel := context.WithCancel(context.Background())
387 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
388 require.NoError(t, err)
389
390 waitForSocket(t, ipc.BrowserLaunchSocket)
391
392 cancel()
393 waitForSocketGone(t, ipc.BrowserLaunchSocket)
394 require.NoError(t, closer.Close())
395
396 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/after-cancel")
397 require.NoError(t, err)
398 assert.False(t, delivered)
399}
400
401func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RespectsResponseReadTimeout(t *testing.T) {
402 ipc := testIPC(shortTempDir(t))
403 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
404
405 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
406 require.NoError(t, err)
407 defer func() {
408 _ = listener.Close()
409 _ = os.Remove(ipc.BrowserLaunchSocket)
410 }()
411
412 ready := make(chan struct{})
413 go func() {
414 close(ready)
415 conn, acceptErr := listener.AcceptUnix()
416 if acceptErr != nil {
417 return
418 }
419 defer conn.Close()
420 <-time.After(time.Second)
421 }()
422 <-ready
423
424 relay := NewBrowserLaunchRelay(ipc)
425 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
426 defer cancel()
427
428 delivered, err := relay.DeliverOpenFreshWindow(ctx, "https://example.com/slow")
429 require.Error(t, err)
430 require.ErrorIs(t, err, context.DeadlineExceeded)
431 assert.False(t, delivered)
432}
433
434func TestBrowserLaunchRelay_DeliverOpenFreshWindow_ReturnsUnconfirmedErrorOnNoDeadlineTimeout(t *testing.T) {
435 ipc := testIPC(shortTempDir(t))
436 require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
437
438 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
439 require.NoError(t, err)
440 defer func() {
441 _ = listener.Close()
442 _ = os.Remove(ipc.BrowserLaunchSocket)
443 }()
444
445 ready := make(chan struct{})
446 go func() {
447 close(ready)
448 conn, acceptErr := listener.AcceptUnix()
449 if acceptErr != nil {
450 return
451 }
452 defer conn.Close()
453 <-time.After(time.Second)
454 }()
455 <-ready
456
457 relay := NewBrowserLaunchRelay(ipc)
458
459 started := make(chan struct{})
460
461 type deliverResult struct {
462 delivered bool
463 err error
464 }
465 result := make(chan deliverResult, 1)
466 go func() {
467 close(started)
468 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/slow")
469 result <- deliverResult{delivered: delivered, err: err}
470 }()
471 <-started
472
473 select {
474 case got := <-result:
475 assert.True(t, got.delivered)
476 require.Error(t, got.err)
477 require.ErrorIs(t, got.err, ErrBrowserLaunchRelayUnconfirmed)
478 case <-time.After(browserLaunchIOTimeout * 3):
479 t.Fatal("DeliverOpenFreshWindow blocked without a caller deadline")
480 }
481}
482
483func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
484 ipc := testIPC(shortTempDir(t))
485 relay := NewBrowserLaunchRelay(ipc)
486
487 started := make(chan struct{})
488 release := make(chan struct{})
489 ctx, cancel := context.WithCancel(context.Background())
490 defer cancel()
491
492 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
493 close(started)
494 <-release
495 return nil
496 }))
497 require.NoError(t, err)
498 defer closer.Close()
499
500 waitForSocket(t, ipc.BrowserLaunchSocket)
501
502 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
503 require.NoError(t, err)
504 defer conn.Close()
505 require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
506 require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-1", URL: "https://example.com/slow-but-healthy"}))
507
508 var response browserLaunchResponse
509 require.NoError(t, json.NewDecoder(conn).Decode(&response))
510 assert.Empty(t, response.Error)
511 assert.True(t, response.Accepted)
512 assert.Equal(t, "raw-request-1", response.RequestID)
513
514 select {
515 case <-started:
516 case <-time.After(time.Second):
517 t.Fatal("expected opener to be invoked after acknowledgement")
518 }
519 close(release)
520}
521
522func TestBrowserLaunchRelay_AcknowledgesAcceptedEvenWhenOpenerReturnsLateError(t *testing.T) {
523 ipc := testIPC(shortTempDir(t))
524 relay := NewBrowserLaunchRelay(ipc)
525
526 started := make(chan struct{})
527 ctx, cancel := context.WithCancel(context.Background())
528 defer cancel()
529
530 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
531 close(started)
532 return errors.New("late open failure")
533 }))
534 require.NoError(t, err)
535 defer closer.Close()
536
537 waitForSocket(t, ipc.BrowserLaunchSocket)
538
539 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
540 require.NoError(t, err)
541 defer conn.Close()
542 require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
543 require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-2", URL: "https://example.com/fails-later"}))
544
545 var response browserLaunchResponse
546 require.NoError(t, json.NewDecoder(conn).Decode(&response))
547 assert.Empty(t, response.Error)
548 assert.True(t, response.Accepted)
549 assert.Equal(t, "raw-request-2", response.RequestID)
550
551 select {
552 case <-started:
553 case <-time.After(time.Second):
554 t.Fatal("expected opener to be invoked after accepted acknowledgement")
555 }
556}
557
558func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
559 ipc := testIPC(shortTempDir(t))
560 relay := NewBrowserLaunchRelay(ipc)
561
562 received := make(chan string, 1)
563 ctx, cancel := context.WithCancel(context.Background())
564 defer cancel()
565
566 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
567 received <- url
568 return nil
569 }))
570 require.NoError(t, err)
571 defer closer.Close()
572
573 waitForSocket(t, ipc.BrowserLaunchSocket)
574
575 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
576 require.NoError(t, err)
577 defer conn.Close()
578 time.Sleep(20 * time.Millisecond)
579
580 type deliverResult struct {
581 delivered bool
582 err error
583 }
584 result := make(chan deliverResult, 1)
585 go func() {
586 delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/recovered")
587 result <- deliverResult{delivered: delivered, err: err}
588 }()
589
590 select {
591 case got := <-result:
592 require.NoError(t, got.err)
593 require.True(t, got.delivered)
594 case <-time.After(300 * time.Millisecond):
595 t.Fatal("silent client stalled the relay listener")
596 }
597
598 select {
599 case got := <-received:
600 assert.Equal(t, "https://example.com/recovered", got)
601 case <-time.After(time.Second):
602 t.Fatal("expected opener to receive the URL")
603 }
604}