package config import ( "strings" "testing" ) func TestValidate_DevRequiresDevToken(t *testing.T) { c := &Config{AuthMode: "dev", DevToken: ""} err := c.validate() if err == nil { t.Fatal("dev mode without DevToken must reject; got nil error") } if !strings.Contains(err.Error(), "CDROP_DEV_TOKEN") { t.Errorf("error must mention CDROP_DEV_TOKEN; got %q", err.Error()) } } func TestValidate_DevWithDevTokenOK(t *testing.T) { c := &Config{AuthMode: "dev", DevToken: "x"} if err := c.validate(); err != nil { t.Fatalf("dev with token should pass; got %v", err) } } func TestValidate_ProdOK(t *testing.T) { c := &Config{AuthMode: "prod", BrokerBaseURL: "http://broker:8080", BrokerInternalKey: "s3cr3t", PublicURL: "https://drop.example.net"} if err := c.validate(); err != nil { t.Fatalf("prod mode with broker config should pass; got %v", err) } } func TestValidate_ProdRequiresPublicURL(t *testing.T) { // PublicURL fixes the CSRF Origin check's expected value; without it sameOrigin fails // open, silently disabling the guard on the state-changing POST endpoints. Refuse to boot. c := &Config{AuthMode: "prod", BrokerBaseURL: "http://broker:8080", BrokerInternalKey: "k", PublicURL: ""} err := c.validate() if err == nil { t.Fatal("prod without CDROP_PUBLIC_URL must reject; got nil error") } if !strings.Contains(err.Error(), "CDROP_PUBLIC_URL") { t.Errorf("error must mention CDROP_PUBLIC_URL; got %q", err.Error()) } } func TestValidate_ProdRequiresBrokerBaseURL(t *testing.T) { // cdrop delegates session lifecycle to the Auth Broker; without its address the // service can't mint on approval. Refuse to boot in prod. c := &Config{AuthMode: "prod", BrokerBaseURL: "", BrokerInternalKey: "k"} err := c.validate() if err == nil { t.Fatal("prod without CDROP_BROKER_BASE_URL must reject; got nil error") } if !strings.Contains(err.Error(), "CDROP_BROKER_BASE_URL") { t.Errorf("error must mention CDROP_BROKER_BASE_URL; got %q", err.Error()) } } func TestValidate_ProdRequiresBrokerInternalKey(t *testing.T) { // The shared key guards the broker's /internal/* mint API; without it cdrop // can't authenticate to the broker. Refuse to boot in prod. c := &Config{AuthMode: "prod", BrokerBaseURL: "http://broker:8080", BrokerInternalKey: ""} err := c.validate() if err == nil { t.Fatal("prod without CDROP_BROKER_INTERNAL_KEY must reject; got nil error") } if !strings.Contains(err.Error(), "CDROP_BROKER_INTERNAL_KEY") { t.Errorf("error must mention CDROP_BROKER_INTERNAL_KEY; got %q", err.Error()) } } func TestValidate_VAPIDHalfPairRejected(t *testing.T) { // A single VAPID key is always an operator mistake: every push would fail at // runtime while the feature reads as "on". Reject either half alone. pubOnly := &Config{AuthMode: "dev", DevToken: "x", VAPIDPublicKey: "pub"} if err := pubOnly.validate(); err == nil { t.Fatal("public key without private must reject") } privOnly := &Config{AuthMode: "dev", DevToken: "x", VAPIDPrivateKey: "priv"} if err := privOnly.validate(); err == nil { t.Fatal("private key without public must reject") } } func TestValidate_VAPIDBothOrNeitherOK(t *testing.T) { both := &Config{AuthMode: "dev", DevToken: "x", VAPIDPublicKey: "pub", VAPIDPrivateKey: "priv"} if err := both.validate(); err != nil { t.Fatalf("both VAPID keys should pass; got %v", err) } if !both.PushEnabled() { t.Fatal("PushEnabled should be true when both keys set") } neither := &Config{AuthMode: "dev", DevToken: "x"} if err := neither.validate(); err != nil { t.Fatalf("no VAPID keys should pass (push disabled); got %v", err) } if neither.PushEnabled() { t.Fatal("PushEnabled should be false when no keys set") } } func TestVAPIDSubjectOrDefault(t *testing.T) { explicit := &Config{VAPIDSubject: "mailto:ops@example.com"} if got := explicit.VAPIDSubjectOrDefault(); got != "mailto:ops@example.com" { t.Fatalf("explicit subject: got %q", got) } derived := &Config{PublicURL: "https://drop.example.com"} if got := derived.VAPIDSubjectOrDefault(); got != "https://drop.example.com" { t.Fatalf("derived subject: got %q, want scheme://host", got) } fallback := &Config{} if got := fallback.VAPIDSubjectOrDefault(); !strings.HasPrefix(got, "https://") { t.Fatalf("fallback subject should be an https URL; got %q", got) } } func TestValidate_UnknownMode(t *testing.T) { c := &Config{AuthMode: "rogue"} err := c.validate() if err == nil { t.Fatal("unknown auth mode must reject") } } func TestLoad_EnvOverridesDefaults(t *testing.T) { t.Setenv("CDROP_AUTH_MODE", "dev") t.Setenv("CDROP_DEV_TOKEN", "abc") t.Setenv("CDROP_LISTEN", ":9090") t.Setenv("CDROP_DB_PATH", "/tmp/x.db") cfg, err := Load() if err != nil { t.Fatalf("load: %v", err) } if cfg.AuthMode != "dev" { t.Errorf("AuthMode: got %q, want %q", cfg.AuthMode, "dev") } if cfg.DevToken != "abc" { t.Errorf("DevToken: got %q, want %q", cfg.DevToken, "abc") } if cfg.Listen != ":9090" { t.Errorf("Listen: got %q, want %q", cfg.Listen, ":9090") } if cfg.DBPath != "/tmp/x.db" { t.Errorf("DBPath: got %q, want %q", cfg.DBPath, "/tmp/x.db") } }