package server import ( "net/http" "net/http/httptest" "strconv" "testing" "sneak.berlin/go/pixa/internal/config" ) // requestImageFrom sends method path to s from remoteAddr and returns the // response. The image handlers refuse an unsigned image URL with 401 and a // token they cannot decrypt with 400, so either status shows the limit let // the request through. func requestImageFrom( t *testing.T, s *Server, method, path, remoteAddr string, ) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequestWithContext(t.Context(), method, path, nil) req.RemoteAddr = remoteAddr rec := httptest.NewRecorder() s.ServeHTTP(rec, req) return rec } // TestImageRequestLimitRefusesRequestOverLimit verifies that GET and HEAD on // both image routes count toward one limit per client, and that the request // after image_requests_per_minute of them is refused with 429 and a // Retry-After header. func TestImageRequestLimitRefusesRequestOverLimit(t *testing.T) { t.Parallel() requests := []struct { method string path string want int }{ {http.MethodGet, unsignedImagePath, http.StatusUnauthorized}, {http.MethodHead, unsignedImagePath, http.StatusUnauthorized}, {http.MethodGet, encryptedImagePath, http.StatusBadRequest}, } s := newTestServer(t) s.config.ImageRequestsPerMinute = len(requests) s.SetupRoutes() for _, tc := range requests { rec := requestImageFrom(t, s, tc.method, tc.path, firstClient) if rec.Code != tc.want { t.Fatalf("%s %s within the limit status = %d, want %d", tc.method, tc.path, rec.Code, tc.want) } } rec := requestImageFrom(t, s, http.MethodHead, encryptedImagePath, firstClient) if rec.Code != http.StatusTooManyRequests { t.Fatalf("request over the limit status = %d, want %d", rec.Code, http.StatusTooManyRequests) } 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) } } // TestImageRequestLimitCountsClientsSeparately verifies that one client going // over the limit does not limit another. func TestImageRequestLimitCountsClientsSeparately(t *testing.T) { t.Parallel() s := newTestServer(t) s.config.ImageRequestsPerMinute = 1 s.SetupRoutes() rec := requestImageFrom(t, s, http.MethodGet, unsignedImagePath, firstClient) if rec.Code != http.StatusUnauthorized { t.Fatalf("first client's first request status = %d, want %d", rec.Code, http.StatusUnauthorized) } rec = requestImageFrom(t, s, http.MethodGet, unsignedImagePath, firstClient) if rec.Code != http.StatusTooManyRequests { t.Fatalf("first client's second request status = %d, want %d", rec.Code, http.StatusTooManyRequests) } rec = requestImageFrom(t, s, http.MethodGet, unsignedImagePath, secondClient) if rec.Code != http.StatusUnauthorized { t.Errorf("second client's first request status = %d, want %d", rec.Code, http.StatusUnauthorized) } } // TestImageRequestLimitZeroLetsEveryRequestThrough verifies that with // image_requests_per_minute set to 0 no image request is refused with 429, // not even past the default limit. func TestImageRequestLimitZeroLetsEveryRequestThrough(t *testing.T) { t.Parallel() s := newTestServer(t) s.config.ImageRequestsPerMinute = 0 s.SetupRoutes() for request := range config.DefaultImageRequestsPerMinute + 1 { rec := requestImageFrom(t, s, http.MethodGet, unsignedImagePath, firstClient) if rec.Code != http.StatusUnauthorized { t.Fatalf("request %d status = %d, want %d", request+1, rec.Code, http.StatusUnauthorized) } } } // TestImageRequestLimitDoesNotCountPreflight verifies that a browser's CORS // preflight request to an image route is answered without being counted, so // a client that has sent several still gets its image request through. func TestImageRequestLimitDoesNotCountPreflight(t *testing.T) { t.Parallel() s := newTestServer(t) s.config.AccessControlAllowOrigin = config.DefaultAccessControlAllowOrigin s.config.ImageRequestsPerMinute = 1 s.SetupRoutes() for range 3 { req := httptest.NewRequestWithContext( t.Context(), http.MethodOptions, unsignedImagePath, nil) req.RemoteAddr = firstClient req.Header.Set("Origin", "https://app.example.com") req.Header.Set("Access-Control-Request-Method", http.MethodGet) rec := httptest.NewRecorder() s.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("preflight status = %d, want %d", rec.Code, http.StatusOK) } } rec := requestImageFrom(t, s, http.MethodGet, unsignedImagePath, firstClient) if rec.Code != http.StatusUnauthorized { t.Errorf("image request after preflights status = %d, want %d", rec.Code, http.StatusUnauthorized) } }