detached 3094000

fix: address CodeRabbit review — dedup validation, testify, docs clarity

commit
30940005350d1420d734830ed1cfa2e88dae89a2
author
brice · · 2026-03-22 17:54 UTC
parent
175d6d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +40 -60
M docs/upgrading.md +3 -1
@@ -42,6 +42,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
4242 3. **Update your config** to remove password fields:
4343
4444 **Before (v2.16.0):**
45+
4546 ```toml
4647 [auth]
4748 enabled = true
@@ -52,6 +53,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
5253 ```
5354
5455 **After (v2.30.0):**
56+
5557 ```toml
5658 [auth]
5759 enabled = true
@@ -79,7 +81,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
7981
8082 Ephemeral access tokens issued by `/auth/token` now have a configurable lifetime via `auth.access_token_ttl` (default `"15m"`, maximum `"1h"`).
8183
82-Tokens with a lifetime at or below `MaxAccessTokenLifetime` (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they **cannot be individually revoked**. They become invalid only when they naturally expire. If you need tighter revocation control, use a shorter TTL.
84+Tokens with a lifetime at or below `MaxAccessTokenLifetime` (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they **cannot be individually revoked**. They become invalid only when they naturally expire. Shortening `auth.access_token_ttl` only reduces the exposure window; it does not enable per-token revocation. If you need explicit revocation, use a stored long-lived token instead of `/auth/token`.
8385
8486 ```toml
8587 [auth]
@@ -42,6 +42,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
42 3. **Update your config** to remove password fields:42 3. **Update your config** to remove password fields:
43 43
44 **Before (v2.16.0):**44 **Before (v2.16.0):**
45+
45 ```toml46 ```toml
46 [auth]47 [auth]
47 enabled = true48 enabled = true
@@ -52,6 +53,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
52 ```53 ```
53 54
54 **After (v2.30.0):**55 **After (v2.30.0):**
56+
55 ```toml57 ```toml
56 [auth]58 [auth]
57 enabled = true59 enabled = true
@@ -79,7 +81,7 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
79 81
80 Ephemeral access tokens issued by `/auth/token` now have a configurable lifetime via `auth.access_token_ttl` (default `"15m"`, maximum `"1h"`).82 Ephemeral access tokens issued by `/auth/token` now have a configurable lifetime via `auth.access_token_ttl` (default `"15m"`, maximum `"1h"`).
81 83
82-Tokens with a lifetime at or below `MaxAccessTokenLifetime` (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they **cannot be individually revoked**. They become invalid only when they naturally expire. If you need tighter revocation control, use a shorter TTL.84+Tokens with a lifetime at or below `MaxAccessTokenLifetime` (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they **cannot be individually revoked**. They become invalid only when they naturally expire. Shortening `auth.access_token_ttl` only reduces the exposure window; it does not enable per-token revocation. If you need explicit revocation, use a stored long-lived token instead of `/auth/token`.
83 85
84 ```toml86 ```toml
85 [auth]87 [auth]
M internal/adapters/out/domainsecrets/store.go +13 -9
@@ -219,15 +219,22 @@ func (s *FileStore) GetAllAttachment(containerName string) (map[string]string, e
219219 return secrets, nil
220220 }
221221
222-// writeSecretsAtomic writes all secrets to the domain's env file atomically.
223-// It writes to a temporary file first, syncs it, then renames to the final path.
224-func (s *FileStore) writeSecretsAtomic(domainName string, secrets map[string]string) error {
225- // Validate secret values before any file operations
222+// validateSecretValues checks that no secret value contains newline characters.
223+func validateSecretValues(secrets map[string]string) error {
226224 for key, value := range secrets {
227225 if strings.ContainsAny(value, "\n\r") {
228226 return fmt.Errorf("secret value for %q contains newline characters", key)
229227 }
230228 }
229+ return nil
230+}
231+
232+// writeSecretsAtomic writes all secrets to the domain's env file atomically.
233+// It writes to a temporary file first, syncs it, then renames to the final path.
234+func (s *FileStore) writeSecretsAtomic(domainName string, secrets map[string]string) error {
235+ if err := validateSecretValues(secrets); err != nil {
236+ return err
237+ }
231238
232239 envFile, err := s.validateEnvFilePath(domainName)
233240 if err != nil {
@@ -370,11 +377,8 @@ func (s *FileStore) validateAttachmentEnvFilePath(containerName string) (string,
370377 // writeAttachmentSecretsAtomic writes all secrets to the attachment's env file atomically.
371378 // It writes to a temporary file first, syncs it, then renames to the final path.
372379 func (s *FileStore) writeAttachmentSecretsAtomic(containerName string, secrets map[string]string) error {
373- // Validate secret values before any file operations
374- for key, value := range secrets {
375- if strings.ContainsAny(value, "\n\r") {
376- return fmt.Errorf("secret value for %q contains newline characters", key)
377- }
380+ if err := validateSecretValues(secrets); err != nil {
381+ return err
378382 }
379383
380384 envFile, err := s.validateAttachmentEnvFilePath(containerName)
@@ -219,15 +219,22 @@ func (s *FileStore) GetAllAttachment(containerName string) (map[string]string, e
219 return secrets, nil219 return secrets, nil
220 }220 }
221 221
222-// writeSecretsAtomic writes all secrets to the domain's env file atomically.222+// validateSecretValues checks that no secret value contains newline characters.
223-// It writes to a temporary file first, syncs it, then renames to the final path.223+func validateSecretValues(secrets map[string]string) error {
224-func (s *FileStore) writeSecretsAtomic(domainName string, secrets map[string]string) error {
225- // Validate secret values before any file operations
226 for key, value := range secrets {224 for key, value := range secrets {
227 if strings.ContainsAny(value, "\n\r") {225 if strings.ContainsAny(value, "\n\r") {
228 return fmt.Errorf("secret value for %q contains newline characters", key)226 return fmt.Errorf("secret value for %q contains newline characters", key)
229 }227 }
230 }228 }
229+ return nil
230+}
231+
232+// writeSecretsAtomic writes all secrets to the domain's env file atomically.
233+// It writes to a temporary file first, syncs it, then renames to the final path.
234+func (s *FileStore) writeSecretsAtomic(domainName string, secrets map[string]string) error {
235+ if err := validateSecretValues(secrets); err != nil {
236+ return err
237+ }
231 238
232 envFile, err := s.validateEnvFilePath(domainName)239 envFile, err := s.validateEnvFilePath(domainName)
233 if err != nil {240 if err != nil {
@@ -370,11 +377,8 @@ func (s *FileStore) validateAttachmentEnvFilePath(containerName string) (string,
370 // writeAttachmentSecretsAtomic writes all secrets to the attachment's env file atomically.377 // writeAttachmentSecretsAtomic writes all secrets to the attachment's env file atomically.
371 // It writes to a temporary file first, syncs it, then renames to the final path.378 // It writes to a temporary file first, syncs it, then renames to the final path.
372 func (s *FileStore) writeAttachmentSecretsAtomic(containerName string, secrets map[string]string) error {379 func (s *FileStore) writeAttachmentSecretsAtomic(containerName string, secrets map[string]string) error {
373- // Validate secret values before any file operations380+ if err := validateSecretValues(secrets); err != nil {
374- for key, value := range secrets {381+ return err
375- if strings.ContainsAny(value, "\n\r") {
376- return fmt.Errorf("secret value for %q contains newline characters", key)
377- }
378 }382 }
379 383
380 envFile, err := s.validateAttachmentEnvFilePath(containerName)384 envFile, err := s.validateAttachmentEnvFilePath(containerName)
M internal/app/run_auth_config_test.go +16 -44
@@ -2,12 +2,13 @@ package app
22
33 import (
44 "context"
5- "strings"
65 "testing"
76
87 "github.com/bnema/gordon/internal/domain"
98 "github.com/bnema/zerowrap"
109 "github.com/spf13/viper"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
1112 )
1213
1314 func TestBuildAuthConfig_UsesConfigEnabledFlag(t *testing.T) {
@@ -18,78 +19,49 @@ func TestBuildAuthConfig_UsesConfigEnabledFlag(t *testing.T) {
1819 cfg.Auth.Type = "token"
1920
2021 authCfg, err := buildAuthConfig(context.Background(), cfg, domain.AuthTypeToken, domain.SecretsBackendUnsafe, t.TempDir(), zerowrap.Default())
21- if err != nil {
22- t.Fatalf("expected no error, got %v", err)
23- }
24- if authCfg.Enabled {
25- t.Fatalf("expected auth config enabled=false, got true")
26- }
22+ require.NoError(t, err)
23+ assert.False(t, authCfg.Enabled, "expected auth config enabled=false")
2724 }
2825
2926 func TestResolveSecretsBackend_RejectsMissingBackend(t *testing.T) {
3027 _, err := resolveSecretsBackend("")
31- if err == nil {
32- t.Fatal("expected error for missing secrets backend")
33- }
34- if !strings.Contains(err.Error(), "auth.secrets_backend is required") {
35- t.Fatalf("unexpected error: %v", err)
36- }
28+ require.Error(t, err)
29+ assert.Contains(t, err.Error(), "auth.secrets_backend is required")
3730 }
3831
3932 func TestResolveSecretsBackend_RejectsUnknownBackend(t *testing.T) {
4033 _, err := resolveSecretsBackend("vault")
41- if err == nil {
42- t.Fatal("expected error for unsupported secrets backend")
43- }
44- if !strings.Contains(err.Error(), `unsupported auth.secrets_backend "vault"`) {
45- t.Fatalf("unexpected error: %v", err)
46- }
34+ require.Error(t, err)
35+ assert.Contains(t, err.Error(), `unsupported auth.secrets_backend "vault"`)
4736 }
4837
4938 func TestResolveAuthType_RejectsPassword(t *testing.T) {
5039 cfg := Config{}
5140 cfg.Auth.Type = "password"
5241 _, err := resolveAuthType(cfg)
53- if err == nil {
54- t.Fatal("expected error for auth.type=password")
55- }
56- if !strings.Contains(err.Error(), "unsupported auth.type") {
57- t.Fatalf("unexpected error: %v", err)
58- }
42+ require.Error(t, err)
43+ assert.Contains(t, err.Error(), "unsupported auth.type")
5944 }
6045
6146 func TestResolveAuthType_AcceptsToken(t *testing.T) {
6247 cfg := Config{}
6348 cfg.Auth.Type = "token"
6449 authType, err := resolveAuthType(cfg)
65- if err != nil {
66- t.Fatalf("unexpected error: %v", err)
67- }
68- if authType != domain.AuthTypeToken {
69- t.Fatalf("expected AuthTypeToken, got %v", authType)
70- }
50+ require.NoError(t, err)
51+ assert.Equal(t, domain.AuthTypeToken, authType)
7152 }
7253
7354 func TestResolveAuthType_AcceptsEmpty(t *testing.T) {
7455 cfg := Config{}
7556 authType, err := resolveAuthType(cfg)
76- if err != nil {
77- t.Fatalf("unexpected error: %v", err)
78- }
79- if authType != domain.AuthTypeToken {
80- t.Fatalf("expected AuthTypeToken, got %v", authType)
81- }
57+ require.NoError(t, err)
58+ assert.Equal(t, domain.AuthTypeToken, authType)
8259 }
8360
8461 func TestLoadConfig_DoesNotDefaultSecretsBackendToUnsafe(t *testing.T) {
8562 v := viper.New()
8663
8764 err := loadConfig(v, "")
88- if err != nil {
89- t.Fatalf("expected no error, got %v", err)
90- }
91-
92- if got := v.GetString("auth.secrets_backend"); got != "" {
93- t.Fatalf("expected empty default for auth.secrets_backend, got %q", got)
94- }
65+ require.NoError(t, err)
66+ assert.Empty(t, v.GetString("auth.secrets_backend"), "expected empty default for auth.secrets_backend")
9567 }
@@ -2,12 +2,13 @@ package app
2 2
3 import (3 import (
4 "context"4 "context"
5- "strings"
6 "testing"5 "testing"
7 6
8 "github.com/bnema/gordon/internal/domain"7 "github.com/bnema/gordon/internal/domain"
9 "github.com/bnema/zerowrap"8 "github.com/bnema/zerowrap"
10 "github.com/spf13/viper"9 "github.com/spf13/viper"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
11 )12 )
12 13
13 func TestBuildAuthConfig_UsesConfigEnabledFlag(t *testing.T) {14 func TestBuildAuthConfig_UsesConfigEnabledFlag(t *testing.T) {
@@ -18,78 +19,49 @@ func TestBuildAuthConfig_UsesConfigEnabledFlag(t *testing.T) {
18 cfg.Auth.Type = "token"19 cfg.Auth.Type = "token"
19 20
20 authCfg, err := buildAuthConfig(context.Background(), cfg, domain.AuthTypeToken, domain.SecretsBackendUnsafe, t.TempDir(), zerowrap.Default())21 authCfg, err := buildAuthConfig(context.Background(), cfg, domain.AuthTypeToken, domain.SecretsBackendUnsafe, t.TempDir(), zerowrap.Default())
21- if err != nil {22+ require.NoError(t, err)
22- t.Fatalf("expected no error, got %v", err)23+ assert.False(t, authCfg.Enabled, "expected auth config enabled=false")
23- }
24- if authCfg.Enabled {
25- t.Fatalf("expected auth config enabled=false, got true")
26- }
27 }24 }
28 25
29 func TestResolveSecretsBackend_RejectsMissingBackend(t *testing.T) {26 func TestResolveSecretsBackend_RejectsMissingBackend(t *testing.T) {
30 _, err := resolveSecretsBackend("")27 _, err := resolveSecretsBackend("")
31- if err == nil {28+ require.Error(t, err)
32- t.Fatal("expected error for missing secrets backend")29+ assert.Contains(t, err.Error(), "auth.secrets_backend is required")
33- }
34- if !strings.Contains(err.Error(), "auth.secrets_backend is required") {
35- t.Fatalf("unexpected error: %v", err)
36- }
37 }30 }
38 31
39 func TestResolveSecretsBackend_RejectsUnknownBackend(t *testing.T) {32 func TestResolveSecretsBackend_RejectsUnknownBackend(t *testing.T) {
40 _, err := resolveSecretsBackend("vault")33 _, err := resolveSecretsBackend("vault")
41- if err == nil {34+ require.Error(t, err)
42- t.Fatal("expected error for unsupported secrets backend")35+ assert.Contains(t, err.Error(), `unsupported auth.secrets_backend "vault"`)
43- }
44- if !strings.Contains(err.Error(), `unsupported auth.secrets_backend "vault"`) {
45- t.Fatalf("unexpected error: %v", err)
46- }
47 }36 }
48 37
49 func TestResolveAuthType_RejectsPassword(t *testing.T) {38 func TestResolveAuthType_RejectsPassword(t *testing.T) {
50 cfg := Config{}39 cfg := Config{}
51 cfg.Auth.Type = "password"40 cfg.Auth.Type = "password"
52 _, err := resolveAuthType(cfg)41 _, err := resolveAuthType(cfg)
53- if err == nil {42+ require.Error(t, err)
54- t.Fatal("expected error for auth.type=password")43+ assert.Contains(t, err.Error(), "unsupported auth.type")
55- }
56- if !strings.Contains(err.Error(), "unsupported auth.type") {
57- t.Fatalf("unexpected error: %v", err)
58- }
59 }44 }
60 45
61 func TestResolveAuthType_AcceptsToken(t *testing.T) {46 func TestResolveAuthType_AcceptsToken(t *testing.T) {
62 cfg := Config{}47 cfg := Config{}
63 cfg.Auth.Type = "token"48 cfg.Auth.Type = "token"
64 authType, err := resolveAuthType(cfg)49 authType, err := resolveAuthType(cfg)
65- if err != nil {50+ require.NoError(t, err)
66- t.Fatalf("unexpected error: %v", err)51+ assert.Equal(t, domain.AuthTypeToken, authType)
67- }
68- if authType != domain.AuthTypeToken {
69- t.Fatalf("expected AuthTypeToken, got %v", authType)
70- }
71 }52 }
72 53
73 func TestResolveAuthType_AcceptsEmpty(t *testing.T) {54 func TestResolveAuthType_AcceptsEmpty(t *testing.T) {
74 cfg := Config{}55 cfg := Config{}
75 authType, err := resolveAuthType(cfg)56 authType, err := resolveAuthType(cfg)
76- if err != nil {57+ require.NoError(t, err)
77- t.Fatalf("unexpected error: %v", err)58+ assert.Equal(t, domain.AuthTypeToken, authType)
78- }
79- if authType != domain.AuthTypeToken {
80- t.Fatalf("expected AuthTypeToken, got %v", authType)
81- }
82 }59 }
83 60
84 func TestLoadConfig_DoesNotDefaultSecretsBackendToUnsafe(t *testing.T) {61 func TestLoadConfig_DoesNotDefaultSecretsBackendToUnsafe(t *testing.T) {
85 v := viper.New()62 v := viper.New()
86 63
87 err := loadConfig(v, "")64 err := loadConfig(v, "")
88- if err != nil {65+ require.NoError(t, err)
89- t.Fatalf("expected no error, got %v", err)66+ assert.Empty(t, v.GetString("auth.secrets_backend"), "expected empty default for auth.secrets_backend")
90- }
91-
92- if got := v.GetString("auth.secrets_backend"); got != "" {
93- t.Fatalf("expected empty default for auth.secrets_backend, got %q", got)
94- }
95 }67 }
M pkg/validation/path.go +1 -1
@@ -136,7 +136,7 @@ func ValidateDomainParam(domain string) error {
136136 if strings.Contains(domain, "..") {
137137 return fmt.Errorf("domain contains path traversal sequence")
138138 }
139- if strings.ContainsAny(domain, "\x00\n\r") {
139+ if strings.ContainsAny(domain, "\x00\n\r/") {
140140 return fmt.Errorf("domain contains invalid characters")
141141 }
142142 return nil
@@ -136,7 +136,7 @@ func ValidateDomainParam(domain string) error {
136 if strings.Contains(domain, "..") {136 if strings.Contains(domain, "..") {
137 return fmt.Errorf("domain contains path traversal sequence")137 return fmt.Errorf("domain contains path traversal sequence")
138 }138 }
139- if strings.ContainsAny(domain, "\x00\n\r") {139+ if strings.ContainsAny(domain, "\x00\n\r/") {
140 return fmt.Errorf("domain contains invalid characters")140 return fmt.Errorf("domain contains invalid characters")
141 }141 }
142 return nil142 return nil
M pkg/validation/path_test.go +7 -5
@@ -3,6 +3,8 @@ package validation
33 import (
44 "strings"
55 "testing"
6+
7+ "github.com/stretchr/testify/require"
68 )
79
810 func TestValidateRepositoryName(t *testing.T) {
@@ -215,15 +217,15 @@ func TestValidateDomainParam(t *testing.T) {
215217 {"carriage return", "example\r.com", false},
216218 {"empty", "", false},
217219 {"double dot in middle", "foo..bar", false},
220+ {"forward slash", "example/com", false},
218221 }
219222 for _, tt := range tests {
220223 t.Run(tt.name, func(t *testing.T) {
221224 err := ValidateDomainParam(tt.input)
222- if tt.valid && err != nil {
223- t.Errorf("expected valid, got error: %v", err)
224- }
225- if !tt.valid && err == nil {
226- t.Errorf("expected error for input %q", tt.input)
225+ if tt.valid {
226+ require.NoError(t, err, "input: %q", tt.input)
227+ } else {
228+ require.Error(t, err, "input: %q", tt.input)
227229 }
228230 })
229231 }
@@ -3,6 +3,8 @@ package validation
3 import (3 import (
4 "strings"4 "strings"
5 "testing"5 "testing"
6+
7+ "github.com/stretchr/testify/require"
6 )8 )
7 9
8 func TestValidateRepositoryName(t *testing.T) {10 func TestValidateRepositoryName(t *testing.T) {
@@ -215,15 +217,15 @@ func TestValidateDomainParam(t *testing.T) {
215 {"carriage return", "example\r.com", false},217 {"carriage return", "example\r.com", false},
216 {"empty", "", false},218 {"empty", "", false},
217 {"double dot in middle", "foo..bar", false},219 {"double dot in middle", "foo..bar", false},
220+ {"forward slash", "example/com", false},
218 }221 }
219 for _, tt := range tests {222 for _, tt := range tests {
220 t.Run(tt.name, func(t *testing.T) {223 t.Run(tt.name, func(t *testing.T) {
221 err := ValidateDomainParam(tt.input)224 err := ValidateDomainParam(tt.input)
222- if tt.valid && err != nil {225+ if tt.valid {
223- t.Errorf("expected valid, got error: %v", err)226+ require.NoError(t, err, "input: %q", tt.input)
224- }227+ } else {
225- if !tt.valid && err == nil {228+ require.Error(t, err, "input: %q", tt.input)
226- t.Errorf("expected error for input %q", tt.input)
227 }229 }
228 })230 })
229 }231 }

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 7

Tags 106

↑↓ moveenter openesc close