2026-08-04 00:16:26 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-08-04 16:04:18 +02:00
|
|
|
authmock "geniusrun/backend/internal/auth/mock"
|
2026-08-04 00:16:26 +02:00
|
|
|
"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)
|
|
|
|
|
}
|
2026-08-04 17:44:37 +02:00
|
|
|
if len(resp.Application) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 app-config entries, got %d: %+v", len(resp.Application), resp.Application)
|
2026-08-04 00:16:26 +02:00
|
|
|
}
|
2026-08-04 17:44:37 +02:00
|
|
|
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)
|
2026-08-04 00:16:26 +02:00
|
|
|
}
|
|
|
|
|
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()
|
2026-08-04 16:04:18 +02:00
|
|
|
m := &garmin.MockClient{}
|
|
|
|
|
s := NewServer(db, func(garmin.ClientConfig) garmin.Client { return m }, garmin.ClientConfig{}, garmin.SyncConfig{}, &authmock.Verifier{}, testSessionConfig)
|
2026-08-04 00:16:26 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|