/admin/settings edits the document llmgw ctl config edit opens: checked as a control plane would load it, refused when made from a version since replaced (the edit kept), its history viewed and rolled back as a new version, every change audited by size and never by contents, since it can hold webhook secrets. Administrators only; in English and Turkish. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
type fakeSettings struct{ versions []store.SettingsVersion }
|
|
|
|
func (f *fakeSettings) ClusterSettings(context.Context) (store.SettingsVersion, error) {
|
|
if len(f.versions) == 0 {
|
|
return store.SettingsVersion{}, nil
|
|
}
|
|
return f.versions[len(f.versions)-1], nil
|
|
}
|
|
|
|
func (f *fakeSettings) ClusterSettingsAt(_ context.Context, v int64) (store.SettingsVersion, error) {
|
|
for _, s := range f.versions {
|
|
if s.Version == v {
|
|
return s, nil
|
|
}
|
|
}
|
|
return store.SettingsVersion{}, nil
|
|
}
|
|
|
|
func (f *fakeSettings) ClusterSettingsHistory(context.Context, int) ([]store.SettingsVersion, error) {
|
|
out := make([]store.SettingsVersion, len(f.versions))
|
|
for i, v := range f.versions {
|
|
out[len(f.versions)-1-i] = v
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeSettings) PutClusterSettings(_ context.Context, base int64, body, by, note string) (int64, error) {
|
|
cur, _ := f.ClusterSettings(context.Background())
|
|
if base != cur.Version {
|
|
return 0, store.ErrSettingsChanged
|
|
}
|
|
n := cur.Version + 1
|
|
f.versions = append(f.versions, store.SettingsVersion{Version: n, Body: body, By: by, Note: note, At: time.Now()})
|
|
return n, nil
|
|
}
|
|
|
|
type auditSink struct{ events []store.AuditEvent }
|
|
|
|
func (a *auditSink) WriteAudit(_ context.Context, e store.AuditEvent) error {
|
|
a.events = append(a.events, e)
|
|
return nil
|
|
}
|
|
|
|
func clusterSettingsPage(t *testing.T, user store.User) (*http.ServeMux, *fakeSettings, *auditSink) {
|
|
t.Helper()
|
|
s := &fakeSettings{versions: []store.SettingsVersion{{Version: 1, Body: "guard:\n enabled: true\n", By: "init", At: time.Now()}}}
|
|
p := NewAccountPages(fakeAccounts{user: user}, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
sink := &auditSink{}
|
|
p.SetAudit(sink)
|
|
mux := http.NewServeMux()
|
|
p.RegisterAdminSettings(mux, s, func(b []byte) error {
|
|
if strings.Contains(string(b), "bogus") {
|
|
return errors.New("unknown key bogus")
|
|
}
|
|
return nil
|
|
}, []string{"listen", "store"})
|
|
return mux, s, sink
|
|
}
|
|
|
|
func TestTheSettingsPageIsForAdministratorsOnly(t *testing.T) {
|
|
for _, u := range []store.User{
|
|
{ID: "u1", Email: "v@example.com", Role: store.RoleViewer},
|
|
{ID: "u2", Email: "o@example.com", Role: store.RoleOperator},
|
|
{ID: "u3", Email: "b@example.com", Role: store.RoleBilling},
|
|
} {
|
|
mux, _, _ := clusterSettingsPage(t, u)
|
|
if w := mirrorGet(t, mux, "/admin/settings"); w.Code != http.StatusNotFound {
|
|
t.Errorf("%s saw the settings: %d", u.Role, w.Code)
|
|
}
|
|
}
|
|
mux, _, _ := clusterSettingsPage(t, store.User{ID: "a", Email: "a@example.com", IsAdmin: true})
|
|
body := mirrorGet(t, mux, "/admin/settings").Body.String()
|
|
for _, want := range []string{"guard:\n enabled: true", "Version 1, in force", "<code>listen</code>", `href="/admin/settings" aria-current="page"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("no %q in:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSettingsAreCheckedAppliedAndRolledBack(t *testing.T) {
|
|
mux, s, sink := clusterSettingsPage(t, store.User{ID: "a", Email: "a@example.com", IsAdmin: true})
|
|
post := func(form url.Values) string {
|
|
form.Set("csrf", "tok")
|
|
return mirrorPost(t, mux, "/admin/settings", form).Body.String()
|
|
}
|
|
// Refused: does not load. The edit stays in the editor; nothing kept.
|
|
body := post(url.Values{"base": {"1"}, "body": {"bogus: 1\n"}})
|
|
if !strings.Contains(body, "unknown key bogus") || !strings.Contains(body, "bogus: 1") || len(s.versions) != 1 {
|
|
t.Errorf("an invalid document: %d versions\n%s", len(s.versions), body)
|
|
}
|
|
// Applied, with its note and who.
|
|
body = post(url.Values{"base": {"1"}, "body": {"guard:\r\n enabled: false\r\n"}, "note": {"maintenance"}})
|
|
if !strings.Contains(body, "Version 2 is in force") || len(s.versions) != 2 ||
|
|
s.versions[1].Body != "guard:\n enabled: false\n" || s.versions[1].By != "a@example.com" || s.versions[1].Note != "maintenance" {
|
|
t.Errorf("applying: %+v\n%s", s.versions, body)
|
|
}
|
|
// Made from a version no longer the newest: refused, not overwritten.
|
|
body = post(url.Values{"base": {"1"}, "body": {"guard:\n enabled: true\n x: 1\n"}})
|
|
if !strings.Contains(body, "someone changed the settings") || len(s.versions) != 2 {
|
|
t.Errorf("a stale edit: %d versions\n%s", len(s.versions), body)
|
|
}
|
|
// An earlier version is shown, and put back as a new one.
|
|
if body := mirrorGet(t, mux, "/admin/settings?version=1").Body.String(); !strings.Contains(body, "Put version 1 back in force") {
|
|
t.Errorf("viewing version 1:\n%s", body)
|
|
}
|
|
w := mirrorPost(t, mux, "/admin/settings/rollback", url.Values{"csrf": {"tok"}, "version": {"1"}})
|
|
if len(s.versions) != 3 || s.versions[2].Body != s.versions[0].Body || !strings.Contains(w.Body.String(), "as version 3") {
|
|
t.Errorf("rollback: %+v\n%s", s.versions, w.Body.String())
|
|
}
|
|
// Without the CSRF token, nothing.
|
|
if w := mirrorPost(t, mux, "/admin/settings", url.Values{"base": {"3"}, "body": {"a: 1\n"}}); w.Code != http.StatusBadRequest || len(s.versions) != 3 {
|
|
t.Errorf("no token: %d", w.Code)
|
|
}
|
|
// Audited, without the document itself: it can hold secrets.
|
|
var saw bool
|
|
for _, e := range sink.events {
|
|
if strings.Contains(e.Detail, "enabled") {
|
|
t.Errorf("the document is in the audit log: %+v", e)
|
|
}
|
|
if e.Action == "POST /admin/settings" && strings.Contains(e.Detail, "version 2") {
|
|
saw = true
|
|
}
|
|
}
|
|
if !saw {
|
|
t.Errorf("no audit line for version 2: %+v", sink.events)
|
|
}
|
|
}
|