session.idle_timeout (minutes, default 15) joins the app-config registry and drives the onboarding Garmin session eviction, distinct from session.duration (the login cookie lifetime in hours). The raw Keycloak ID token no longer rides in auth.Claims through every request context: it's minted into the session cookie separately and read back only by the logout handler via IDTokenFromSessionCookie. OnboardingWizard uses the type-imported FormEvent<HTMLFormElement> instead of the React.FormEvent namespace alias. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
3.9 KiB
Go
112 lines
3.9 KiB
Go
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.idle_timeout"]; e.value != "15" || e.def != "15" || e.overridden {
|
|
t.Fatalf("session.idle_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)
|
|
}
|
|
}
|