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) } }