check / check (push) Successful in 58s
POST /api/v1/reports stays unauthenticated but is bounded. Each client address, as the trusted-proxy logic resolves it, may send REPORTS_PER_MINUTE reports a minute (default 60, all at once if it likes), using golang.org/x/time/rate; past that it gets 429 with Retry-After. Buckets that have refilled are dropped once a minute, so idle addresses do not pile up. reportbuf refuses a report that would take the report files past DATA_DIR_MAX_BYTES (default 1 GiB) with ErrFull, answered with 507; the count starts from the files already in DATA_DIR, and reports not yet written count at their uncompressed size. CORS adds nothing unless CORS_ALLOWED_ORIGINS lists origins. A limit that is not a positive number stops the server from starting. Model: opus-5-5
116 lines
2.7 KiB
Go
116 lines
2.7 KiB
Go
package server_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/netwatch/internal/config"
|
|
"sneak.berlin/go/netwatch/internal/globals"
|
|
"sneak.berlin/go/netwatch/internal/handlers"
|
|
"sneak.berlin/go/netwatch/internal/healthcheck"
|
|
"sneak.berlin/go/netwatch/internal/logger"
|
|
"sneak.berlin/go/netwatch/internal/middleware"
|
|
"sneak.berlin/go/netwatch/internal/reportbuf"
|
|
"sneak.berlin/go/netwatch/internal/server"
|
|
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
)
|
|
|
|
// newServer builds the server from the same constructors as main,
|
|
// never started: SetupRoutes is called directly, so nothing listens.
|
|
func newServer(t *testing.T) *server.Server {
|
|
t.Helper()
|
|
|
|
var srv *server.Server
|
|
|
|
app := fxtest.New(t,
|
|
fx.Provide(
|
|
config.New,
|
|
globals.New,
|
|
handlers.New,
|
|
healthcheck.New,
|
|
logger.New,
|
|
middleware.New,
|
|
reportbuf.New,
|
|
server.New,
|
|
),
|
|
fx.Populate(&srv),
|
|
)
|
|
|
|
err := app.Err()
|
|
if err != nil {
|
|
t.Fatalf("build server: %v", err)
|
|
}
|
|
|
|
srv.SetupRoutes()
|
|
|
|
return srv
|
|
}
|
|
|
|
// TestReportsAreRateLimited checks that POST /api/v1/reports is
|
|
// behind the per-address rate limit, set here to two a minute.
|
|
func TestReportsAreRateLimited(t *testing.T) {
|
|
t.Setenv("REPORTS_PER_MINUTE", "2")
|
|
|
|
srv := newServer(t)
|
|
|
|
post := func() int {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequestWithContext(t.Context(),
|
|
http.MethodPost, "/api/v1/reports",
|
|
strings.NewReader(`{"clientId":"c1","hosts":[]}`),
|
|
)
|
|
srv.ServeHTTP(rec, req)
|
|
|
|
return rec.Code
|
|
}
|
|
|
|
for i := range 2 {
|
|
if code := post(); code != http.StatusOK {
|
|
t.Fatalf("report %d: status = %d, want %d",
|
|
i+1, code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
if code := post(); code != http.StatusTooManyRequests {
|
|
t.Fatalf("third report in a minute: status = %d, want %d",
|
|
code, http.StatusTooManyRequests)
|
|
}
|
|
}
|
|
|
|
// TestHealthCheckRejectsOversizeBody sends the health check, which
|
|
// never reads its body, a body one byte over the limit. Only the
|
|
// router-wide body limit can reject it.
|
|
func TestHealthCheckRejectsOversizeBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := newServer(t)
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequestWithContext(t.Context(),
|
|
http.MethodGet, "/.well-known/healthcheck",
|
|
strings.NewReader(
|
|
strings.Repeat("x", int(server.MaxRequestBodyBytes)+1),
|
|
),
|
|
)
|
|
|
|
srv.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want %d",
|
|
rec.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
if got := rec.Body.String(); got != "{\"status\":\"error\"}\n" {
|
|
t.Errorf("body = %q, want %q", got, "{\"status\":\"error\"}\n")
|
|
}
|
|
|
|
got := rec.Header().Get("Content-Type")
|
|
if got != "application/json; charset=utf-8" {
|
|
t.Errorf("Content-Type = %q, want a JSON content type", got)
|
|
}
|
|
}
|