handle_permission_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close