package config import ( "os" "strings" "testing" "time" ) func TestDefault(t *testing.T) { cfg := Default() if cfg.Server.GrpcAddr != ":50051" { t.Errorf("expected default grpc_addr :50051, got %s", cfg.Server.GrpcAddr) } if cfg.Database.Path != "relay.db" { t.Errorf("expected default db path relay.db, got %s", cfg.Database.Path) } if cfg.Metrics.Enabled != true { t.Error("expected metrics enabled by default") } } func TestLoadYAML(t *testing.T) { // Create temporary config file tmpfile, err := os.CreateTemp("", "config-*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) configData := ` server: grpc_addr: ":9999" http_addr: ":8888" database: path: "test.db" auth: read: enabled: true write: enabled: true timestamp_window: 120 rate_limit: enabled: true default_rps: 50 default_burst: 100 metrics: enabled: true addr: ":9191" namespace: "test" ` if _, err := tmpfile.Write([]byte(configData)); err != nil { t.Fatal(err) } tmpfile.Close() // Load config cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("failed to load config: %v", err) } // Verify values if cfg.Server.GrpcAddr != ":9999" { t.Errorf("expected grpc_addr :9999, got %s", cfg.Server.GrpcAddr) } if cfg.Database.Path != "test.db" { t.Errorf("expected db path test.db, got %s", cfg.Database.Path) } if !cfg.Auth.Read.Enabled { t.Error("expected auth read enabled") } if !cfg.Auth.Write.Enabled { t.Error("expected auth write enabled") } if cfg.Auth.TimestampWindow != 120 { t.Errorf("expected timestamp window 120, got %d", cfg.Auth.TimestampWindow) } if cfg.RateLimit.DefaultRPS != 50 { t.Errorf("expected rate limit 50, got %.1f", cfg.RateLimit.DefaultRPS) } if cfg.Metrics.Namespace != "test" { t.Errorf("expected metrics namespace test, got %s", cfg.Metrics.Namespace) } } func TestEnvOverrides(t *testing.T) { // Set environment variables os.Setenv("MUXSTR_SERVER_GRPC_ADDR", ":7777") os.Setenv("MUXSTR_AUTH_READ_ENABLED", "true") os.Setenv("MUXSTR_AUTH_WRITE_ENABLED", "true") os.Setenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS", "200") defer func() { os.Unsetenv("MUXSTR_SERVER_GRPC_ADDR") os.Unsetenv("MUXSTR_AUTH_READ_ENABLED") os.Unsetenv("MUXSTR_AUTH_WRITE_ENABLED") os.Unsetenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS") }() // Load with empty file (just defaults + env) cfg, err := Load("") if err != nil { t.Fatalf("failed to load config: %v", err) } // Verify env overrides if cfg.Server.GrpcAddr != ":7777" { t.Errorf("expected env override :7777, got %s", cfg.Server.GrpcAddr) } if !cfg.Auth.Read.Enabled { t.Error("expected auth read enabled from env") } if !cfg.Auth.Write.Enabled { t.Error("expected auth write enabled from env") } if cfg.RateLimit.DefaultRPS != 200 { t.Errorf("expected rate limit 200 from env, got %.1f", cfg.RateLimit.DefaultRPS) } } func TestValidation(t *testing.T) { tests := []struct { name string cfg *Config wantErr bool }{ { name: "valid default config", cfg: Default(), wantErr: false, }, { name: "missing grpc_addr", cfg: &Config{ Server: ServerConfig{ HttpAddr: ":8080", }, Database: DatabaseConfig{ Path: "test.db", }, }, wantErr: true, }, { name: "missing http_addr", cfg: &Config{ Server: ServerConfig{ GrpcAddr: ":50051", }, Database: DatabaseConfig{ Path: "test.db", }, }, wantErr: true, }, { name: "missing database path", cfg: &Config{ Server: ServerConfig{ GrpcAddr: ":50051", HttpAddr: ":8080", }, Database: DatabaseConfig{}, }, wantErr: true, }, { name: "invalid log level", cfg: &Config{ Server: ServerConfig{ GrpcAddr: ":50051", HttpAddr: ":8080", }, Database: DatabaseConfig{ Path: "test.db", }, Logging: LoggingConfig{ Level: "invalid", Format: "json", }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestSaveAndLoad(t *testing.T) { // Create config cfg := Default() cfg.Server.GrpcAddr = ":9999" cfg.Auth.Read.Enabled = true cfg.Auth.Write.Enabled = true cfg.RateLimit.DefaultRPS = 100 // Save to temp file tmpfile, err := os.CreateTemp("", "config-*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) tmpfile.Close() if err := cfg.Save(tmpfile.Name()); err != nil { t.Fatalf("failed to save config: %v", err) } // Load it back loaded, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("failed to load config: %v", err) } // Verify if loaded.Server.GrpcAddr != ":9999" { t.Errorf("expected grpc_addr :9999, got %s", loaded.Server.GrpcAddr) } if !loaded.Auth.Read.Enabled { t.Error("expected auth read enabled") } if !loaded.Auth.Write.Enabled { t.Error("expected auth write enabled") } if loaded.RateLimit.DefaultRPS != 100 { t.Errorf("expected rate limit 100, got %.1f", loaded.RateLimit.DefaultRPS) } } func TestNpubNormalization(t *testing.T) { // Create a test key to get a valid npub tmpfile, err := os.CreateTemp("", "config-*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) // Use a real npub for testing configData := ` server: grpc_addr: ":50051" http_addr: ":8080" database: path: "test.db" auth: read: enabled: true allowed_npubs: - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft write: enabled: true allowed_npubs: - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 ` if _, err := tmpfile.Write([]byte(configData)); err != nil { t.Fatal(err) } tmpfile.Close() cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("failed to load config: %v", err) } // Verify read npubs were normalized to hex if len(cfg.Auth.Read.AllowedNpubs) != 2 { t.Errorf("expected 2 allowed npubs for read, got %d", len(cfg.Auth.Read.AllowedNpubs)) } // Verify write npubs were normalized to hex if len(cfg.Auth.Write.AllowedNpubs) != 1 { t.Errorf("expected 1 allowed npub for write, got %d", len(cfg.Auth.Write.AllowedNpubs)) } // Check that they're hex format (64 chars, not npub1...) for i, pubkey := range cfg.Auth.Read.AllowedNpubs { if len(pubkey) != 64 { t.Errorf("read npub %d: expected 64 hex chars, got %d", i, len(pubkey)) } if len(pubkey) >= 5 && pubkey[:5] == "npub1" { t.Errorf("read npub %d: should be normalized to hex, still in npub format", i) } } for i, pubkey := range cfg.Auth.Write.AllowedNpubs { if len(pubkey) != 64 { t.Errorf("write npub %d: expected 64 hex chars, got %d", i, len(pubkey)) } if len(pubkey) >= 5 && pubkey[:5] == "npub1" { t.Errorf("write npub %d: should be normalized to hex, still in npub format", i) } } // Verify the actual hex values expectedHex1 := "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" expectedHex2 := "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" if cfg.Auth.Read.AllowedNpubs[0] != expectedHex1 { t.Errorf("read npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Read.AllowedNpubs[0]) } if cfg.Auth.Read.AllowedNpubs[1] != expectedHex2 { t.Errorf("read npub 1: expected %s, got %s", expectedHex2, cfg.Auth.Read.AllowedNpubs[1]) } if cfg.Auth.Write.AllowedNpubs[0] != expectedHex1 { t.Errorf("write npub 0: expected %s, got %s", expectedHex1, cfg.Auth.Write.AllowedNpubs[0]) } } func TestNpubValidation(t *testing.T) { tests := []struct { name string config string expectError bool errorMsg string }{ { name: "invalid hex in read list", config: ` server: grpc_addr: ":50051" http_addr: ":8080" database: path: "test.db" auth: read: enabled: true allowed_npubs: - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d `, expectError: true, errorMsg: "must start with 'npub1'", }, { name: "invalid hex in write list", config: ` server: grpc_addr: ":50051" http_addr: ":8080" database: path: "test.db" auth: write: enabled: true allowed_npubs: - 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d `, expectError: true, errorMsg: "must start with 'npub1'", }, { name: "valid npub lists", config: ` server: grpc_addr: ":50051" http_addr: ":8080" database: path: "test.db" auth: read: enabled: true allowed_npubs: - npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 write: enabled: true allowed_npubs: - npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft `, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tmpfile, err := os.CreateTemp("", "config-*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) if _, err := tmpfile.Write([]byte(tt.config)); err != nil { t.Fatal(err) } tmpfile.Close() _, err = Load(tmpfile.Name()) if tt.expectError { if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), tt.errorMsg) { t.Errorf("expected error containing %q, got: %v", tt.errorMsg, err) } } else { if err != nil { t.Errorf("unexpected error: %v", err) } } }) } } func TestDurationParsing(t *testing.T) { // Create config with durations tmpfile, err := os.CreateTemp("", "config-*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) configData := ` server: grpc_addr: ":50051" http_addr: ":8080" read_timeout: "1m" write_timeout: "2m" database: path: "test.db" rate_limit: cleanup_interval: "10m" max_idle_time: "20m" ` if _, err := tmpfile.Write([]byte(configData)); err != nil { t.Fatal(err) } tmpfile.Close() cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("failed to load config: %v", err) } if cfg.Server.ReadTimeout != 1*time.Minute { t.Errorf("expected read timeout 1m, got %v", cfg.Server.ReadTimeout) } if cfg.Server.WriteTimeout != 2*time.Minute { t.Errorf("expected write timeout 2m, got %v", cfg.Server.WriteTimeout) } if cfg.RateLimit.CleanupInterval != 10*time.Minute { t.Errorf("expected cleanup interval 10m, got %v", cfg.RateLimit.CleanupInterval) } }