Files
geniusrun/backend/internal/api/config_test.go
Christophe Vila 8c9285f33c fix: IDEAS.md quickfixes — idle-timeout app config, id_token out of Claims, FormEvent import
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>
2026-08-04 17:44:37 +02:00

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)
}
}