From 0b29d16419c949f88b4478ec543ff3a4ccade3de Mon Sep 17 00:00:00 2001 From: Jah-yee <166608075+Jah-yee@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:53:52 +0800 Subject: [PATCH] fix(config): validate relay_url scheme (ws/wss) at config load Validate that relay_url uses the ws:// or wss:// scheme in config.Load, and add table-driven tests covering the valid and invalid cases. The unparseable-URL test case uses "ws://[::1" to exercise the url.Parse error path, and "not-a-url" is covered separately as a missing-scheme case. The test uses strings.Contains instead of a custom substring helper. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/config/config.go | 12 ++++++ pkg/config/config_test.go | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 pkg/config/config_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index f5eebd0..4e671ee 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net/url" "strings" "github.com/spf13/viper" @@ -197,5 +198,16 @@ func Load(configPath string) (*Config, error) { return nil, fmt.Errorf("access_secret is required (set NB_ACCESS_SECRET or access_secret in config)") } + // Validate relay_url scheme + if cfg.RelayURL != "" { + u, err := url.Parse(cfg.RelayURL) + if err != nil { + return nil, fmt.Errorf("relay_url must be a valid URL: %w", err) + } + if u.Scheme != "ws" && u.Scheme != "wss" { + return nil, fmt.Errorf("relay_url must use ws:// or wss:// scheme, got: %q", cfg.RelayURL) + } + } + return &cfg, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..821f8ab --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,79 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestLoadRelayURLScheme(t *testing.T) { + tests := []struct { + name string + relayURL string + wantErr bool + errMsg string + }{ + { + name: "valid wss URL", + relayURL: "wss://relay.example.com/register", + wantErr: false, + }, + { + name: "valid ws URL", + relayURL: "ws://localhost:8080/register", + wantErr: false, + }, + { + name: "invalid https URL", + relayURL: "https://relay.example.com/register", + wantErr: true, + errMsg: `relay_url must use ws:// or wss:// scheme`, + }, + { + name: "invalid http URL", + relayURL: "http://relay.example.com/register", + wantErr: true, + errMsg: `relay_url must use ws:// or wss:// scheme`, + }, + { + name: "not a URL", + relayURL: "ws://[::1", + wantErr: true, + errMsg: "relay_url must be a valid URL", + }, + { + name: "relative path without scheme", + relayURL: "not-a-url", + wantErr: true, + errMsg: "relay_url must use ws:// or wss:// scheme", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temp config file with required fields + relay_url + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "forager.yaml") + content := "relay_url: " + tt.relayURL + "\n" + + "access_key: testkey\n" + + "access_secret: testsecret\n" + if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { + t.Fatalf("failed to write temp config: %v", err) + } + + _, err := Load(configFile) + if (err != nil) != tt.wantErr { + t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && tt.errMsg != "" { + if err == nil { + t.Errorf("Load() expected error containing %q, got nil", tt.errMsg) + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("Load() error = %v, want error containing %q", err, tt.errMsg) + } + } + }) + } +}