1package desktop
2
3import (
4 "context"
5 "net"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/stretchr/testify/require"
13)
14
15type prepareTestCloser struct{}
16
17func (*prepareTestCloser) Close() error { return nil }
18
19func TestBrowserLaunchRelayPrepareReservesUntilListen(t *testing.T) {
20 ipc := testIPC(shortTempDir(t))
21 relay := NewBrowserLaunchRelay(ipc)
22 reservation := &prepareTestCloser{}
23 err := relay.(port.BrowserLaunchRelayPreparer).Prepare(reservation)
24 require.NoError(t, err)
25 defer reservation.Close()
26
27 // A competing process must not steal the socket before GTK is ready.
28 competitor := NewBrowserLaunchRelay(ipc)
29 err = competitor.(port.BrowserLaunchRelayPreparer).Prepare(&prepareTestCloser{})
30 require.Error(t, err)
31
32 // Bound is not ready: requests must not be acknowledged yet.
33 conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
34 require.NoError(t, err)
35 defer conn.Close()
36 _, err = conn.Write([]byte("{\"url\":\"https://example.com\"}\n"))
37 require.NoError(t, err)
38 require.NoError(t, conn.SetReadDeadline(time.Now().Add(30*time.Millisecond)))
39 var response [1]byte
40 _, err = conn.Read(response[:])
41 require.Error(t, err)
42 var timeout net.Error
43 require.ErrorAs(t, err, &timeout)
44 require.True(t, timeout.Timeout())
45
46 received := make(chan string, 2)
47 closer, err := relay.Listen(t.Context(), browserWindowOpenerFunc(func(_ context.Context, url string) error {
48 received <- url
49 return nil
50 }))
51 require.NoError(t, err)
52 require.NotSame(t, reservation, closer)
53 select {
54 case url := <-received:
55 require.Equal(t, "https://example.com", url)
56 case <-time.After(time.Second):
57 t.Fatal("reserved socket was not activated")
58 }
59 require.NoError(t, closer.Close())
60 require.NoError(t, reservation.Close())
61 require.NoFileExists(t, ipc.BrowserLaunchSocket)
62}
63
64func TestBrowserLaunchRelayPrepareRecoversStalePathWhileLeaseHeld(t *testing.T) {
65 ipc := testIPC(shortTempDir(t))
66 require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
67 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
68 relay := NewBrowserLaunchRelay(ipc)
69 err := relay.(port.BrowserLaunchRelayPreparer).Prepare(&prepareTestCloser{})
70 require.NoError(t, err)
71 info, err := os.Stat(ipc.BrowserLaunchSocket)
72 require.NoError(t, err)
73 require.NotZero(t, info.Mode()&os.ModeSocket)
74}
75
76func TestBrowserLaunchRelayListenerStopsIndependentlyOfLease(t *testing.T) {
77 ipc := testIPC(shortTempDir(t))
78 relay := NewBrowserLaunchRelay(ipc)
79 lease := &prepareTestCloser{}
80 require.NoError(t, relay.(port.BrowserLaunchRelayPreparer).Prepare(lease))
81 listener, err := relay.Listen(t.Context(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
82 require.NoError(t, err)
83
84 // Closing the listener unlinks its socket but does not own or release lease.
85 require.NoError(t, listener.Close())
86 require.NoFileExists(t, ipc.BrowserLaunchSocket)
87 require.NoError(t, lease.Close())
88
89 replacement := NewBrowserLaunchRelay(ipc)
90 require.NoError(t, replacement.(port.BrowserLaunchRelayPreparer).Prepare(&prepareTestCloser{}))
91 require.FileExists(t, ipc.BrowserLaunchSocket)
92 // Idempotent old close must not unlink the successor socket.
93 require.NoError(t, listener.Close())
94 require.FileExists(t, ipc.BrowserLaunchSocket)
95}