package api // #66: admin key attempts must be rate limited per IP (5/min), constant-time // compared, and failures logged. Hammering bad keys must yield 429s. import ( "net/http/httptest" "strings" "testing" ) // TestAdminKeyRateLimited: burst of 5 bad-key attempts allowed (401), the 6th // gets 429, and even the correct key is blocked from that IP until refill. func TestAdminKeyRateLimited(t *testing.T) { srv := newTestServer(t) h := srv.routes() reqIP := "10.7.7.1:1234" var got429, retryAfter bool var lastCode int for i := 0; i < 10; i++ { req := httptest.NewRequest("POST", "/admin/api/settings", nil) req.RemoteAddr = reqIP req.Header.Set("X-Admin-Key", "wrong-key") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) lastCode = rec.Code if rec.Code == 429 { got429 = true retryAfter = rec.Header().Get("Retry-After") != "" break } } if !got429 { t.Fatalf("expected 429 after hammering bad keys, last status %d", lastCode) } if !retryAfter { t.Error("429 missing Retry-After header") } // Correct key from the same IP is also locked out. req := httptest.NewRequest("POST", "/admin/api/settings", nil) req.RemoteAddr = reqIP req.Header.Set("X-Admin-Key", srv.adminKey) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 429 { t.Errorf("correct key after lockout: got %d, want 429", rec.Code) } // A different IP is unaffected. req2 := httptest.NewRequest("POST", "/admin/api/settings", strings.NewReader(`{"rate_limit_burst":5,"rate_limit_per_minute":60,"max_content_bytes":1048576,"custom_slug_reservation_days":30,"burn_viewer_window_minutes":15}`)) req2.RemoteAddr = "203.0.113.9:1234" req2.Header.Set("X-Admin-Key", srv.adminKey) rec2 := httptest.NewRecorder() h.ServeHTTP(rec2, req2) if rec2.Code != 200 { t.Errorf("correct key from another IP: got %d, want 200", rec2.Code) } } // TestAdminKeyConstantTimeCompare: sanity check that the comparison is // constant-time (uses subtle.ConstantTimeCompare, not ==). func TestAdminKeyConstantTimeCompare(t *testing.T) { srv := newTestServer(t) r := httptest.NewRequest("GET", "/", nil) r.Header.Set("X-Admin-Key", "test-admin-key") if !srv.adminKeyOK(r, srv.adminKey) { t.Fatal("correct key rejected") } r.Header.Set("X-Admin-Key", "wrong") if srv.adminKeyOK(r, srv.adminKey) { t.Fatal("wrong key accepted") } // differ in length: must not panic/mismatch unexpectedly r.Header.Set("X-Admin-Key", "test-admin-key-longer") if srv.adminKeyOK(r, srv.adminKey) { t.Fatal("longer wrong key accepted") } }