fix(permission): honor overrides and enforce persistability
QueryPermissionState now checks stored decisions before auto-allow defaults so manual denies are respected, and manual decision writes reject non-persistable permission types. Also moves permission repository contract to application port, updates docs for removed revoke API, and hardens IPv6 origin plus GObject helper safety/logging.
- commit
- 2595e459f35d8dd63260819a01a8827b6508a7f3
- author
- brice · · 2026-02-08 10:10 UTC
- parent
- d037679
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +82 -24
M cmd/dumber/main.go +1 -1
| @@ -388,7 +388,7 @@ type repositories struct { | |||
| 388 | 388 | folder repository.FolderRepository | |
| 389 | 389 | tag repository.TagRepository | |
| 390 | 390 | zoom repository.ZoomRepository | |
| 391 | - | permission repository.PermissionRepository | |
| 391 | + | permission port.PermissionRepository | |
| 392 | 392 | session repository.SessionRepository | |
| 393 | 393 | sessionState repository.SessionStateRepository | |
| 394 | 394 | } | |
| @@ -388,7 +388,7 @@ type repositories struct { | |||||
| 388 | folder repository.FolderRepository | 388 | folder repository.FolderRepository | ||
| 389 | tag repository.TagRepository | 389 | tag repository.TagRepository | ||
| 390 | zoom repository.ZoomRepository | 390 | zoom repository.ZoomRepository | ||
| 391 | - | permission repository.PermissionRepository | 391 | + | permission port.PermissionRepository |
| 392 | session repository.SessionRepository | 392 | session repository.SessionRepository | ||
| 393 | sessionState repository.SessionStateRepository | 393 | sessionState repository.SessionStateRepository | ||
| 394 | } | 394 | } | ||
M docs/reference/permissions.md +1 -1
| @@ -58,7 +58,7 @@ The permission system follows W3C specifications: | |||
| 58 | 58 | ||
| 59 | 59 | Currently, stored permissions can only be cleared by: | |
| 60 | 60 | ||
| 61 | - | 1. **Site-specific reset**: The site can use `navigator.permissions.revoke()` (if supported) | |
| 61 | + | 1. **Site-specific reset**: `navigator.permissions.revoke()` was removed from the Permissions API and is not supported in modern browsers; direct users to browser/site permission settings (or account-level controls when available) to reset access. | |
| 62 | 62 | 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions) | |
| 63 | 63 | ||
| 64 | 64 | A permission management UI is planned for a future release. | |
| @@ -58,7 +58,7 @@ The permission system follows W3C specifications: | |||||
| 58 | 58 | ||||
| 59 | Currently, stored permissions can only be cleared by: | 59 | Currently, stored permissions can only be cleared by: | ||
| 60 | 60 | ||||
| 61 | - | 1. **Site-specific reset**: The site can use `navigator.permissions.revoke()` (if supported) | 61 | + | 1. **Site-specific reset**: `navigator.permissions.revoke()` was removed from the Permissions API and is not supported in modern browsers; direct users to browser/site permission settings (or account-level controls when available) to reset access. |
| 62 | 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions) | 62 | 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions) | ||
| 63 | 63 | ||||
| 64 | A permission management UI is planned for a future release. | 64 | A permission management UI is planned for a future release. | ||
R internal/domain/repository/permission.go → internal/application/port/permission_repository.go +1 -1 renamed, 97% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package repository | |
| 1 | + | package port | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package repository | 1 | + | package port |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
M internal/application/usecase/handle_permission.go +16 -15
| @@ -9,7 +9,6 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/application/port" | |
| 11 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | - | "github.com/bnema/dumber/internal/domain/repository" | |
| 13 | 12 | "github.com/bnema/dumber/internal/logging" | |
| 14 | 13 | ) | |
| 15 | 14 | ||
| @@ -25,14 +24,14 @@ type PermissionCallback struct { | |||
| 25 | 24 | // - Device enumeration: auto-allow (low risk) | |
| 26 | 25 | // - Mic/Camera: check stored → dialog → persist if "Always" | |
| 27 | 26 | type HandlePermissionUseCase struct { | |
| 28 | - | permRepo repository.PermissionRepository | |
| 27 | + | permRepo port.PermissionRepository | |
| 29 | 28 | dialog port.PermissionDialogPresenter | |
| 30 | 29 | dialogMu sync.RWMutex | |
| 31 | 30 | } | |
| 32 | 31 | ||
| 33 | 32 | // NewHandlePermissionUseCase creates a new permission handling use case. | |
| 34 | 33 | func NewHandlePermissionUseCase( | |
| 35 | - | permRepo repository.PermissionRepository, | |
| 34 | + | permRepo port.PermissionRepository, | |
| 36 | 35 | dialog port.PermissionDialogPresenter, | |
| 37 | 36 | ) *HandlePermissionUseCase { | |
| 38 | 37 | return &HandlePermissionUseCase{ | |
| @@ -135,26 +134,25 @@ func (uc *HandlePermissionUseCase) QueryPermissionState( | |||
| 135 | 134 | Str("type", string(permType)). | |
| 136 | 135 | Logger() | |
| 137 | 136 | ||
| 138 | - | // Auto-allow types | |
| 139 | - | if entity.IsAutoAllow(permType) { | |
| 140 | - | log.Debug().Msg("query: auto-allow type returns granted") | |
| 141 | - | return entity.PermissionGranted | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | // Check stored permission | |
| 137 | + | // Check stored permission first (including manual overrides on auto-allow types). | |
| 145 | 138 | record, err := uc.permRepo.Get(ctx, origin, permType) | |
| 146 | 139 | if err != nil { | |
| 147 | 140 | log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt") | |
| 148 | 141 | return entity.PermissionPrompt | |
| 149 | 142 | } | |
| 150 | 143 | ||
| 151 | - | if record == nil { | |
| 152 | - | log.Debug().Msg("query: no stored permission, returning prompt") | |
| 153 | - | return entity.PermissionPrompt | |
| 144 | + | if record != nil { | |
| 145 | + | log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission") | |
| 146 | + | return record.Decision | |
| 154 | 147 | } | |
| 155 | 148 | ||
| 156 | - | log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission") | |
| 157 | - | return record.Decision | |
| 149 | + | if entity.IsAutoAllow(permType) { | |
| 150 | + | log.Debug().Msg("query: no stored permission for auto-allow type, returning granted") | |
| 151 | + | return entity.PermissionGranted | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | log.Debug().Msg("query: no stored permission, returning prompt") | |
| 155 | + | return entity.PermissionPrompt | |
| 158 | 156 | } | |
| 159 | 157 | ||
| 160 | 158 | // isAutoAllowSet returns true if all permission types in the set are auto-allow. | |
| @@ -318,6 +316,9 @@ func (uc *HandlePermissionUseCase) SetManualPermissionDecision( | |||
| 318 | 316 | if origin == "" { | |
| 319 | 317 | return errors.New("origin is required") | |
| 320 | 318 | } | |
| 319 | + | if !entity.CanPersist(permType) { | |
| 320 | + | return errors.New("permission type not persistable") | |
| 321 | + | } | |
| 321 | 322 | if decision == entity.PermissionPrompt { | |
| 322 | 323 | return uc.permRepo.Delete(ctx, origin, permType) | |
| 323 | 324 | } | |
| @@ -9,7 +9,6 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | "github.com/bnema/dumber/internal/application/port" | 10 | "github.com/bnema/dumber/internal/application/port" | ||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 12 | - | "github.com/bnema/dumber/internal/domain/repository" | |||
| 13 | "github.com/bnema/dumber/internal/logging" | 12 | "github.com/bnema/dumber/internal/logging" | ||
| 14 | ) | 13 | ) | ||
| 15 | 14 | ||||
| @@ -25,14 +24,14 @@ type PermissionCallback struct { | |||||
| 25 | // - Device enumeration: auto-allow (low risk) | 24 | // - Device enumeration: auto-allow (low risk) | ||
| 26 | // - Mic/Camera: check stored → dialog → persist if "Always" | 25 | // - Mic/Camera: check stored → dialog → persist if "Always" | ||
| 27 | type HandlePermissionUseCase struct { | 26 | type HandlePermissionUseCase struct { | ||
| 28 | - | permRepo repository.PermissionRepository | 27 | + | permRepo port.PermissionRepository |
| 29 | dialog port.PermissionDialogPresenter | 28 | dialog port.PermissionDialogPresenter | ||
| 30 | dialogMu sync.RWMutex | 29 | dialogMu sync.RWMutex | ||
| 31 | } | 30 | } | ||
| 32 | 31 | ||||
| 33 | // NewHandlePermissionUseCase creates a new permission handling use case. | 32 | // NewHandlePermissionUseCase creates a new permission handling use case. | ||
| 34 | func NewHandlePermissionUseCase( | 33 | func NewHandlePermissionUseCase( | ||
| 35 | - | permRepo repository.PermissionRepository, | 34 | + | permRepo port.PermissionRepository, |
| 36 | dialog port.PermissionDialogPresenter, | 35 | dialog port.PermissionDialogPresenter, | ||
| 37 | ) *HandlePermissionUseCase { | 36 | ) *HandlePermissionUseCase { | ||
| 38 | return &HandlePermissionUseCase{ | 37 | return &HandlePermissionUseCase{ | ||
| @@ -135,26 +134,25 @@ func (uc *HandlePermissionUseCase) QueryPermissionState( | |||||
| 135 | Str("type", string(permType)). | 134 | Str("type", string(permType)). | ||
| 136 | Logger() | 135 | Logger() | ||
| 137 | 136 | ||||
| 138 | - | // Auto-allow types | 137 | + | // Check stored permission first (including manual overrides on auto-allow types). |
| 139 | - | if entity.IsAutoAllow(permType) { | |||
| 140 | - | log.Debug().Msg("query: auto-allow type returns granted") | |||
| 141 | - | return entity.PermissionGranted | |||
| 142 | - | } | |||
| 143 | - | ||||
| 144 | - | // Check stored permission | |||
| 145 | record, err := uc.permRepo.Get(ctx, origin, permType) | 138 | record, err := uc.permRepo.Get(ctx, origin, permType) | ||
| 146 | if err != nil { | 139 | if err != nil { | ||
| 147 | log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt") | 140 | log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt") | ||
| 148 | return entity.PermissionPrompt | 141 | return entity.PermissionPrompt | ||
| 149 | } | 142 | } | ||
| 150 | 143 | ||||
| 151 | - | if record == nil { | 144 | + | if record != nil { |
| 152 | - | log.Debug().Msg("query: no stored permission, returning prompt") | 145 | + | log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission") |
| 153 | - | return entity.PermissionPrompt | 146 | + | return record.Decision |
| 154 | } | 147 | } | ||
| 155 | 148 | ||||
| 156 | - | log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission") | 149 | + | if entity.IsAutoAllow(permType) { |
| 157 | - | return record.Decision | 150 | + | log.Debug().Msg("query: no stored permission for auto-allow type, returning granted") |
| 151 | + | return entity.PermissionGranted | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | log.Debug().Msg("query: no stored permission, returning prompt") | |||
| 155 | + | return entity.PermissionPrompt | |||
| 158 | } | 156 | } | ||
| 159 | 157 | ||||
| 160 | // isAutoAllowSet returns true if all permission types in the set are auto-allow. | 158 | // isAutoAllowSet returns true if all permission types in the set are auto-allow. | ||
| @@ -318,6 +316,9 @@ func (uc *HandlePermissionUseCase) SetManualPermissionDecision( | |||||
| 318 | if origin == "" { | 316 | if origin == "" { | ||
| 319 | return errors.New("origin is required") | 317 | return errors.New("origin is required") | ||
| 320 | } | 318 | } | ||
| 319 | + | if !entity.CanPersist(permType) { | |||
| 320 | + | return errors.New("permission type not persistable") | |||
| 321 | + | } | |||
| 321 | if decision == entity.PermissionPrompt { | 322 | if decision == entity.PermissionPrompt { | ||
| 322 | return uc.permRepo.Delete(ctx, origin, permType) | 323 | return uc.permRepo.Delete(ctx, origin, permType) | ||
| 323 | } | 324 | } | ||
M internal/application/usecase/handle_permission_test.go +39 -0
| @@ -299,12 +299,33 @@ func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) { | |||
| 299 | 299 | dialog := portmocks.NewMockPermissionDialogPresenter(t) | |
| 300 | 300 | ||
| 301 | 301 | uc := usecase.NewHandlePermissionUseCase(permRepo, dialog) | |
| 302 | + | permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay). | |
| 303 | + | Return(nil, nil) | |
| 302 | 304 | ||
| 303 | 305 | state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay) | |
| 304 | 306 | ||
| 305 | 307 | assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted") | |
| 306 | 308 | } | |
| 307 | 309 | ||
| 310 | + | func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) { | |
| 311 | + | ctx := testContext() | |
| 312 | + | permRepo := repomocks.NewMockPermissionRepository(t) | |
| 313 | + | dialog := portmocks.NewMockPermissionDialogPresenter(t) | |
| 314 | + | ||
| 315 | + | uc := usecase.NewHandlePermissionUseCase(permRepo, dialog) | |
| 316 | + | ||
| 317 | + | permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay). | |
| 318 | + | Return(&entity.PermissionRecord{ | |
| 319 | + | Origin: "https://example.com", | |
| 320 | + | Type: entity.PermissionTypeDisplay, | |
| 321 | + | Decision: entity.PermissionDenied, | |
| 322 | + | }, nil) | |
| 323 | + | ||
| 324 | + | state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay) | |
| 325 | + | ||
| 326 | + | assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow") | |
| 327 | + | } | |
| 328 | + | ||
| 308 | 329 | func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) { | |
| 309 | 330 | ctx := testContext() | |
| 310 | 331 | permRepo := repomocks.NewMockPermissionRepository(t) | |
| @@ -457,3 +478,21 @@ func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) { | |||
| 457 | 478 | err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone) | |
| 458 | 479 | require.NoError(t, err) | |
| 459 | 480 | } | |
| 481 | + | ||
| 482 | + | func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) { | |
| 483 | + | ctx := testContext() | |
| 484 | + | permRepo := repomocks.NewMockPermissionRepository(t) | |
| 485 | + | uc := usecase.NewHandlePermissionUseCase(permRepo, nil) | |
| 486 | + | ||
| 487 | + | err := uc.SetManualPermissionDecision( | |
| 488 | + | ctx, | |
| 489 | + | "https://app.zoom.us", | |
| 490 | + | entity.PermissionTypeDisplay, | |
| 491 | + | entity.PermissionDenied, | |
| 492 | + | ) | |
| 493 | + | ||
| 494 | + | require.Error(t, err) | |
| 495 | + | require.ErrorContains(t, err, "permission type not persistable") | |
| 496 | + | permRepo.AssertNotCalled(t, "Set") | |
| 497 | + | permRepo.AssertNotCalled(t, "Delete") | |
| 498 | + | } | |
| @@ -299,12 +299,33 @@ func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) { | |||||
| 299 | dialog := portmocks.NewMockPermissionDialogPresenter(t) | 299 | dialog := portmocks.NewMockPermissionDialogPresenter(t) | ||
| 300 | 300 | ||||
| 301 | uc := usecase.NewHandlePermissionUseCase(permRepo, dialog) | 301 | uc := usecase.NewHandlePermissionUseCase(permRepo, dialog) | ||
| 302 | + | permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay). | |||
| 303 | + | Return(nil, nil) | |||
| 302 | 304 | ||||
| 303 | state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay) | 305 | state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay) | ||
| 304 | 306 | ||||
| 305 | assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted") | 307 | assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted") | ||
| 306 | } | 308 | } | ||
| 307 | 309 | ||||
| 310 | + | func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) { | |||
| 311 | + | ctx := testContext() | |||
| 312 | + | permRepo := repomocks.NewMockPermissionRepository(t) | |||
| 313 | + | dialog := portmocks.NewMockPermissionDialogPresenter(t) | |||
| 314 | + | ||||
| 315 | + | uc := usecase.NewHandlePermissionUseCase(permRepo, dialog) | |||
| 316 | + | ||||
| 317 | + | permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay). | |||
| 318 | + | Return(&entity.PermissionRecord{ | |||
| 319 | + | Origin: "https://example.com", | |||
| 320 | + | Type: entity.PermissionTypeDisplay, | |||
| 321 | + | Decision: entity.PermissionDenied, | |||
| 322 | + | }, nil) | |||
| 323 | + | ||||
| 324 | + | state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay) | |||
| 325 | + | ||||
| 326 | + | assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow") | |||
| 327 | + | } | |||
| 328 | + | ||||
| 308 | func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) { | 329 | func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) { | ||
| 309 | ctx := testContext() | 330 | ctx := testContext() | ||
| 310 | permRepo := repomocks.NewMockPermissionRepository(t) | 331 | permRepo := repomocks.NewMockPermissionRepository(t) | ||
| @@ -457,3 +478,21 @@ func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) { | |||||
| 457 | err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone) | 478 | err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone) | ||
| 458 | require.NoError(t, err) | 479 | require.NoError(t, err) | ||
| 459 | } | 480 | } | ||
| 481 | + | ||||
| 482 | + | func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) { | |||
| 483 | + | ctx := testContext() | |||
| 484 | + | permRepo := repomocks.NewMockPermissionRepository(t) | |||
| 485 | + | uc := usecase.NewHandlePermissionUseCase(permRepo, nil) | |||
| 486 | + | ||||
| 487 | + | err := uc.SetManualPermissionDecision( | |||
| 488 | + | ctx, | |||
| 489 | + | "https://app.zoom.us", | |||
| 490 | + | entity.PermissionTypeDisplay, | |||
| 491 | + | entity.PermissionDenied, | |||
| 492 | + | ) | |||
| 493 | + | ||||
| 494 | + | require.Error(t, err) | |||
| 495 | + | require.ErrorContains(t, err, "permission type not persistable") | |||
| 496 | + | permRepo.AssertNotCalled(t, "Set") | |||
| 497 | + | permRepo.AssertNotCalled(t, "Delete") | |||
| 498 | + | } | |||
M internal/domain/url/normalize.go +3 -0
| @@ -222,6 +222,9 @@ func ExtractOrigin(uri string) (string, error) { | |||
| 222 | 222 | // Canonicalize: lowercase scheme and hostname | |
| 223 | 223 | scheme := strings.ToLower(parsed.Scheme) | |
| 224 | 224 | hostname := strings.ToLower(parsed.Hostname()) | |
| 225 | + | if strings.Contains(hostname, ":") && !strings.HasPrefix(hostname, "[") { | |
| 226 | + | hostname = "[" + hostname + "]" | |
| 227 | + | } | |
| 225 | 228 | ||
| 226 | 229 | // Determine if we should include the port | |
| 227 | 230 | port := parsed.Port() | |
| @@ -222,6 +222,9 @@ func ExtractOrigin(uri string) (string, error) { | |||||
| 222 | // Canonicalize: lowercase scheme and hostname | 222 | // Canonicalize: lowercase scheme and hostname | ||
| 223 | scheme := strings.ToLower(parsed.Scheme) | 223 | scheme := strings.ToLower(parsed.Scheme) | ||
| 224 | hostname := strings.ToLower(parsed.Hostname()) | 224 | hostname := strings.ToLower(parsed.Hostname()) | ||
| 225 | + | if strings.Contains(hostname, ":") && !strings.HasPrefix(hostname, "[") { | |||
| 226 | + | hostname = "[" + hostname + "]" | |||
| 227 | + | } | |||
| 225 | 228 | ||||
| 226 | // Determine if we should include the port | 229 | // Determine if we should include the port | ||
| 227 | port := parsed.Port() | 230 | port := parsed.Port() | ||
M internal/domain/url/normalize_test.go +5 -0
| @@ -561,6 +561,11 @@ func TestExtractOrigin_ValidURI(t *testing.T) { | |||
| 561 | 561 | uri: "HTTPS://EXAMPLE.COM:443/path", | |
| 562 | 562 | expected: "https://example.com", | |
| 563 | 563 | }, | |
| 564 | + | { | |
| 565 | + | name: "ipv6 with explicit port", | |
| 566 | + | uri: "https://[::1]:8443/path", | |
| 567 | + | expected: "https://[::1]:8443", | |
| 568 | + | }, | |
| 564 | 569 | } | |
| 565 | 570 | ||
| 566 | 571 | for _, tt := range tests { | |
| @@ -561,6 +561,11 @@ func TestExtractOrigin_ValidURI(t *testing.T) { | |||||
| 561 | uri: "HTTPS://EXAMPLE.COM:443/path", | 561 | uri: "HTTPS://EXAMPLE.COM:443/path", | ||
| 562 | expected: "https://example.com", | 562 | expected: "https://example.com", | ||
| 563 | }, | 563 | }, | ||
| 564 | + | { | |||
| 565 | + | name: "ipv6 with explicit port", | |||
| 566 | + | uri: "https://[::1]:8443/path", | |||
| 567 | + | expected: "https://[::1]:8443", | |||
| 568 | + | }, | |||
| 564 | } | 569 | } | ||
| 565 | 570 | ||||
| 566 | for _, tt := range tests { | 571 | for _, tt := range tests { | ||
M internal/infrastructure/persistence/sqlite/lazy_repos.go +3 -3
| @@ -43,13 +43,13 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History | |||
| 43 | 43 | // LazyPermissionRepository wraps a permission repository with lazy database initialization. | |
| 44 | 44 | type LazyPermissionRepository struct { | |
| 45 | 45 | provider port.DatabaseProvider | |
| 46 | - | repo repository.PermissionRepository | |
| 46 | + | repo port.PermissionRepository | |
| 47 | 47 | once sync.Once | |
| 48 | 48 | initErr error | |
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | 51 | // NewLazyPermissionRepository creates a lazy-loading permission repository. | |
| 52 | - | func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository { | |
| 52 | + | func NewLazyPermissionRepository(provider port.DatabaseProvider) port.PermissionRepository { | |
| 53 | 53 | return &LazyPermissionRepository{provider: provider} | |
| 54 | 54 | } | |
| 55 | 55 | ||
| @@ -753,7 +753,7 @@ type LazyRepositories struct { | |||
| 753 | 753 | Session repository.SessionRepository | |
| 754 | 754 | SessionState repository.SessionStateRepository | |
| 755 | 755 | Filter repository.ContentWhitelistRepository | |
| 756 | - | Permission repository.PermissionRepository | |
| 756 | + | Permission port.PermissionRepository | |
| 757 | 757 | } | |
| 758 | 758 | ||
| 759 | 759 | // NewLazyRepositories creates all lazy repositories from a database provider. | |
| @@ -43,13 +43,13 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History | |||||
| 43 | // LazyPermissionRepository wraps a permission repository with lazy database initialization. | 43 | // LazyPermissionRepository wraps a permission repository with lazy database initialization. | ||
| 44 | type LazyPermissionRepository struct { | 44 | type LazyPermissionRepository struct { | ||
| 45 | provider port.DatabaseProvider | 45 | provider port.DatabaseProvider | ||
| 46 | - | repo repository.PermissionRepository | 46 | + | repo port.PermissionRepository |
| 47 | once sync.Once | 47 | once sync.Once | ||
| 48 | initErr error | 48 | initErr error | ||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | // NewLazyPermissionRepository creates a lazy-loading permission repository. | 51 | // NewLazyPermissionRepository creates a lazy-loading permission repository. | ||
| 52 | - | func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository { | 52 | + | func NewLazyPermissionRepository(provider port.DatabaseProvider) port.PermissionRepository { |
| 53 | return &LazyPermissionRepository{provider: provider} | 53 | return &LazyPermissionRepository{provider: provider} | ||
| 54 | } | 54 | } | ||
| 55 | 55 | ||||
| @@ -753,7 +753,7 @@ type LazyRepositories struct { | |||||
| 753 | Session repository.SessionRepository | 753 | Session repository.SessionRepository | ||
| 754 | SessionState repository.SessionStateRepository | 754 | SessionState repository.SessionStateRepository | ||
| 755 | Filter repository.ContentWhitelistRepository | 755 | Filter repository.ContentWhitelistRepository | ||
| 756 | - | Permission repository.PermissionRepository | 756 | + | Permission port.PermissionRepository |
| 757 | } | 757 | } | ||
| 758 | 758 | ||||
| 759 | // NewLazyRepositories creates all lazy repositories from a database provider. | 759 | // NewLazyRepositories creates all lazy repositories from a database provider. | ||
M internal/infrastructure/persistence/sqlite/permission_repo.go +2 -2
| @@ -5,8 +5,8 @@ import ( | |||
| 5 | 5 | "database/sql" | |
| 6 | 6 | "errors" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | - | "github.com/bnema/dumber/internal/domain/repository" | |
| 10 | 10 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |
| 11 | 11 | "github.com/bnema/dumber/internal/logging" | |
| 12 | 12 | ) | |
| @@ -16,7 +16,7 @@ type permissionRepo struct { | |||
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | 18 | // NewPermissionRepository creates a new SQLite-backed permission repository. | |
| 19 | - | func NewPermissionRepository(db *sql.DB) repository.PermissionRepository { | |
| 19 | + | func NewPermissionRepository(db *sql.DB) port.PermissionRepository { | |
| 20 | 20 | return &permissionRepo{queries: sqlc.New(db)} | |
| 21 | 21 | } | |
| 22 | 22 | ||
| @@ -5,8 +5,8 @@ import ( | |||||
| 5 | "database/sql" | 5 | "database/sql" | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 9 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | - | "github.com/bnema/dumber/internal/domain/repository" | |||
| 10 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | 10 | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | ||
| 11 | "github.com/bnema/dumber/internal/logging" | 11 | "github.com/bnema/dumber/internal/logging" | ||
| 12 | ) | 12 | ) | ||
| @@ -16,7 +16,7 @@ type permissionRepo struct { | |||||
| 16 | } | 16 | } | ||
| 17 | 17 | ||||
| 18 | // NewPermissionRepository creates a new SQLite-backed permission repository. | 18 | // NewPermissionRepository creates a new SQLite-backed permission repository. | ||
| 19 | - | func NewPermissionRepository(db *sql.DB) repository.PermissionRepository { | 19 | + | func NewPermissionRepository(db *sql.DB) port.PermissionRepository { |
| 20 | return &permissionRepo{queries: sqlc.New(db)} | 20 | return &permissionRepo{queries: sqlc.New(db)} | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
M internal/infrastructure/webkit/gobject_type_helpers.go +10 -0
| @@ -45,6 +45,10 @@ func permissionRequestTypeName(ctx context.Context, instancePtr uintptr) string | |||
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | 47 | func loadGObjectTypeFns(ctx context.Context) { | |
| 48 | + | if ctx == nil { | |
| 49 | + | ctx = context.Background() | |
| 50 | + | } | |
| 51 | + | ||
| 48 | 52 | loadGObjectTypeFnsOnce.Do(func() { | |
| 49 | 53 | log := logging.FromContext(ctx) | |
| 50 | 54 | libs := make([]uintptr, 0, 2) | |
| @@ -62,6 +66,12 @@ func loadGObjectTypeFns(ctx context.Context) { | |||
| 62 | 66 | } | |
| 63 | 67 | ||
| 64 | 68 | core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a") | |
| 69 | + | if typeCheckInstanceIsA == nil { | |
| 70 | + | log.Debug().Msg("failed to register g_type_check_instance_is_a") | |
| 71 | + | } | |
| 65 | 72 | core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance") | |
| 73 | + | if typeNameFromInstance == nil { | |
| 74 | + | log.Debug().Msg("failed to register g_type_name_from_instance") | |
| 75 | + | } | |
| 66 | 76 | }) | |
| 67 | 77 | } | |
| @@ -45,6 +45,10 @@ func permissionRequestTypeName(ctx context.Context, instancePtr uintptr) string | |||||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | func loadGObjectTypeFns(ctx context.Context) { | 47 | func loadGObjectTypeFns(ctx context.Context) { | ||
| 48 | + | if ctx == nil { | |||
| 49 | + | ctx = context.Background() | |||
| 50 | + | } | |||
| 51 | + | ||||
| 48 | loadGObjectTypeFnsOnce.Do(func() { | 52 | loadGObjectTypeFnsOnce.Do(func() { | ||
| 49 | log := logging.FromContext(ctx) | 53 | log := logging.FromContext(ctx) | ||
| 50 | libs := make([]uintptr, 0, 2) | 54 | libs := make([]uintptr, 0, 2) | ||
| @@ -62,6 +66,12 @@ func loadGObjectTypeFns(ctx context.Context) { | |||||
| 62 | } | 66 | } | ||
| 63 | 67 | ||||
| 64 | core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a") | 68 | core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a") | ||
| 69 | + | if typeCheckInstanceIsA == nil { | |||
| 70 | + | log.Debug().Msg("failed to register g_type_check_instance_is_a") | |||
| 71 | + | } | |||
| 65 | core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance") | 72 | core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance") | ||
| 73 | + | if typeNameFromInstance == nil { | |||
| 74 | + | log.Debug().Msg("failed to register g_type_name_from_instance") | |||
| 75 | + | } | |||
| 66 | }) | 76 | }) | ||
| 67 | } | 77 | } | ||
M internal/ui/deps.go +1 -1
| @@ -47,7 +47,7 @@ type Dependencies struct { | |||
| 47 | 47 | HistoryRepo repository.HistoryRepository | |
| 48 | 48 | FavoriteRepo repository.FavoriteRepository | |
| 49 | 49 | ZoomRepo repository.ZoomRepository | |
| 50 | - | PermissionRepo repository.PermissionRepository | |
| 50 | + | PermissionRepo port.PermissionRepository | |
| 51 | 51 | FilterRepo repository.ContentWhitelistRepository | |
| 52 | 52 | ||
| 53 | 53 | // Use Cases | |
| @@ -47,7 +47,7 @@ type Dependencies struct { | |||||
| 47 | HistoryRepo repository.HistoryRepository | 47 | HistoryRepo repository.HistoryRepository | ||
| 48 | FavoriteRepo repository.FavoriteRepository | 48 | FavoriteRepo repository.FavoriteRepository | ||
| 49 | ZoomRepo repository.ZoomRepository | 49 | ZoomRepo repository.ZoomRepository | ||
| 50 | - | PermissionRepo repository.PermissionRepository | 50 | + | PermissionRepo port.PermissionRepository |
| 51 | FilterRepo repository.ContentWhitelistRepository | 51 | FilterRepo repository.ContentWhitelistRepository | ||
| 52 | 52 | ||||
| 53 | // Use Cases | 53 | // Use Cases | ||