169 lines
4.9 KiB
Go
169 lines
4.9 KiB
Go
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")
|
|
}
|
|
}
|