Admin endpoint: ENV/file admin key, --reset-admin-key, settings API wired into ratelimit/max-bytes/default-expiry/burn-window (#40)
CI / test (push) Successful in 22s
CI / docker (push) Skipped

This commit is contained in:
2026-09-09 00:44:02 -05:00
parent 03600b2ed5
commit a3349b4a98
10 changed files with 562 additions and 11 deletions
+168
View File
@@ -0,0 +1,168 @@
package main
import (
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// newTestSettingsStore builds an in-memory settings store with a temp file.
func newTestSettingsStore(t *testing.T, cfg Config) *settingsStore {
t.Helper()
dir := t.TempDir()
ss := loadSettingsStore(filepath.Join(dir, "palette.db"), cfg)
// point persistence at a temp path (dir(dbPath) == dir)
return ss
}
func TestAdminAuth(t *testing.T) {
s := testServer(t)
h := s.routes()
req := httptest.NewRequest("GET", "/admin/api/settings", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 401 {
t.Fatalf("no key: expected 401, got %d", rec.Code)
}
req = httptest.NewRequest("GET", "/admin/api/settings", nil)
req.Header.Set("X-Admin-Key", "wrong-key")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 401 {
t.Fatalf("wrong key: expected 401, got %d", rec.Code)
}
req = httptest.NewRequest("GET", "/admin/api/settings?key=test-admin-key", nil)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatalf("query key: expected 200, got %d", rec.Code)
}
req = httptest.NewRequest("GET", "/admin/api/settings", nil)
req.Header.Set("X-Admin-Key", "test-admin-key")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatalf("header key: expected 200, got %d", rec.Code)
}
// HTML page itself is open (key entered via form)
req = httptest.NewRequest("GET", "/admin", nil)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatalf("admin page: expected 200, got %d", rec.Code)
}
}
func TestAdminEnvKeyPrecedence(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "palette.db")
t.Setenv("PALETTE_ADMIN_KEY", "envkey1234567890abcdef")
key, err := resolveAdminKey(dbPath)
if err != nil {
t.Fatal(err)
}
if key != "envkey1234567890abcdef" {
t.Fatalf("env key not used: %q", key)
}
if _, err := os.Stat(filepath.Join(dir, "admin-key")); !os.IsNotExist(err) {
t.Fatal("env key should not create a key file")
}
// unset env: file takes over
os.Unsetenv("PALETTE_ADMIN_KEY")
key2, err := resolveAdminKey(dbPath)
if err != nil {
t.Fatal(err)
}
if len(key2) != 32 {
t.Fatalf("generated key should be 32 hex chars, got %d", len(key2))
}
if fi, err := os.Stat(filepath.Join(dir, "admin-key")); err != nil || fi.Mode().Perm() != 0600 {
t.Fatalf("admin-key file perms: %v err %v", fi, err)
}
// reuse on subsequent boots
key3, _ := resolveAdminKey(dbPath)
if key3 != key2 {
t.Fatal("persisted key not reused")
}
}
func TestAdminSettingsRoundTrip(t *testing.T) {
s := testServer(t)
h := s.routes()
post := func(body string) *httptest.ResponseRecorder {
req := httptest.NewRequest("POST", "/admin/api/settings", strings.NewReader(body))
req.Header.Set("X-Admin-Key", "test-admin-key")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
rec := post(`{"rate_limit_burst": 9, "rate_limit_per_minute": 120, "max_content_bytes": 1024, "default_expiry": "1h", "custom_slug_reservation_days": 10, "burn_viewer_window_minutes": 7}`)
if rec.Code != 200 {
t.Fatalf("post settings: %d %s", rec.Code, rec.Body.String())
}
got := s.settings.get()
if got.RateLimitBurst != 9 || got.RateLimitPerMinute != 120 || got.MaxContentBytes != 1024 ||
got.DefaultExpiry != "1h" || got.CustomSlugReservationDays != 10 || got.BurnViewerWindowMinutes != 7 {
t.Fatalf("settings not applied: %+v", got)
}
// persisted to disk
b, err := os.ReadFile(s.settings.path)
if err != nil {
t.Fatal(err)
}
var persisted Settings
if err := json.Unmarshal(b, &persisted); err != nil {
t.Fatal(err)
}
if persisted.BurnViewerWindowMinutes != 7 {
t.Fatalf("persisted settings wrong: %+v", persisted)
}
// invalid rejected
if rec := post(`{"rate_limit_burst": -1}`); rec.Code != 400 {
t.Fatalf("invalid settings: expected 400, got %d", rec.Code)
}
if rec := post(`{"rate_limit_burst": 5, "rate_limit_per_minute": 60, "max_content_bytes": 1024, "default_expiry": "bogus", "custom_slug_reservation_days": 10, "burn_viewer_window_minutes": 5}`); rec.Code != 400 {
t.Fatalf("bad expiry: expected 400, got %d", rec.Code)
}
// settings actually consumed: default expiry applied on create
req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"x"}`))
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 201 {
t.Fatalf("create: %d %s", rec.Code, rec.Body.String())
}
var created struct {
ExpiresAt *int64 `json:"expires_at"`
}
json.Unmarshal(rec.Body.Bytes(), &created)
if created.ExpiresAt == nil {
t.Fatal("default expiry not applied to new paste")
}
}
func TestAdminResetKey(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "palette.db")
os.Unsetenv("PALETTE_ADMIN_KEY")
key1, _ := resolveAdminKey(dbPath)
// direct invocation of the reset behavior
resetAdminKeyFile(dbPath)
key2, _ := resolveAdminKey(dbPath)
if key1 == key2 {
t.Fatal("reset did not regenerate key")
}
}