-
-
Notifications
You must be signed in to change notification settings - Fork 70
Fix edge cases, improve validation, and clean up orchestrator, config, and CLI workflows #185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
52efa9b
fbd9aa9
5d049bb
323abc1
c2cd1cf
5cdbf7b
fe4240b
e1beabc
97a04c0
da617e0
f8d5b60
3e3884b
0a1d889
c8a7044
fbd1f20
7706577
b74c625
a098750
2bf4e33
4f184cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,38 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/tis24dev/proxsave/pkg/utils" | ||
| ) | ||
|
|
||
| func encodeEnvValue(value string) string { | ||
| value = strings.TrimSpace(value) | ||
| if value == "" { | ||
| return "" | ||
| } | ||
|
|
||
| if !strings.ContainsAny(value, "# \t\r\n\"'") { | ||
| return value | ||
| } | ||
| if !strings.Contains(value, "'") { | ||
| return "'" + value + "'" | ||
| } | ||
| if !strings.Contains(value, `"`) { | ||
| return `"` + value + `"` | ||
| } | ||
| return strconv.Quote(value) | ||
|
Comment on lines
+19
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if TrimQuotes handles strconv.Quote escape sequences
# Find TrimQuotes implementation
rg -n -A10 'func TrimQuotes' pkg/utils/
# Check if there's any strconv.Unquote usage in parsing
rg -n 'strconv\.Unquote' internal/config/ pkg/utils/Repository: tis24dev/proxsave Length of output: 615 🏁 Script executed: # Find parseEnvFile and see how it processes values
rg -n -A20 'func parseEnvFile' internal/config/
# Check how encodeEnvValue results are used
rg -n -B5 -A5 'encodeEnvValue' internal/config/
# Look for any unquoting logic after parsing
rg -n 'TrimQuotes' internal/config/ pkg/Repository: tis24dev/proxsave Length of output: 3319 🏁 Script executed: # Get the complete parseEnvFile function
sed -n '1329,1400p' internal/config/config.go
# Get SplitKeyValue implementation
rg -n -A15 'func SplitKeyValue' pkg/utils/
# Check the full context of line 116 in strings.go
sed -n '110,125p' pkg/utils/strings.goRepository: tis24dev/proxsave Length of output: 3276 🏁 Script executed: # Check where SECONDARY_PATH and SECONDARY_LOG_PATH are used
rg -n 'SECONDARY_PATH|SECONDARY_LOG_PATH' internal/config/
# Check for test cases that might trigger strconv.Quote path
rg -n 'encodeEnvValue' --include='*_test.go'
# Look for any tests with paths containing both quotes
rg -n "both.*quote|quote.*both" pkg/utils/ internal/config/Repository: tis24dev/proxsave Length of output: 7685 🏁 Script executed: # Search for tests that might have paths with both quotes
rg -n "both.*quote|'.*\"|\"\.*'" pkg/utils/ internal/config/ --type go
# Check env_mutation_test.go for all test cases
sed -n '1,120p' internal/config/env_mutation_test.goRepository: tis24dev/proxsave Length of output: 5474 Round-trip correctness issue with When a path contains both single and double quotes, The round-trip test ( Consider updating 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // ApplySecondaryStorageSettings writes the canonical secondary-storage state | ||
| // into an env template. Disabled secondary storage always clears both | ||
| // SECONDARY_PATH and SECONDARY_LOG_PATH so the saved config matches user intent. | ||
| func ApplySecondaryStorageSettings(template string, enabled bool, secondaryPath string, secondaryLogPath string) string { | ||
| if enabled { | ||
| template = utils.SetEnvValue(template, "SECONDARY_ENABLED", "true") | ||
| template = utils.SetEnvValue(template, "SECONDARY_PATH", strings.TrimSpace(secondaryPath)) | ||
| template = utils.SetEnvValue(template, "SECONDARY_LOG_PATH", strings.TrimSpace(secondaryLogPath)) | ||
| template = utils.SetEnvValue(template, "SECONDARY_PATH", encodeEnvValue(secondaryPath)) | ||
| template = utils.SetEnvValue(template, "SECONDARY_LOG_PATH", encodeEnvValue(secondaryLogPath)) | ||
| return template | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,112 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestApplySecondaryStorageSettingsEnabled(t *testing.T) { | ||
| template := "SECONDARY_ENABLED=false\nSECONDARY_PATH=\nSECONDARY_LOG_PATH=\n" | ||
| func parseMutatedEnvTemplate(t *testing.T, template string) (map[string]string, map[string]int) { | ||
| t.Helper() | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, true, " /mnt/secondary ", " /mnt/secondary/log ") | ||
| values := make(map[string]string) | ||
| counts := make(map[string]int) | ||
|
|
||
| for _, line := range strings.Split(template, "\n") { | ||
| trimmed := strings.TrimSpace(line) | ||
| if trimmed == "" || strings.HasPrefix(trimmed, "#") { | ||
| continue | ||
| } | ||
|
|
||
| for _, needle := range []string{ | ||
| "SECONDARY_ENABLED=true", | ||
| "SECONDARY_PATH=/mnt/secondary", | ||
| "SECONDARY_LOG_PATH=/mnt/secondary/log", | ||
| } { | ||
| if !strings.Contains(got, needle) { | ||
| t.Fatalf("expected %q in template:\n%s", needle, got) | ||
| parts := strings.SplitN(line, "=", 2) | ||
| if len(parts) != 2 { | ||
| t.Fatalf("invalid env line %q in template:\n%s", line, template) | ||
| } | ||
|
|
||
| key := strings.TrimSpace(parts[0]) | ||
| value := parts[1] | ||
| counts[key]++ | ||
| values[key] = value | ||
| } | ||
|
|
||
| return values, counts | ||
| } | ||
|
|
||
| func assertMutatedEnvValue(t *testing.T, values map[string]string, counts map[string]int, key, want string) { | ||
| t.Helper() | ||
|
|
||
| if got := counts[key]; got != 1 { | ||
| t.Fatalf("%s occurrences = %d; want 1", key, got) | ||
| } | ||
| if got := values[key]; got != want { | ||
| t.Fatalf("%s = %q; want %q", key, got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestApplySecondaryStorageSettingsEnabled(t *testing.T) { | ||
| template := "SECONDARY_ENABLED=false\nSECONDARY_PATH=\nSECONDARY_LOG_PATH=\n" | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, true, " /mnt/secondary ", " /mnt/secondary/log ") | ||
| values, counts := parseMutatedEnvTemplate(t, got) | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_ENABLED", "true") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_PATH", "/mnt/secondary") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_LOG_PATH", "/mnt/secondary/log") | ||
| } | ||
|
|
||
| func TestApplySecondaryStorageSettingsEnabledWithEmptyLogPath(t *testing.T) { | ||
| template := "SECONDARY_ENABLED=false\nSECONDARY_PATH=\nSECONDARY_LOG_PATH=/old/log\n" | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, true, "/mnt/secondary", "") | ||
|
|
||
| for _, needle := range []string{ | ||
| "SECONDARY_ENABLED=true", | ||
| "SECONDARY_PATH=/mnt/secondary", | ||
| "SECONDARY_LOG_PATH=", | ||
| } { | ||
| if !strings.Contains(got, needle) { | ||
| t.Fatalf("expected %q in template:\n%s", needle, got) | ||
| } | ||
| } | ||
| values, counts := parseMutatedEnvTemplate(t, got) | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_ENABLED", "true") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_PATH", "/mnt/secondary") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_LOG_PATH", "") | ||
| } | ||
|
|
||
| func TestApplySecondaryStorageSettingsDisabledClearsValues(t *testing.T) { | ||
| template := "SECONDARY_ENABLED=true\nSECONDARY_PATH=/mnt/old-secondary\nSECONDARY_LOG_PATH=/mnt/old-secondary/logs\n" | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, false, "/ignored", "/ignored/logs") | ||
|
|
||
| for _, needle := range []string{ | ||
| "SECONDARY_ENABLED=false", | ||
| "SECONDARY_PATH=", | ||
| "SECONDARY_LOG_PATH=", | ||
| } { | ||
| if !strings.Contains(got, needle) { | ||
| t.Fatalf("expected %q in template:\n%s", needle, got) | ||
| } | ||
| } | ||
| if strings.Contains(got, "/mnt/old-secondary") { | ||
| t.Fatalf("expected old secondary values to be cleared:\n%s", got) | ||
| } | ||
| values, counts := parseMutatedEnvTemplate(t, got) | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_ENABLED", "false") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_PATH", "") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_LOG_PATH", "") | ||
| } | ||
|
|
||
| func TestApplySecondaryStorageSettingsDisabledAppendsCanonicalState(t *testing.T) { | ||
| template := "BACKUP_ENABLED=true\n" | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, false, "", "") | ||
| values, counts := parseMutatedEnvTemplate(t, got) | ||
| assertMutatedEnvValue(t, values, counts, "BACKUP_ENABLED", "true") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_ENABLED", "false") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_PATH", "") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_LOG_PATH", "") | ||
| } | ||
|
|
||
| for _, needle := range []string{ | ||
| "BACKUP_ENABLED=true", | ||
| "SECONDARY_ENABLED=false", | ||
| "SECONDARY_PATH=", | ||
| "SECONDARY_LOG_PATH=", | ||
| } { | ||
| if !strings.Contains(got, needle) { | ||
| t.Fatalf("expected %q in template:\n%s", needle, got) | ||
| } | ||
| func TestApplySecondaryStorageSettingsQuotesUnsafePaths(t *testing.T) { | ||
| template := "SECONDARY_ENABLED=false\nSECONDARY_PATH=\nSECONDARY_LOG_PATH=\n" | ||
|
|
||
| got := ApplySecondaryStorageSettings(template, true, " /mnt/secondary #1 ", " /mnt/secondary/log dir ") | ||
| values, counts := parseMutatedEnvTemplate(t, got) | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_ENABLED", "true") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_PATH", "'/mnt/secondary #1'") | ||
| assertMutatedEnvValue(t, values, counts, "SECONDARY_LOG_PATH", "'/mnt/secondary/log dir'") | ||
|
|
||
| configPath := filepath.Join(t.TempDir(), "backup.env") | ||
| if err := os.WriteFile(configPath, []byte(got), 0o600); err != nil { | ||
| t.Fatalf("write config: %v", err) | ||
| } | ||
|
|
||
| raw, err := parseEnvFile(configPath) | ||
| if err != nil { | ||
| t.Fatalf("parseEnvFile() error = %v", err) | ||
| } | ||
| if gotPath := raw["SECONDARY_PATH"]; gotPath != "/mnt/secondary #1" { | ||
| t.Fatalf("SECONDARY_PATH = %q; want %q", gotPath, "/mnt/secondary #1") | ||
| } | ||
| if gotLogPath := raw["SECONDARY_LOG_PATH"]; gotLogPath != "/mnt/secondary/log dir" { | ||
| t.Fatalf("SECONDARY_LOG_PATH = %q; want %q", gotLogPath, "/mnt/secondary/log dir") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Soften the guarantee in this sentence.
Line 849 is a bit stronger than the current implementation.
sendViaCloudRelaystill treats some HTTP 200 responses with empty or unparsable bodies as success, so this log line does not always prove the upstream provider accepted the message. Wording this as “the relay accepted the request / returned HTTP 200” would be safer.🤖 Prompt for AI Agents