1package dialog
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"
9 dialogmocks "github.com/bnema/dumber/internal/ui/dialog/mocks"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"
12)
13
14type permissionPopupShowCall struct {
15 heading string
16 body string
17}
18
19func TestPermissionDialog_QueuesRequestsWhilePopupVisible(t *testing.T) {
20 popup := dialogmocks.NewMockPermissionPopup(t)
21 var showCalls []permissionPopupShowCall
22 var callback func(allowed, persistent bool)
23 popup.EXPECT().
24 Show(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
25 Run(func(_ context.Context, heading string, body string, _ bool, cb func(allowed, persistent bool)) {
26 showCalls = append(showCalls, permissionPopupShowCall{
27 heading: heading,
28 body: body,
29 })
30 callback = cb
31 }).
32 Twice()
33
34 respond := func(allowed, persistent bool) {
35 if callback == nil {
36 return
37 }
38 cb := callback
39 callback = nil
40 cb(allowed, persistent)
41 }
42
43 d := &PermissionDialog{popup: popup}
44
45 var firstResult port.PermissionDialogResult
46 firstCalled := false
47 d.ShowPermissionDialog(
48 context.Background(),
49 "https://example.com",
50 []entity.PermissionType{entity.PermissionTypeMicrophone},
51 nil,
52 func(result port.PermissionDialogResult) {
53 firstCalled = true
54 firstResult = result
55 },
56 )
57
58 secondCalled := false
59 var secondResult port.PermissionDialogResult
60 d.ShowPermissionDialog(
61 context.Background(),
62 "https://example.com",
63 []entity.PermissionType{entity.PermissionTypeCamera},
64 nil,
65 func(result port.PermissionDialogResult) {
66 secondCalled = true
67 secondResult = result
68 },
69 )
70
71 if assert.Len(t, showCalls, 1) {
72 assert.Equal(t, "Allow Microphone Access?", showCalls[0].heading)
73 }
74 assert.False(t, firstCalled)
75 assert.False(t, secondCalled)
76
77 respond(true, false)
78 assert.True(t, firstCalled)
79 assert.Equal(t, port.PermissionDialogResult{Allowed: true, Persistent: false}, firstResult)
80
81 if assert.Len(t, showCalls, 2) {
82 assert.Equal(t, "Allow Camera Access?", showCalls[1].heading)
83 }
84 assert.False(t, secondCalled)
85
86 respond(false, true)
87 assert.True(t, secondCalled)
88 assert.Equal(t, port.PermissionDialogResult{Allowed: false, Persistent: true}, secondResult)
89}
90
91func TestPermissionDialog_NoPopup_DeniesRequest(t *testing.T) {
92 d := &PermissionDialog{popup: nil}
93
94 called := false
95 result := port.PermissionDialogResult{}
96 d.ShowPermissionDialog(
97 context.Background(),
98 "https://example.com",
99 []entity.PermissionType{entity.PermissionTypeMicrophone},
100 nil,
101 func(r port.PermissionDialogResult) {
102 called = true
103 result = r
104 },
105 )
106
107 assert.True(t, called)
108 assert.Equal(t, port.PermissionDialogResult{Allowed: false, Persistent: false}, result)
109}
110
111func TestPermissionDialog_BuildHeadingAndBody_DisplayCombinations(t *testing.T) {
112 d := &PermissionDialog{}
113 origin := "https://meet.example.com"
114
115 tests := []struct {
116 name string
117 permTypes []entity.PermissionType
118 expectHeading string
119 expectedAction string
120 }{
121 {
122 name: "display only",
123 permTypes: []entity.PermissionType{entity.PermissionTypeDisplay},
124 expectHeading: "Allow Screen Sharing?",
125 expectedAction: "share your screen",
126 },
127 {
128 name: "microphone and display",
129 permTypes: []entity.PermissionType{entity.PermissionTypeMicrophone, entity.PermissionTypeDisplay},
130 expectHeading: "Allow Microphone and Screen Sharing?",
131 expectedAction: "access your microphone and share your screen",
132 },
133 {
134 name: "camera and display",
135 permTypes: []entity.PermissionType{entity.PermissionTypeCamera, entity.PermissionTypeDisplay},
136 expectHeading: "Allow Camera and Screen Sharing?",
137 expectedAction: "access your camera and share your screen",
138 },
139 {
140 name: "all three",
141 permTypes: []entity.PermissionType{entity.PermissionTypeMicrophone, entity.PermissionTypeCamera, entity.PermissionTypeDisplay},
142 expectHeading: "Allow Microphone, Camera, and Screen Sharing?",
143 expectedAction: "access your microphone and camera, and share your screen",
144 },
145 {
146 name: "notification only",
147 permTypes: []entity.PermissionType{entity.PermissionTypeNotification},
148 expectHeading: "Allow Notifications?",
149 expectedAction: "show notifications",
150 },
151 {
152 name: "geolocation only",
153 permTypes: []entity.PermissionType{entity.PermissionTypeGeolocation},
154 expectHeading: "Allow Location Access?",
155 expectedAction: "access your location",
156 },
157 {
158 name: "website data access only",
159 permTypes: []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess},
160 expectHeading: "Allow Third-Party Data Access?",
161 expectedAction: "access its stored data while you browse this site",
162 },
163 }
164
165 for _, tt := range tests {
166 t.Run(tt.name, func(t *testing.T) {
167 assert.Equal(t, tt.expectHeading, d.buildHeading(tt.permTypes))
168 assert.Equal(t, origin+" wants to "+tt.expectedAction+".", d.buildBody(origin, tt.permTypes, nil))
169 })
170 }
171
172 // Test website_data_access with populated metadata (Epiphany-style rich message)
173 t.Run("website data access with both domains", func(t *testing.T) {
174 meta := entity.PermissionMetadata{
175 entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com",
176 entity.PermissionMetadataKeyCurrentDomain: "shop.example.com",
177 }
178 body := d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess}, meta)
179 assert.Equal(t, origin+" wants to allow accounts.google.com to access its data (including cookies) while you browse shop.example.com.", body)
180 })
181}
182
183func TestPermissionDialog_BuildBody_UnmediatedDisplay(t *testing.T) {
184 d := &PermissionDialog{}
185 origin := "https://meet.example.com"
186 meta := entity.PermissionMetadata{
187 entity.PermissionMetadataKeyUnmediatedCapture: entity.PermissionMetadataValueTrue,
188 }
189
190 assert.Equal(t, origin+" wants to share your entire screen.",
191 d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeDisplay}, meta))
192 assert.Equal(t, origin+" wants to access your camera and share your entire screen.",
193 d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeCamera, entity.PermissionTypeDisplay}, meta))
194 // Mediated display (WebKit portal) keeps the generic wording.
195 assert.Equal(t, origin+" wants to share your screen.",
196 d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeDisplay}, nil))
197}
198
199func TestPermissionDialog_BuildBody_UnmediatedDisplayWithSystemAudio(t *testing.T) {
200 d := &PermissionDialog{}
201 origin := "https://meet.example.com"
202 meta := entity.PermissionMetadata{
203 entity.PermissionMetadataKeyUnmediatedCapture: entity.PermissionMetadataValueTrue,
204 entity.PermissionMetadataKeyUnmediatedAudio: entity.PermissionMetadataValueTrue,
205 }
206
207 assert.Equal(t, origin+" wants to share your entire screen and system audio.",
208 d.buildBody(origin, []entity.PermissionType{entity.PermissionTypeDisplay}, meta))
209}
210
211func TestAnyPersistable(t *testing.T) {
212 tests := []struct {
213 name string
214 types []entity.PermissionType
215 want bool
216 }{
217 {"display only", []entity.PermissionType{entity.PermissionTypeDisplay}, false},
218 {"microphone", []entity.PermissionType{entity.PermissionTypeMicrophone}, true},
219 {"microphone and display", []entity.PermissionType{entity.PermissionTypeMicrophone, entity.PermissionTypeDisplay}, true},
220 {"geolocation", []entity.PermissionType{entity.PermissionTypeGeolocation}, true},
221 {"none", nil, false},
222 }
223 for _, tt := range tests {
224 t.Run(tt.name, func(t *testing.T) {
225 assert.Equal(t, tt.want, anyPersistable(tt.types))
226 })
227 }
228}
229
230func TestPermissionDialog_PassesPersistableToPopup(t *testing.T) {
231 tests := []struct {
232 name string
233 types []entity.PermissionType
234 want bool
235 }{
236 {"display only hides always buttons", []entity.PermissionType{entity.PermissionTypeDisplay}, false},
237 {"mixed keeps always buttons", []entity.PermissionType{entity.PermissionTypeMicrophone, entity.PermissionTypeDisplay}, true},
238 }
239 for _, tt := range tests {
240 t.Run(tt.name, func(t *testing.T) {
241 popup := dialogmocks.NewMockPermissionPopup(t)
242 var got *bool
243 popup.EXPECT().
244 Show(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
245 Run(func(_ context.Context, _ string, _ string, persistable bool, _ func(allowed, persistent bool)) {
246 got = &persistable
247 }).Once()
248
249 d := &PermissionDialog{popup: popup}
250 d.ShowPermissionDialog(context.Background(), "https://meet.example.com", tt.types, nil, func(port.PermissionDialogResult) {})
251
252 if assert.NotNil(t, got) {
253 assert.Equal(t, tt.want, *got)
254 }
255 })
256 }
257}
258
259func TestPermissionDialog_ExternalLaunchConfirmHidesQueryAndIsNotPersistable(t *testing.T) {
260 popup := dialogmocks.NewMockPermissionPopup(t)
261 var body string
262 var persistable bool
263 var cb func(allowed, persistent bool)
264 popup.EXPECT().
265 Show(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
266 Run(func(_ context.Context, _ string, b string, p bool, c func(allowed, persistent bool)) {
267 body, persistable, cb = b, p, c
268 }).Once()
269
270 d := &PermissionDialog{popup: popup}
271 var allowed bool
272 d.ShowExternalLaunchConfirm(context.Background(), "claude.ai", "claude://login/google-auth?code=secret", func(a bool) { allowed = a })
273
274 assert.Contains(t, body, "claude.ai wants to open claude://login")
275 assert.NotContains(t, body, "secret")
276 assert.False(t, persistable)
277
278 cb(true, false)
279 assert.True(t, allowed)
280}