chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/deviceauth"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
)
|
||||
|
||||
type fakeRateLimiter struct {
|
||||
result RateLimitResult
|
||||
err error
|
||||
calls int
|
||||
scopes []string
|
||||
}
|
||||
|
||||
func (f *fakeRateLimiter) Allow(_ context.Context, scope, identifier string) (RateLimitResult, error) {
|
||||
f.calls++
|
||||
f.scopes = append(f.scopes, scope+":"+identifier)
|
||||
return f.result, f.err
|
||||
}
|
||||
|
||||
func TestRateLimitRejectsBusinessRequest(t *testing.T) {
|
||||
limiter := &fakeRateLimiter{result: RateLimitResult{Allowed: false, RetryAfter: 1500 * time.Millisecond}}
|
||||
handler := Middleware(slog.Default(), "test", 1024, limiter, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("downstream handler must not be called")
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/items", nil)
|
||||
request.RemoteAddr = "192.0.2.1:1234"
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusTooManyRequests || recorder.Header().Get("Retry-After") != "2" {
|
||||
t.Fatalf("status = %d, retry-after = %q", recorder.Code, recorder.Header().Get("Retry-After"))
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), "RATE_LIMITED") {
|
||||
t.Fatalf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRateLimitUsesVerifiedUserAndSelectedTenant(t *testing.T) {
|
||||
limiter := &fakeRateLimiter{result: RateLimitResult{Allowed: true, Remaining: 7}}
|
||||
called := false
|
||||
handler := UserRateLimit(slog.Default(), limiter, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/households", nil)
|
||||
request.Header.Set("X-Tenant-Id", "tenant-public-id")
|
||||
request = request.WithContext(identity.WithPrincipal(request.Context(), identity.Principal{Issuer: "issuer", Subject: "user-subject"}))
|
||||
handler.ServeHTTP(httptest.NewRecorder(), request)
|
||||
if !called || strings.Join(limiter.scopes, ",") != "user:issuer\x00user-subject,tenant:tenant-public-id" {
|
||||
t.Fatalf("called=%t scopes=%q", called, limiter.scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceRateLimitRejectsVerifiedDeviceScopeBeforeTenant(t *testing.T) {
|
||||
limiter := &fakeRateLimiter{result: RateLimitResult{Allowed: false, RetryAfter: time.Second}}
|
||||
handler := DeviceRateLimit(slog.Default(), limiter, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("downstream handler must not be called")
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodGet, "/device/v2/config", nil)
|
||||
request = request.WithContext(deviceauth.WithIdentity(request.Context(), deviceauth.Identity{ID: "device-internal", TenantID: "tenant-internal"}))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusTooManyRequests || recorder.Header().Get("X-RateLimit-Scope") != "device" {
|
||||
t.Fatalf("status=%d headers=%v", recorder.Code, recorder.Header())
|
||||
}
|
||||
if strings.Join(limiter.scopes, ",") != "device:device-internal" {
|
||||
t.Fatalf("scopes=%q", limiter.scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitSkipsHealthAndFailsOpen(t *testing.T) {
|
||||
limiter := &fakeRateLimiter{err: errors.New("redis unavailable")}
|
||||
downstreamCalls := 0
|
||||
handler := Middleware(slog.Default(), "test", 1024, limiter, http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
downstreamCalls++
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
for _, path := range []string{"/health/live", "/api/v1/items"} {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Fatalf("path %s status = %d", path, recorder.Code)
|
||||
}
|
||||
}
|
||||
if downstreamCalls != 2 || limiter.calls != 1 {
|
||||
t.Fatalf("downstream calls = %d, limiter calls = %d", downstreamCalls, limiter.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimitRejectsKnownOversizedRequestBeforeHandler(t *testing.T) {
|
||||
called := false
|
||||
handler := Middleware(slog.Default(), "test", 16, nil, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/items", strings.NewReader(strings.Repeat("x", 17)))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("status=%d called=%t body=%s", recorder.Code, called, recorder.Body.String())
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), "REQUEST_BODY_TOO_LARGE") {
|
||||
t.Fatalf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user