Test that maintenance mode refuses image requests (closes #71)
Tests ahead of the change: with maintenance_mode on, both image routes must answer 503 with a Retry-After header and the JSON error body, while the health check keeps answering 200 and reporting maintenance_mode, and the login page and /metrics keep answering 200; these fail until the next commit. With it off, image requests must still reach the image handlers. The test server now also builds the health check, which these tests request. Model: opus-5-5
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"sneak.berlin/go/pixa/internal/database"
|
||||
"sneak.berlin/go/pixa/internal/globals"
|
||||
"sneak.berlin/go/pixa/internal/handlers"
|
||||
"sneak.berlin/go/pixa/internal/healthcheck"
|
||||
"sneak.berlin/go/pixa/internal/logger"
|
||||
"sneak.berlin/go/pixa/internal/middleware"
|
||||
)
|
||||
@@ -71,8 +72,15 @@ func newTestServer(t *testing.T) *Server {
|
||||
t.Fatalf("database.New() error = %v", err)
|
||||
}
|
||||
|
||||
hc, err := healthcheck.New(lc, healthcheck.Params{
|
||||
Globals: &globals.Globals{}, Config: cfg, Logger: log, Database: db,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("healthcheck.New() error = %v", err)
|
||||
}
|
||||
|
||||
h, err := handlers.New(lc, handlers.Params{
|
||||
Logger: log, Database: db, Config: cfg,
|
||||
Logger: log, Healthcheck: hc, Database: db, Config: cfg,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("handlers.New() error = %v", err)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/healthcheck"
|
||||
)
|
||||
|
||||
// unsignedImagePath is an image URL that carries no signature.
|
||||
const unsignedImagePath = "/v1/image/cdn.example.com/cat.jpg/100x100.jpeg"
|
||||
|
||||
// TestMaintenanceModeRefusesImageRequests verifies that while maintenance
|
||||
// mode is on, both image routes answer 503 Service Unavailable with a
|
||||
// Retry-After header and the JSON error body the image handlers send.
|
||||
func TestMaintenanceModeRefusesImageRequests(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newTestServer(t)
|
||||
s.config.MaintenanceMode = true
|
||||
|
||||
requests := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, unsignedImagePath},
|
||||
{http.MethodHead, unsignedImagePath},
|
||||
{http.MethodGet, "/v1/e/token/cat.jpg"},
|
||||
}
|
||||
|
||||
for _, tc := range requests {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
s.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), tc.method, tc.path, nil))
|
||||
t.Logf("status %d, body %s", rec.Code, rec.Body.String())
|
||||
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d, want %d",
|
||||
rec.Code, http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
retryAfter := rec.Header().Get("Retry-After")
|
||||
|
||||
seconds, err := strconv.Atoi(retryAfter)
|
||||
if err != nil || seconds <= 0 {
|
||||
t.Errorf("Retry-After = %q, want a positive number of seconds",
|
||||
retryAfter)
|
||||
}
|
||||
|
||||
// A HEAD response carries no body.
|
||||
if tc.method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Error string `json:"error"`
|
||||
Status int `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(rec.Body).Decode(&body)
|
||||
if err != nil {
|
||||
t.Fatalf("body is not JSON: %v", err)
|
||||
}
|
||||
|
||||
if body.Error == "" || body.Status != http.StatusServiceUnavailable ||
|
||||
body.Timestamp == "" {
|
||||
t.Errorf("body = %+v, want an error, status %d and a timestamp",
|
||||
body, http.StatusServiceUnavailable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestImageRequestsServedWithoutMaintenanceMode verifies that while
|
||||
// maintenance mode is off, image requests reach the image handlers instead
|
||||
// of the 503. The handlers refuse an unsigned image URL with 401 and a token
|
||||
// they cannot decrypt with 400, so either status shows a request got through.
|
||||
func TestImageRequestsServedWithoutMaintenanceMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newTestServer(t)
|
||||
s.config.MaintenanceMode = false
|
||||
|
||||
requests := []struct {
|
||||
method string
|
||||
path string
|
||||
want int
|
||||
}{
|
||||
{http.MethodGet, unsignedImagePath, http.StatusUnauthorized},
|
||||
{http.MethodHead, unsignedImagePath, http.StatusUnauthorized},
|
||||
{http.MethodGet, "/v1/e/token/cat.jpg", http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tc := range requests {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
s.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), tc.method, tc.path, nil))
|
||||
t.Logf("status %d, body %s", rec.Code, rec.Body.String())
|
||||
|
||||
if rec.Code != tc.want {
|
||||
t.Errorf("status = %d, want %d from the image handler",
|
||||
rec.Code, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaintenanceModeKeepsOtherRoutes verifies that while maintenance mode
|
||||
// is on, the health check still answers 200 and reports it, and the login
|
||||
// page and /metrics still answer 200. The image's Docker HEALTHCHECK
|
||||
// requests the health check: a 503 there would make the container
|
||||
// unhealthy, and upaas marks a deploy failed when its container is
|
||||
// unhealthy.
|
||||
func TestMaintenanceModeKeepsOtherRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newTestServer(t)
|
||||
s.config.MaintenanceMode = true
|
||||
|
||||
// /metrics is routed only when its username is set.
|
||||
s.config.MetricsUsername = "metrics"
|
||||
s.config.MetricsPassword = "metrics-password"
|
||||
s.SetupRoutes()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
s.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(),
|
||||
http.MethodGet, "/.well-known/healthcheck.json", nil))
|
||||
t.Logf("health check status %d, body %s", rec.Code, rec.Body.String())
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("health check status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var health healthcheck.Response
|
||||
|
||||
err := json.NewDecoder(rec.Body).Decode(&health)
|
||||
if err != nil || !health.Maintenance {
|
||||
t.Errorf("health check maintenance_mode = %v (error %v), want true",
|
||||
health.Maintenance, err)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
s.ServeHTTP(rec, clientRequest(t, http.MethodGet, nil, firstClient, ""))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("login page status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(),
|
||||
http.MethodGet, "/metrics", nil)
|
||||
req.SetBasicAuth(s.config.MetricsUsername, s.config.MetricsPassword)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
s.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("/metrics status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user