Refactor: split monolith into cmd/palette + internal/{store,api,web,lang} (#35)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package api
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user