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 "github.com/rs/zerolog"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16)
17
18func permissionLoggerFromContext(ctx context.Context) *zerolog.Logger {
19 return zerolog.Ctx(ctx)
20}
21
22func TestHandlePermissionUseCase_AutoAllowDisplayCapture(t *testing.T) {
23 ctx := testContext()
24 permRepo := portmocks.NewMockPermissionRepository(t)
25 dialog := portmocks.NewMockPermissionDialogPresenter(t)
26
27 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
28
29 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
30 Return(nil, nil)
31
32 allowed := false
33 denied := false
34 callback := usecase.PermissionCallback{
35 Allow: func() { allowed = true },
36 Deny: func() { denied = true },
37 }
38
39 // Display capture should be auto-allowed (portal handles UI)
40 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
41 entity.PermissionTypeDisplay,
42 }, nil, callback)
43
44 assert.True(t, allowed, "display capture should be auto-allowed")
45 assert.False(t, denied)
46 dialog.AssertNotCalled(t, "ShowPermissionDialog")
47}
48
49func TestHandlePermissionUseCase_AutoAllowDeviceInfo(t *testing.T) {
50 ctx := testContext()
51 permRepo := portmocks.NewMockPermissionRepository(t)
52 dialog := portmocks.NewMockPermissionDialogPresenter(t)
53
54 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
55
56 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDeviceInfo).
57 Return(nil, nil)
58
59 allowed := false
60 callback := usecase.PermissionCallback{
61 Allow: func() { allowed = true },
62 Deny: func() {},
63 }
64
65 // Device info should be auto-allowed
66 uc.HandlePermissionRequest(ctx, "https://example.com", []entity.PermissionType{
67 entity.PermissionTypeDeviceInfo,
68 }, nil, callback)
69
70 assert.True(t, allowed, "device info should be auto-allowed")
71}
72
73func TestHandlePermissionUseCase_StoredPermissionGranted(t *testing.T) {
74 ctx := testContext()
75 permRepo := portmocks.NewMockPermissionRepository(t)
76 dialog := portmocks.NewMockPermissionDialogPresenter(t)
77
78 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
79
80 // Return stored granted permission
81 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
82 Return(&entity.PermissionRecord{
83 Origin: "https://meet.example.com",
84 Type: entity.PermissionTypeMicrophone,
85 Decision: entity.PermissionGranted,
86 UpdatedAt: time.Now().Unix(),
87 }, nil)
88
89 allowed := false
90 callback := usecase.PermissionCallback{
91 Allow: func() { allowed = true },
92 Deny: func() {},
93 }
94
95 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
96 entity.PermissionTypeMicrophone,
97 }, nil, callback)
98
99 assert.True(t, allowed, "should use stored granted permission")
100 dialog.AssertNotCalled(t, "ShowPermissionDialog")
101}
102
103func TestHandlePermissionUseCase_StoredPermissionDenied(t *testing.T) {
104 ctx := testContext()
105 permRepo := portmocks.NewMockPermissionRepository(t)
106 dialog := portmocks.NewMockPermissionDialogPresenter(t)
107
108 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
109
110 // Return stored denied permission
111 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
112 Return(&entity.PermissionRecord{
113 Origin: "https://meet.example.com",
114 Type: entity.PermissionTypeCamera,
115 Decision: entity.PermissionDenied,
116 UpdatedAt: time.Now().Unix(),
117 }, nil)
118
119 denied := false
120 callback := usecase.PermissionCallback{
121 Allow: func() {},
122 Deny: func() { denied = true },
123 }
124
125 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
126 entity.PermissionTypeCamera,
127 }, nil, callback)
128
129 assert.True(t, denied, "should use stored denied permission")
130 dialog.AssertNotCalled(t, "ShowPermissionDialog")
131}
132
133func TestHandlePermissionUseCase_ShowDialogForPrompt(t *testing.T) {
134 ctx := testContext()
135 permRepo := portmocks.NewMockPermissionRepository(t)
136 dialog := portmocks.NewMockPermissionDialogPresenter(t)
137
138 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
139
140 // No stored permission (returns nil)
141 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
142 Return(nil, nil)
143
144 allowed := false
145 callback := usecase.PermissionCallback{
146 Allow: func() { allowed = true },
147 Deny: func() {},
148 }
149
150 // Dialog shows and user clicks Allow (not persistent)
151 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
152 entity.PermissionTypeMicrophone,
153 }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, callback func(port.PermissionDialogResult)) {
154 callback(port.PermissionDialogResult{Allowed: true, Persistent: false})
155 })
156
157 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
158 entity.PermissionTypeMicrophone,
159 }, nil, callback)
160
161 assert.True(t, allowed, "user allowed permission")
162 // Should not persist since Persistent=false
163 permRepo.AssertNotCalled(t, "Set")
164}
165
166func TestHandlePermissionUseCase_PersistAllowed(t *testing.T) {
167 ctx := testContext()
168 permRepo := portmocks.NewMockPermissionRepository(t)
169 dialog := portmocks.NewMockPermissionDialogPresenter(t)
170
171 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
172
173 // No stored permission
174 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
175 Return(nil, nil)
176
177 allowed := false
178 callback := usecase.PermissionCallback{
179 Allow: func() { allowed = true },
180 Deny: func() {},
181 }
182
183 // Dialog shows and user clicks "Always Allow"
184 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
185 entity.PermissionTypeMicrophone,
186 }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
187 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
188 })
189
190 // Should persist the granted permission
191 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
192 Run(func(_ context.Context, r *entity.PermissionRecord) {
193 assert.Equal(t, "https://meet.example.com", r.Origin)
194 assert.Equal(t, entity.PermissionTypeMicrophone, r.Type)
195 assert.Equal(t, entity.PermissionGranted, r.Decision)
196 }).Return(nil)
197
198 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
199 entity.PermissionTypeMicrophone,
200 }, nil, callback)
201
202 assert.True(t, allowed)
203}
204
205func TestHandlePermissionUseCase_PersistDenied(t *testing.T) {
206 ctx := testContext()
207 permRepo := portmocks.NewMockPermissionRepository(t)
208 dialog := portmocks.NewMockPermissionDialogPresenter(t)
209
210 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
211
212 // No stored permission
213 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
214 Return(nil, nil)
215
216 denied := false
217 callback := usecase.PermissionCallback{
218 Allow: func() {},
219 Deny: func() { denied = true },
220 }
221
222 // Dialog shows and user clicks "Always Deny"
223 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", []entity.PermissionType{
224 entity.PermissionTypeCamera,
225 }, mock.Anything, mock.Anything).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
226 // User denied but wants to persist this decision
227 cb(port.PermissionDialogResult{Allowed: false, Persistent: true})
228 })
229
230 // Should persist the denied permission
231 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
232 Run(func(_ context.Context, r *entity.PermissionRecord) {
233 assert.Equal(t, entity.PermissionDenied, r.Decision)
234 }).Return(nil)
235
236 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
237 entity.PermissionTypeCamera,
238 }, nil, callback)
239
240 assert.True(t, denied)
241}
242
243func TestHandlePermissionUseCase_EmptyOrigin(t *testing.T) {
244 ctx := testContext()
245 permRepo := portmocks.NewMockPermissionRepository(t)
246 dialog := portmocks.NewMockPermissionDialogPresenter(t)
247
248 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
249
250 denied := false
251 callback := usecase.PermissionCallback{
252 Allow: func() {},
253 Deny: func() { denied = true },
254 }
255
256 // Empty origin should be denied
257 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{
258 entity.PermissionTypeMicrophone,
259 }, nil, callback)
260
261 assert.True(t, denied, "empty origin should be denied")
262}
263
264func TestHandlePermissionUseCase_CombinedPermissions(t *testing.T) {
265 ctx := testContext()
266 permRepo := portmocks.NewMockPermissionRepository(t)
267 dialog := portmocks.NewMockPermissionDialogPresenter(t)
268
269 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
270
271 // Both mic and camera - one denied means whole request denied
272 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
273 Return(&entity.PermissionRecord{
274 Origin: "https://meet.example.com",
275 Type: entity.PermissionTypeMicrophone,
276 Decision: entity.PermissionGranted,
277 }, nil)
278
279 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
280 Return(&entity.PermissionRecord{
281 Origin: "https://meet.example.com",
282 Type: entity.PermissionTypeCamera,
283 Decision: entity.PermissionDenied,
284 }, nil)
285
286 denied := false
287 callback := usecase.PermissionCallback{
288 Allow: func() {},
289 Deny: func() { denied = true },
290 }
291
292 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
293 entity.PermissionTypeMicrophone,
294 entity.PermissionTypeCamera,
295 }, nil, callback)
296
297 assert.True(t, denied, "if any permission is denied, whole request is denied")
298}
299
300func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) {
301 ctx := testContext()
302 permRepo := portmocks.NewMockPermissionRepository(t)
303 dialog := portmocks.NewMockPermissionDialogPresenter(t)
304
305 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
306 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
307 Return(nil, nil)
308
309 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
310
311 assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted")
312}
313
314func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) {
315 ctx := testContext()
316 permRepo := portmocks.NewMockPermissionRepository(t)
317 dialog := portmocks.NewMockPermissionDialogPresenter(t)
318
319 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
320
321 permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
322 Return(&entity.PermissionRecord{
323 Origin: "https://example.com",
324 Type: entity.PermissionTypeDisplay,
325 Decision: entity.PermissionDenied,
326 }, nil)
327
328 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
329
330 assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow")
331}
332
333func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) {
334 ctx := testContext()
335 permRepo := portmocks.NewMockPermissionRepository(t)
336 dialog := portmocks.NewMockPermissionDialogPresenter(t)
337
338 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
339
340 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
341 Return(&entity.PermissionRecord{
342 Origin: "https://meet.example.com",
343 Type: entity.PermissionTypeMicrophone,
344 Decision: entity.PermissionDenied,
345 }, nil)
346
347 state := uc.QueryPermissionState(ctx, "https://meet.example.com", entity.PermissionTypeMicrophone)
348
349 assert.Equal(t, entity.PermissionDenied, state)
350}
351
352func TestHandlePermissionUseCase_QueryPermissionState_NoRecord(t *testing.T) {
353 ctx := testContext()
354 permRepo := portmocks.NewMockPermissionRepository(t)
355 dialog := portmocks.NewMockPermissionDialogPresenter(t)
356
357 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
358
359 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeCamera).
360 Return(nil, nil)
361
362 state := uc.QueryPermissionState(ctx, "https://meet.example.com", entity.PermissionTypeCamera)
363
364 assert.Equal(t, entity.PermissionPrompt, state, "no record should return prompt")
365}
366
367func TestHandlePermissionUseCase_QueryPermissionState_EmptyOriginReturnsPrompt(t *testing.T) {
368 ctx := testContext()
369 permRepo := portmocks.NewMockPermissionRepository(t)
370 dialog := portmocks.NewMockPermissionDialogPresenter(t)
371
372 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
373
374 state := uc.QueryPermissionState(ctx, "", entity.PermissionTypeDisplay)
375
376 assert.Equal(t, entity.PermissionPrompt, state, "empty origin should return prompt")
377 permRepo.AssertNotCalled(t, "Get")
378}
379
380func TestHandlePermissionUseCase_NonPersistableNotSaved(t *testing.T) {
381 ctx := testContext()
382 permRepo := portmocks.NewMockPermissionRepository(t)
383 dialog := portmocks.NewMockPermissionDialogPresenter(t)
384
385 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
386
387 // Display capture is not persistable per W3C spec
388 // A stored override may exist, so a Get call is expected before auto-allow.
389 // No Set call should happen even if we try to persist
390 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
391 Return(nil, nil)
392
393 allowed := false
394 callback := usecase.PermissionCallback{
395 Allow: func() { allowed = true },
396 Deny: func() {},
397 }
398
399 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
400 entity.PermissionTypeDisplay,
401 }, nil, callback)
402
403 assert.True(t, allowed)
404 permRepo.AssertNotCalled(t, "Set")
405}
406
407func TestHandlePermissionUseCase_NoDialogPresenter(t *testing.T) {
408 ctx := testContext()
409 permRepo := portmocks.NewMockPermissionRepository(t)
410
411 // Create use case with nil dialog presenter
412 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
413
414 denied := false
415 callback := usecase.PermissionCallback{
416 Allow: func() {},
417 Deny: func() { denied = true },
418 }
419
420 // Mic request with no stored permission and no dialog should be denied
421 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).
422 Return(nil, nil)
423
424 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
425 entity.PermissionTypeMicrophone,
426 }, nil, callback)
427
428 assert.True(t, denied, "should deny when no dialog presenter is available")
429}
430
431func TestHandlePermissionUseCase_DisplayOverrideDenied(t *testing.T) {
432 ctx := testContext()
433 permRepo := portmocks.NewMockPermissionRepository(t)
434 dialog := portmocks.NewMockPermissionDialogPresenter(t)
435
436 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
437
438 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).
439 Return(&entity.PermissionRecord{
440 Origin: "https://meet.example.com",
441 Type: entity.PermissionTypeDisplay,
442 Decision: entity.PermissionDenied,
443 }, nil)
444
445 denied := false
446 callback := usecase.PermissionCallback{
447 Allow: func() {},
448 Deny: func() { denied = true },
449 }
450
451 uc.HandlePermissionRequest(ctx, "https://meet.example.com", []entity.PermissionType{
452 entity.PermissionTypeDisplay,
453 }, nil, callback)
454
455 assert.True(t, denied, "display should be denied when manual override is denied")
456 dialog.AssertNotCalled(t, "ShowPermissionDialog")
457}
458
459func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) {
460 ctx := testContext()
461 permRepo := portmocks.NewMockPermissionRepository(t)
462 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
463
464 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
465 Run(func(_ context.Context, record *entity.PermissionRecord) {
466 assert.Equal(t, "https://app.zoom.us", record.Origin)
467 assert.Equal(t, entity.PermissionTypeMicrophone, record.Type)
468 assert.Equal(t, entity.PermissionGranted, record.Decision)
469 }).
470 Return(nil)
471
472 err := uc.SetManualPermissionDecision(
473 ctx,
474 "https://app.zoom.us",
475 entity.PermissionTypeMicrophone,
476 entity.PermissionGranted,
477 )
478 require.NoError(t, err)
479
480 permRepo.EXPECT().Get(mock.Anything, "https://app.zoom.us", entity.PermissionTypeMicrophone).
481 Return(&entity.PermissionRecord{
482 Origin: "https://app.zoom.us",
483 Type: entity.PermissionTypeMicrophone,
484 Decision: entity.PermissionGranted,
485 }, nil)
486
487 record, err := uc.GetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
488 require.NoError(t, err)
489 if assert.NotNil(t, record) {
490 assert.Equal(t, entity.PermissionGranted, record.Decision)
491 }
492
493 permRepo.EXPECT().Delete(mock.Anything, "https://app.zoom.us", entity.PermissionTypeMicrophone).
494 Return(nil)
495 err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
496 require.NoError(t, err)
497}
498
499func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) {
500 ctx := testContext()
501 permRepo := portmocks.NewMockPermissionRepository(t)
502 uc := usecase.NewHandlePermissionUseCase(permRepo, nil, permissionLoggerFromContext)
503
504 err := uc.SetManualPermissionDecision(
505 ctx,
506 "https://app.zoom.us",
507 entity.PermissionTypeDisplay,
508 entity.PermissionDenied,
509 )
510
511 require.Error(t, err)
512 require.ErrorContains(t, err, "permission type not persistable")
513 permRepo.AssertNotCalled(t, "Set")
514 permRepo.AssertNotCalled(t, "Delete")
515}
516
517func TestHandlePermissionUseCase_UsesInjectedLoggerFactory(t *testing.T) {
518 ctx := testContext()
519 permRepo := portmocks.NewMockPermissionRepository(t)
520 dialog := portmocks.NewMockPermissionDialogPresenter(t)
521
522 called := false
523 loggerFactory := func(ctx context.Context) *zerolog.Logger {
524 called = true
525 return zerolog.Ctx(ctx)
526 }
527
528 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, loggerFactory)
529 uc.HandlePermissionRequest(ctx, "", []entity.PermissionType{entity.PermissionTypeMicrophone}, nil, usecase.PermissionCallback{
530 Allow: func() {},
531 Deny: func() {},
532 })
533
534 assert.True(t, called)
535}
536
537func TestHandlePermissionUseCase_WebsiteDataAccess_ShowsDialogAndPersists(t *testing.T) {
538 ctx := testContext()
539 permRepo := portmocks.NewMockPermissionRepository(t)
540 dialog := portmocks.NewMockPermissionDialogPresenter(t)
541
542 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
543
544 // No stored permission
545 permRepo.EXPECT().Get(mock.Anything, "https://shop.example.com", entity.PermissionTypeWebsiteDataAccess).
546 Return(nil, nil)
547
548 allowed := false
549 callback := usecase.PermissionCallback{
550 Allow: func() { allowed = true },
551 Deny: func() {},
552 }
553
554 meta := entity.PermissionMetadata{
555 entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com",
556 entity.PermissionMetadataKeyCurrentDomain: "shop.example.com",
557 }
558
559 // Dialog shows and user clicks "Always Allow"
560 dialog.EXPECT().ShowPermissionDialog(
561 mock.Anything,
562 "https://shop.example.com",
563 []entity.PermissionType{entity.PermissionTypeWebsiteDataAccess},
564 meta,
565 mock.Anything,
566 ).Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
567 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
568 })
569
570 // Should persist the granted permission
571 permRepo.EXPECT().Set(mock.Anything, mock.AnythingOfType("*entity.PermissionRecord")).
572 Run(func(_ context.Context, r *entity.PermissionRecord) {
573 assert.Equal(t, "https://shop.example.com", r.Origin)
574 assert.Equal(t, entity.PermissionTypeWebsiteDataAccess, r.Type)
575 assert.Equal(t, entity.PermissionGranted, r.Decision)
576 }).Return(nil)
577
578 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
579 entity.PermissionTypeWebsiteDataAccess,
580 }, meta, callback)
581
582 assert.True(t, allowed)
583}
584
585func TestHandlePermissionUseCase_WebsiteDataAccess_UsesStoredGrant(t *testing.T) {
586 ctx := testContext()
587 permRepo := portmocks.NewMockPermissionRepository(t)
588 dialog := portmocks.NewMockPermissionDialogPresenter(t)
589
590 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
591
592 // Stored granted permission
593 permRepo.EXPECT().Get(mock.Anything, "https://shop.example.com", entity.PermissionTypeWebsiteDataAccess).
594 Return(&entity.PermissionRecord{
595 Origin: "https://shop.example.com",
596 Type: entity.PermissionTypeWebsiteDataAccess,
597 Decision: entity.PermissionGranted,
598 }, nil)
599
600 allowed := false
601 callback := usecase.PermissionCallback{
602 Allow: func() { allowed = true },
603 Deny: func() {},
604 }
605
606 uc.HandlePermissionRequest(ctx, "https://shop.example.com", []entity.PermissionType{
607 entity.PermissionTypeWebsiteDataAccess,
608 }, entity.PermissionMetadata{entity.PermissionMetadataKeyRequestingDomain: "accounts.google.com", entity.PermissionMetadataKeyCurrentDomain: "shop.example.com"}, callback)
609
610 assert.True(t, allowed, "should use stored granted permission without dialog")
611 dialog.AssertNotCalled(t, "ShowPermissionDialog")
612}
613
614var unmediatedDisplayMetadata = entity.PermissionMetadata{
615 entity.PermissionMetadataKeyUnmediatedCapture: entity.PermissionMetadataValueTrue,
616}
617
618func TestHandlePermissionUseCase_UnmediatedDisplayShowsDialog(t *testing.T) {
619 tests := []struct {
620 name string
621 result port.PermissionDialogResult
622 wantAllow bool
623 wantDenied bool
624 }{
625 {"allow", port.PermissionDialogResult{Allowed: true}, true, false},
626 {"always allow is never persisted", port.PermissionDialogResult{Allowed: true, Persistent: true}, true, false},
627 {"deny", port.PermissionDialogResult{Allowed: false}, false, true},
628 {"always deny is never persisted", port.PermissionDialogResult{Allowed: false, Persistent: true}, false, true},
629 }
630 for _, tt := range tests {
631 t.Run(tt.name, func(t *testing.T) {
632 permRepo := portmocks.NewMockPermissionRepository(t)
633 dialog := portmocks.NewMockPermissionDialogPresenter(t)
634 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
635
636 var gotMeta entity.PermissionMetadata
637 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com",
638 []entity.PermissionType{entity.PermissionTypeDisplay}, mock.Anything, mock.Anything).
639 Run(func(_ context.Context, _ string, _ []entity.PermissionType, meta entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
640 gotMeta = meta
641 cb(tt.result)
642 })
643
644 allowed, denied := false, false
645 uc.HandlePermissionRequest(testContext(), "https://meet.example.com",
646 []entity.PermissionType{entity.PermissionTypeDisplay}, unmediatedDisplayMetadata,
647 usecase.PermissionCallback{Allow: func() { allowed = true }, Deny: func() { denied = true }})
648
649 assert.Equal(t, tt.wantAllow, allowed)
650 assert.Equal(t, tt.wantDenied, denied)
651 assert.True(t, gotMeta.IsUnmediatedCapture(), "metadata must reach the dialog")
652 permRepo.AssertNotCalled(t, "Set")
653 })
654 }
655}
656
657func TestHandlePermissionUseCase_UnmediatedDisplayIgnoresStoredDecision(t *testing.T) {
658 permRepo := portmocks.NewMockPermissionRepository(t)
659 dialog := portmocks.NewMockPermissionDialogPresenter(t)
660 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
661
662 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com",
663 []entity.PermissionType{entity.PermissionTypeDisplay}, mock.Anything, mock.Anything).
664 Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
665 cb(port.PermissionDialogResult{Allowed: false})
666 }).Once()
667
668 uc.HandlePermissionRequest(testContext(), "https://meet.example.com",
669 []entity.PermissionType{entity.PermissionTypeDisplay}, unmediatedDisplayMetadata,
670 usecase.PermissionCallback{Allow: func() { t.Fatal("must not auto-allow") }, Deny: func() {}})
671
672 // Display is never looked up, so no stored decision can auto-allow or auto-deny it.
673 permRepo.AssertNotCalled(t, "Get", mock.Anything, mock.Anything, entity.PermissionTypeDisplay)
674}
675
676func TestHandlePermissionUseCase_UnmediatedDisplayWithMicrophoneDoesNotPersistDisplay(t *testing.T) {
677 permRepo := portmocks.NewMockPermissionRepository(t)
678 dialog := portmocks.NewMockPermissionDialogPresenter(t)
679 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
680
681 types := []entity.PermissionType{entity.PermissionTypeMicrophone, entity.PermissionTypeDisplay}
682 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeMicrophone).Return(nil, nil)
683 dialog.EXPECT().ShowPermissionDialog(mock.Anything, "https://meet.example.com", types, mock.Anything, mock.Anything).
684 Run(func(_ context.Context, _ string, _ []entity.PermissionType, _ entity.PermissionMetadata, cb func(port.PermissionDialogResult)) {
685 cb(port.PermissionDialogResult{Allowed: true, Persistent: true})
686 })
687 permRepo.EXPECT().Set(mock.Anything, mock.MatchedBy(func(r *entity.PermissionRecord) bool {
688 return r.Type == entity.PermissionTypeMicrophone && r.Decision == entity.PermissionGranted
689 })).Return(nil).Once()
690
691 allowed := false
692 uc.HandlePermissionRequest(testContext(), "https://meet.example.com", types, unmediatedDisplayMetadata,
693 usecase.PermissionCallback{Allow: func() { allowed = true }, Deny: func() {}})
694
695 assert.True(t, allowed)
696}
697
698func TestHandlePermissionUseCase_DisplayWithoutFlagStillAutoAllowed(t *testing.T) {
699 permRepo := portmocks.NewMockPermissionRepository(t)
700 dialog := portmocks.NewMockPermissionDialogPresenter(t)
701 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog, permissionLoggerFromContext)
702
703 permRepo.EXPECT().Get(mock.Anything, "https://meet.example.com", entity.PermissionTypeDisplay).Return(nil, nil)
704
705 allowed := false
706 uc.HandlePermissionRequest(testContext(), "https://meet.example.com",
707 []entity.PermissionType{entity.PermissionTypeDisplay}, entity.PermissionMetadata{},
708 usecase.PermissionCallback{Allow: func() { allowed = true }, Deny: func() {}})
709
710 assert.True(t, allowed)
711 dialog.AssertNotCalled(t, "ShowPermissionDialog")
712 permRepo.AssertNotCalled(t, "Set")
713}