1package usecase_test
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/port"
9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/bnema/dumber/internal/domain/entity"
12 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
13 "github.com/rs/zerolog"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/mock"
16 "github.com/stretchr/testify/require"
17)
18
19func permissionLoggerFromContext(ctx context.Context) *zerolog.Logger {
20 return zerolog.Ctx(ctx)
21}
22
23func TestHandlePermissionUseCase_AutoAllowDisplayCapture(t *testing.T) {
24 ctx := testContext()
25 permRepo := repomocks.NewMockPermissionRepository(t)
26 dialog := portmocks.NewMockPermissionDialogPresenter(t)
27
28 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
29
30 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
31 Return(nil, nil)
32
33 allowed := false
34 denied := false
35 callback := usecase.PermissionCallback{
36 Allow: func() { allowed = true },
37 Deny: func() { denied = true },
38 }
39
40 // Display capture should be auto-allowed (portal handles UI)
41 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
42 entity.PermissionTypeDisplay,
43 }, callback)
44
45 assert.True(t, allowed, "display capture should be auto-allowed")
46 assert.False(t, denied)
47 dialog.AssertNotCalled(t, "ShowPermissionDialog")
48}
49
50func TestHandlePermissionUseCase_AutoAllowDeviceInfo(t *testing.T) {
51 ctx := testContext()
52 permRepo := repomocks.NewMockPermissionRepository(t)
53 dialog := portmocks.NewMockPermissionDialogPresenter(t)
54
55 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
56
57 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDeviceInfo).
58 Return(nil, nil)
59
60 allowed := false
61 callback := usecase.PermissionCallback{
62 Allow: func() { allowed = true },
63 Deny: func() {},
64 }
65
66 // Device info should be auto-allowed
67 uc.HandlePermissionRequest(ctx, "https://example.com", []entity.PermissionType{
68 entity.PermissionTypeDeviceInfo,
69 }, callback)
70
71 assert.True(t, allowed, "device info should be auto-allowed")
72}
73
74func TestHandlePermissionUseCase_StoredPermissionGranted(t *testing.T) {
75 ctx := testContext()
76 permRepo := repomocks.NewMockPermissionRepository(t)
77 dialog := portmocks.NewMockPermissionDialogPresenter(t)
78
79 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
80
81 // Return stored granted permission
82 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
83 Return(&entity.PermissionRecord{
84 Origin: "https://meet.example.com",
85 Type: entity.PermissionTypeMicrophone,
86 Decision: entity.PermissionGranted,
87 UpdatedAt: time.Now().Unix(),
88 }, nil)
89
90 allowed := false
91 callback := usecase.PermissionCallback{
92 Allow: func() { allowed = true },
93 Deny: func() {},
94 }
95
96 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
97 entity.PermissionTypeMicrophone,
98 }, callback)
99
100 assert.True(t, allowed, "should use stored granted permission")
101 dialog.AssertNotCalled(t, "ShowPermissionDialog")
102}
103
104func TestHandlePermissionUseCase_StoredPermissionDenied(t *testing.T) {
105 ctx := testContext()
106 permRepo := repomocks.NewMockPermissionRepository(t)
107 dialog := portmocks.NewMockPermissionDialogPresenter(t)
108
109 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
110
111 // Return stored denied permission
112 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
113 Return(&entity.PermissionRecord{
114 Origin: "https://meet.example.com",
115 Type: entity.PermissionTypeCamera,
116 Decision: entity.PermissionDenied,
117 UpdatedAt: time.Now().Unix(),
118 }, nil)
119
120 denied := false
121 callback := usecase.PermissionCallback{
122 Allow: func() {},
123 Deny: func() { denied = true },
124 }
125
126 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
127 entity.PermissionTypeCamera,
128 }, callback)
129
130 assert.True(t, denied, "should use stored denied permission")
131 dialog.AssertNotCalled(t, "ShowPermissionDialog")
132}
133
134func TestHandlePermissionUseCase_ShowDialogForPrompt(t *testing.T) {
135 ctx := testContext()
136 permRepo := repomocks.NewMockPermissionRepository(t)
137 dialog := portmocks.NewMockPermissionDialogPresenter(t)
138
139 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
140
141 // No stored permission (returns nil)
142 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
143 Return(nil, nil)
144
145 allowed := false
146 callback := usecase.PermissionCallback{
147 Allow: func() { allowed = true },
148 Deny: func() {},
149 }
150
151 // Dialog shows and user clicks Allow (not persistent)
152 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
153 entity.PermissionTypeMicrophone,
154 }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, callback func(port.PermissionDialogResult)) {
155 callback(port.PermissionDialogResult{Allowed: true, Persistent: false})
156 })
157
158 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
159 entity.PermissionTypeMicrophone,
160 }, callback)
161
162 assert.True(t, allowed, "user allowed permission")
163 // Should not persist since Persistent=false
164 permRepo.AssertNotCalled(t, "Set")
165}
166
167func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
168 ctx := testContext()
169 permRepo := repomocks.NewMockPermissionRepository(t)
170 dialog := portmocks.NewMockPermissionDialogPresenter(t)
171
172 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
173
174 // No stored permission
175 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
176 Return(nil, nil)
177
178 allowed := false
179 callback := usecase.PermissionCallback{
180 Allow: func() { allowed = true },
181 Deny: func() {},
182 }
183
184 // Dialog shows and user clicks "Always Allow"
185 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
186 entity.PermissionTypeMicrophone,
187 }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {
188 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
189 })
190
191 // Should persist the granted permission
192 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
193 Run(func(_ context.Context, r *entity.PermissionRecord) {
194 assert.Equal(t, "https://meet.example.com", r.Origin)
195 assert.Equal(t, entity.PermissionTypeMicrophone, r.Type)
196 assert.Equal(t, entity.PermissionGranted, r.Decision)
197 }).Return(nil)
198
199 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
200 entity.PermissionTypeMicrophone,
201 }, callback)
202
203 assert.True(t, allowed)
204}
205
206func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
207 ctx := testContext()
208 permRepo := repomocks.NewMockPermissionRepository(t)
209 dialog := portmocks.NewMockPermissionDialogPresenter(t)
210
211 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
212
213 // No stored permission
214 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
215 Return(nil, nil)
216
217 denied := false
218 callback := usecase.PermissionCallback{
219 Allow: func() {},
220 Deny: func() { denied = true },
221 }
222
223 // Dialog shows and user clicks "Always Deny"
224 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
225 entity.PermissionTypeCamera,
226 }, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, cb func(port.PermissionDialogResult)) {
227 // User denied but wants to persist this decision
228 cb(port.PermissionDialogResult{Allowed: false, Persistent: true})
229 })
230
231 // Should persist the denied permission
232 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
233 Run(func(_ context.Context, r *entity.PermissionRecord) {
234 assert.Equal(t, entity.PermissionDenied, r.Decision)
235 }).Return(nil)
236
237 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
238 entity.PermissionTypeCamera,
239 }, callback)
240
241 assert.True(t, denied)
242}
243
244func TestHandlePermissionUseCase_EmptyOrigin(t *testing.T) {
245 ctx := testContext()
246 permRepo := repomocks.NewMockPermissionRepository(t)
247 dialog := portmocks.NewMockPermissionDialogPresenter(t)
248
249 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
250
251 denied := false
252 callback := usecase.PermissionCallback{
253 Allow: func() {},
254 Deny: func() { denied = true },
255 }
256
257 // Empty origin should be denied
258 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{
259 entity.PermissionTypeMicrophone,
260 }, callback)
261
262 assert.True(t, denied, "empty origin should be denied")
263}
264
265func TestHandlePermissionUseCase_CombinedPermissions(t *testing.T) {
266 ctx := testContext()
267 permRepo := repomocks.NewMockPermissionRepository(t)
268 dialog := portmocks.NewMockPermissionDialogPresenter(t)
269
270 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
271
272 // Both mic and camera - one denied means whole request denied
273 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
274 Return(&entity.PermissionRecord{
275 Origin: "https://meet.example.com",
276 Type: entity.PermissionTypeMicrophone,
277 Decision: entity.PermissionGranted,
278 }, nil)
279
280 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
281 Return(&entity.PermissionRecord{
282 Origin: "https://meet.example.com",
283 Type: entity.PermissionTypeCamera,
284 Decision: entity.PermissionDenied,
285 }, nil)
286
287 denied := false
288 callback := usecase.PermissionCallback{
289 Allow: func() {},
290 Deny: func() { denied = true },
291 }
292
293 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
294 entity.PermissionTypeMicrophone,
295 entity.PermissionTypeCamera,
296 }, callback)
297
298 assert.True(t, denied, "if any permission is denied, whole request is denied")
299}
300
301func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) {
302 ctx := testContext()
303 permRepo := repomocks.NewMockPermissionRepository(t)
304 dialog := portmocks.NewMockPermissionDialogPresenter(t)
305
306 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
307 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
308 Return(nil, nil)
309
310 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
311
312 assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted")
313}
314
315func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) {
316 ctx := testContext()
317 permRepo := repomocks.NewMockPermissionRepository(t)
318 dialog := portmocks.NewMockPermissionDialogPresenter(t)
319
320 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
321
322 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
323 Return(&entity.PermissionRecord{
324 Origin: "https://example.com",
325 Type: entity.PermissionTypeDisplay,
326 Decision: entity.PermissionDenied,
327 }, nil)
328
329 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
330
331 assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow")
332}
333
334func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) {
335 ctx := testContext()
336 permRepo := repomocks.NewMockPermissionRepository(t)
337 dialog := portmocks.NewMockPermissionDialogPresenter(t)
338
339 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
340
341 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
342 Return(&entity.PermissionRecord{
343 Origin: "https://meet.example.com",
344 Type: entity.PermissionTypeMicrophone,
345 Decision: entity.PermissionDenied,
346 }, nil)
347
348 state := uc.QueryPermissionState(ctx, "https://meet.example.com", entity.PermissionTypeMicrophone)
349
350 assert.Equal(t, entity.PermissionDenied, state)
351}
352
353func TestHandlePermissionUseCase_QueryPermissionState_NoRecord(t *testing.T) {
354 ctx := testContext()
355 permRepo := repomocks.NewMockPermissionRepository(t)
356 dialog := portmocks.NewMockPermissionDialogPresenter(t)
357
358 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
359
360 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
361 Return(nil, nil)
362
363 state := uc.QueryPermissionState(ctx, "https://meet.example.com", entity.PermissionTypeCamera)
364
365 assert.Equal(t, entity.PermissionPrompt, state, "no record should return prompt")
366}
367
368func TestHandlePermissionUseCase_QueryPermissionState_EmptyOriginReturnsPrompt(t *testing.T) {
369 ctx := testContext()
370 permRepo := repomocks.NewMockPermissionRepository(t)
371 dialog := portmocks.NewMockPermissionDialogPresenter(t)
372
373 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
374
375 state := uc.QueryPermissionState(ctx, "", entity.PermissionTypeDisplay)
376
377 assert.Equal(t, entity.PermissionPrompt, state, "empty origin should return prompt")
378 permRepo.AssertNotCalled(t, "Get")
379}
380
381func TestHandlePermissionUseCase_NonPersistableNotSaved(t *testing.T) {
382 ctx := testContext()
383 permRepo := repomocks.NewMockPermissionRepository(t)
384 dialog := portmocks.NewMockPermissionDialogPresenter(t)
385
386 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
387
388 // Display capture is not persistable per W3C spec
389 // A stored override may exist, so a Get call is expected before auto-allow.
390 // No Set call should happen even if we try to persist
391 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
392 Return(nil, nil)
393
394 allowed := false
395 callback := usecase.PermissionCallback{
396 Allow: func() { allowed = true },
397 Deny: func() {},
398 }
399
400 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
401 entity.PermissionTypeDisplay,
402 }, callback)
403
404 assert.True(t, allowed)
405 permRepo.AssertNotCalled(t, "Set")
406}
407
408func TestHandlePermissionUseCase_NoDialogPresenter(t *testing.T) {
409 ctx := testContext()
410 permRepo := repomocks.NewMockPermissionRepository(t)
411
412 // Create use case with nil dialog presenter
413 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
414
415 denied := false
416 callback := usecase.PermissionCallback{
417 Allow: func() {},
418 Deny: func() { denied = true },
419 }
420
421 // Mic request with no stored permission and no dialog should be denied
422 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
423 Return(nil, nil)
424
425 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
426 entity.PermissionTypeMicrophone,
427 }, callback)
428
429 assert.True(t, denied, "should deny when no dialog presenter is available")
430}
431
432func TestHandlePermissionUseCase_DisplayOverrideDenied(t *testing.T) {
433 ctx := testContext()
434 permRepo := repomocks.NewMockPermissionRepository(t)
435 dialog := portmocks.NewMockPermissionDialogPresenter(t)
436
437 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
438
439 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
440 Return(&entity.PermissionRecord{
441 Origin: "https://meet.example.com",
442 Type: entity.PermissionTypeDisplay,
443 Decision: entity.PermissionDenied,
444 }, nil)
445
446 denied := false
447 callback := usecase.PermissionCallback{
448 Allow: func() {},
449 Deny: func() { denied = true },
450 }
451
452 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
453 entity.PermissionTypeDisplay,
454 }, callback)
455
456 assert.True(t, denied, "display should be denied when manual override is denied")
457 dialog.AssertNotCalled(t, "ShowPermissionDialog")
458}
459
460func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) {
461 ctx := testContext()
462 permRepo := repomocks.NewMockPermissionRepository(t)
463 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
464
465 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
466 Run(func(_ context.Context, record *entity.PermissionRecord) {
467 assert.Equal(t, "https://app.zoom.us", record.Origin)
468 assert.Equal(t, entity.PermissionTypeMicrophone, record.Type)
469 assert.Equal(t, entity.PermissionGranted, record.Decision)
470 }).
471 Return(nil)
472
473 err := uc.SetManualPermissionDecision(
474 ctx,
475 "https://app.zoom.us",
476 entity.PermissionTypeMicrophone,
477 entity.PermissionGranted,
478 )
479 require.NoError(t, err)
480
481 permRepo.EXPECT().Get(mock.Anything, "https://app.zoom.us", entity.PermissionTypeMicrophone).
482 Return(&entity.PermissionRecord{
483 Origin: "https://app.zoom.us",
484 Type: entity.PermissionTypeMicrophone,
485 Decision: entity.PermissionGranted,
486 }, nil)
487
488 record, err := uc.GetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
489 require.NoError(t, err)
490 if assert.NotNil(t, record) {
491 assert.Equal(t, entity.PermissionGranted, record.Decision)
492 }
493
494 permRepo.EXPECT().Delete(mock.Anything, "https://app.zoom.us", entity.PermissionTypeMicrophone).
495 Return(nil)
496 err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
497 require.NoError(t, err)
498}
499
500func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) {
501 ctx := testContext()
502 permRepo := repomocks.NewMockPermissionRepository(t)
503 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
504
505 err := uc.SetManualPermissionDecision(
506 ctx,
507 "https://app.zoom.us",
508 entity.PermissionTypeDisplay,
509 entity.PermissionDenied,
510 )
511
512 require.Error(t, err)
513 require.ErrorContains(t, err, "permission type not persistable")
514 permRepo.AssertNotCalled(t, "Set")
515 permRepo.AssertNotCalled(t, "Delete")
516}
517
518func TestHandlePermissionUseCase_UsesInjectedLoggerFactory(t *testing.T) {
519 ctx := testContext()
520 permRepo := repomocks.NewMockPermissionRepository(t)
521 dialog := portmocks.NewMockPermissionDialogPresenter(t)
522
523 called := false
524 loggerFactory := func(ctx context.Context) *zerolog.Logger {
525 called = true
526 return zerolog.Ctx(ctx)
527 }
528
529 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, loggerFactory)
530 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, usecase.PermissionCallback{
531 Allow: func() {},
532 Deny: func() {},
533 })
534
535 assert.True(t, called)
536}