1package coordinator
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/stretchr/testify/assert"
9
10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"
12 "github.com/bnema/dumber/internal/infrastructure/webkit"
13)
14
15func TestComposeOnClose_Order(t *testing.T) {
16 var calls []string
17
18 composed := composeOnClose(
19 func() { calls = append(calls, "existing") },
20 func() { calls = append(calls, "next") },
21 )
22 composed()
23
24 assert.Equal(t, []string{"existing", "next"}, calls)
25}
26
27func TestComposeOnURIChanged_Order(t *testing.T) {
28 var calls []string
29
30 composed := composeOnURIChanged(
31 func(_ string) { calls = append(calls, "existing") },
32 func(_ string) { calls = append(calls, "next") },
33 )
34 composed("https://example.com")
35
36 assert.Equal(t, []string{"existing", "next"}, calls)
37}
38
39func TestComposeOnLoadChanged_Order(t *testing.T) {
40 var calls []string
41
42 composed := composeOnLoadChanged(
43 func(_ webkit.LoadEvent) { calls = append(calls, "existing") },
44 func(_ webkit.LoadEvent) { calls = append(calls, "next") },
45 )
46 composed(webkit.LoadCommitted)
47
48 assert.Equal(t, []string{"existing", "next"}, calls)
49}
50
51func TestHandlePopupOAuthClose_SuccessSchedulesParentRefresh(t *testing.T) {
52 parentPaneID := entity.PaneID("parent-pane")
53 popupID := port.WebViewID(101)
54
55 c := &ContentCoordinator{
56 webViews: make(map[entity.PaneID]*webkit.WebView),
57 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
58 popupRefresh: make(map[entity.PaneID]*time.Timer),
59 }
60
61 c.trackOAuthPopup(popupID, parentPaneID)
62 c.capturePopupOAuthState(popupID, "https://app/callback?code=123")
63 c.handlePopupOAuthClose(context.Background(), popupID)
64
65 c.popupMu.RLock()
66 _, exists := c.popupOAuth[popupID]
67 refreshTimer := c.popupRefresh[parentPaneID]
68 c.popupMu.RUnlock()
69
70 assert.False(t, exists, "oauth state should be removed after close handling")
71 assert.NotNil(t, refreshTimer, "successful oauth close should schedule parent refresh")
72
73 waitFor(t, time.Second, func() bool {
74 c.popupMu.RLock()
75 defer c.popupMu.RUnlock()
76 return c.popupRefresh[parentPaneID] == nil
77 })
78}
79
80func TestHandlePopupOAuthClose_ErrorDoesNotScheduleParentRefresh(t *testing.T) {
81 parentPaneID := entity.PaneID("parent-pane")
82 popupID := port.WebViewID(102)
83
84 c := &ContentCoordinator{
85 webViews: make(map[entity.PaneID]*webkit.WebView),
86 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
87 popupRefresh: make(map[entity.PaneID]*time.Timer),
88 }
89
90 c.trackOAuthPopup(popupID, parentPaneID)
91 c.capturePopupOAuthState(popupID, "https://app/callback?error=access_denied")
92 c.handlePopupOAuthClose(context.Background(), popupID)
93
94 c.popupMu.RLock()
95 defer c.popupMu.RUnlock()
96 assert.Nil(t, c.popupRefresh[parentPaneID], "oauth errors should not refresh parent pane")
97}
98
99func TestIsOAuthURL_DoesNotTreatGenericStateParamAsOAuth(t *testing.T) {
100 url := "https://github.com/apps/linear/installations/new?state=Ar4EJk1ao3eyEDgSbYOtG8Cr4"
101
102 assert.False(t, IsOAuthURL(url))
103}
104
105func TestIsOAuthURL_DetectsOAuthAuthorizeRequest(t *testing.T) {
106 url := "https://auth.openai.com/oauth/authorize?response_type=code&client_id=app_123&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email+offline_access&state=abc"
107
108 assert.True(t, IsOAuthURL(url))
109}
110
111func TestShouldForceCloseOnSafetyTimeout(t *testing.T) {
112 t.Run("does not force close during in-progress auth challenge", func(t *testing.T) {
113 url := "https://accounts.google.com/v3/signin/challenge/dp?TL=AHU8sQabc"
114 assert.False(t, shouldForceCloseOnSafetyTimeout(url))
115 })
116
117 t.Run("does not force close when callback is reached", func(t *testing.T) {
118 url := "http://localhost:1455/auth/callback?code=abc123&state=xyz"
119 assert.False(t, shouldForceCloseOnSafetyTimeout(url))
120 })
121}
122
123func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {
124 t.Helper()
125
126 deadline := time.Now().Add(timeout)
127 for time.Now().Before(deadline) {
128 if condition() {
129 return
130 }
131 time.Sleep(20 * time.Millisecond)
132 }
133 t.Fatalf("condition not met before timeout (%s)", timeout)
134}