package api import ( "encoding/json" "net/http" "testing" authmock "geniusrun/backend/internal/auth/mock" "geniusrun/backend/internal/garmin" "geniusrun/backend/internal/store" ) type configTestResponse struct { Application []struct { Key string `json:"key"` Value string `json:"value"` Default string `json:"default"` Overridden bool `json:"overridden"` Description string `json:"description"` } `json:"application"` Environment []struct { Name string `json:"name"` Value string `json:"value"` } `json:"environment"` } func TestConfig_GetDefaultsAndEnvSnapshot(t *testing.T) { s, _, _ := newTestServer(t) s.EnvVars = []EnvVar{{Name: "GENIUSRUN_OIDC_CLIENT_SECRET", Value: "•••• (set)"}} rec := doJSON(t, s.Router(), http.MethodGet, "/api/config", nil) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String()) } var resp configTestResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } if len(resp.Application) != 2 { t.Fatalf("expected 2 app-config entries, got %d: %+v", len(resp.Application), resp.Application) } byKey := map[string]struct { value, def string overridden bool }{} for _, e := range resp.Application { if e.Description == "" { t.Errorf("entry %q has no description", e.Key) } byKey[e.Key] = struct { value, def string overridden bool }{e.Value, e.Default, e.Overridden} } if e := byKey["session.duration"]; e.value != "720" || e.def != "720" || e.overridden { t.Fatalf("session.duration default entry = %+v", e) } if e := byKey["session.setup_timeout"]; e.value != "15" || e.def != "15" || e.overridden { t.Fatalf("session.setup_timeout default entry = %+v", e) } if len(resp.Environment) != 1 || resp.Environment[0].Value != "•••• (set)" { t.Fatalf("env snapshot not passed through: %+v", resp.Environment) } } func TestConfig_PutPersistsValidatesAllOrNothing(t *testing.T) { s, _, _ := newTestServer(t) router := s.Router() rec := doJSON(t, router, http.MethodPut, "/api/config", map[string]string{"session.duration": "168"}) if rec.Code != http.StatusOK { t.Fatalf("valid PUT status = %d, body = %s", rec.Code, rec.Body.String()) } var resp configTestResponse json.Unmarshal(rec.Body.Bytes(), &resp) if resp.Application[0].Value != "168" || !resp.Application[0].Overridden { t.Fatalf("override not reflected: %+v", resp.Application[0]) } if rec = doJSON(t, router, http.MethodPut, "/api/config", map[string]string{"bogus.key": "1"}); rec.Code != http.StatusBadRequest { t.Fatalf("unknown key status = %d, want 400", rec.Code) } rec = doJSON(t, router, http.MethodPut, "/api/config", map[string]string{"session.duration": "zero"}) if rec.Code != http.StatusBadRequest { t.Fatalf("invalid value status = %d, want 400", rec.Code) } rec = doJSON(t, router, http.MethodGet, "/api/config", nil) json.Unmarshal(rec.Body.Bytes(), &resp) if resp.Application[0].Value != "168" { t.Fatalf("rejected PUT still changed the value: %+v", resp.Application[0]) } } // Same gate as every data route: an unprovisioned session gets 403, per // the repo's adversarial-isolation testing convention. func TestConfig_RequiresProvisionedUser(t *testing.T) { db, err := store.Open(t.TempDir() + "/config_gate_test.db") if err != nil { t.Fatalf("store.Open: %v", err) } defer db.Close() m := &garmin.MockClient{} s := NewServer(db, func(garmin.ClientConfig) garmin.Client { return m }, garmin.ClientConfig{}, garmin.SyncConfig{}, &authmock.Verifier{}, testSessionConfig) if rec := doJSON(t, s.Router(), http.MethodGet, "/api/config", nil); rec.Code != http.StatusForbidden { t.Fatalf("GET status = %d, want 403", rec.Code) } if rec := doJSON(t, s.Router(), http.MethodPut, "/api/config", map[string]string{"session.duration": "1"}); rec.Code != http.StatusForbidden { t.Fatalf("PUT status = %d, want 403", rec.Code) } }