package api_test import ( "bytes" "net/http" "strings" "testing" "git.ryuvia.com/niklas/terdut-server/internal/api" ) // --------------------------------------------------------------------------- // Security headers // --------------------------------------------------------------------------- func TestSecurityHeaders_NosniffAlwaysSet(t *testing.T) { s := newTS(t) // no PublicURL: the HTTPS signal is off resp := s.req(t, http.MethodGet, "/api/me", nil) defer resp.Body.Close() if got := resp.Header.Get("X-Content-Type-Options"); got != "nosniff" { t.Errorf("X-Content-Type-Options = %q, want nosniff", got) } if got := resp.Header.Get("Strict-Transport-Security"); got != "" { t.Errorf("Strict-Transport-Security = %q, want unset without an https PublicURL", got) } } func TestSecurityHeaders_HSTSWhenPublicURLIsHTTPS(t *testing.T) { s := newTS(t, api.NotifyConfig{PublicURL: "https://terdut.example.com"}) resp := s.req(t, http.MethodGet, "/api/me", nil) defer resp.Body.Close() got := resp.Header.Get("Strict-Transport-Security") if !strings.HasPrefix(got, "max-age=") || !strings.Contains(got, "includeSubDomains") { t.Errorf("Strict-Transport-Security = %q, want a max-age with includeSubDomains", got) } } // --------------------------------------------------------------------------- // Request body size limits // --------------------------------------------------------------------------- // TestBodySizeLimit_OrdinaryEndpointRejectsOversizedBody confirms an // unauthenticated endpoint can't be made to buffer an arbitrarily large body: // past maxBodyBytes, decodeJSON fails exactly as it would on any other // malformed body, rather than the server reading the whole thing first. func TestBodySizeLimit_OrdinaryEndpointRejectsOversizedBody(t *testing.T) { s := newTS(t) huge := bytes.Repeat([]byte("a"), 2<<20) // 2 MiB, past the 1 MiB default body := []byte(`{"username":"` + string(huge) + `","password":"x"}`) resp, err := http.Post(s.URL+"/api/login", "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("POST /api/login: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("status = %d, want %d (oversized body treated as invalid)", resp.StatusCode, http.StatusBadRequest) } } // TestBodySizeLimit_WebhookAllowsLargerBodyThanDefault confirms the // Alertmanager webhook's separate, larger cap actually takes effect: a body // bigger than the ordinary default but within maxWebhookBodyBytes is still // accepted, not rejected by the smaller limit every other endpoint gets. func TestBodySizeLimit_WebhookAllowsLargerBodyThanDefault(t *testing.T) { s := newTS(t) // Padding kept inside one alert's annotation, comfortably past the 1 MiB // default and still well under the webhook's 8 MiB cap. padding := strings.Repeat("a", 3<<20) // 3 MiB payload := `{"version":"4","status":"firing","groupKey":"big-group",` + `"groupLabels":{"alertname":"BigAlert"},"alerts":[{"status":"firing",` + `"labels":{"alertname":"BigAlert"},"annotations":{"note":"` + padding + `"},` + `"startsAt":"2026-05-20T10:00:00Z","endsAt":"0001-01-01T00:00:00Z",` + `"fingerprint":"fp-big"}]}` resp, err := http.Post(s.URL+"/api/integrations/"+s.ingestKey+"/alertmanager", "application/json", strings.NewReader(payload)) if err != nil { t.Fatalf("POST webhook: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d (body under the webhook's own cap)", resp.StatusCode, http.StatusOK) } } // TestBodySizeLimit_WebhookRejectsPastItsOwnCap confirms the webhook's larger // cap is still a cap, not an exemption from one. func TestBodySizeLimit_WebhookRejectsPastItsOwnCap(t *testing.T) { s := newTS(t) huge := strings.Repeat("a", 9<<20) // 9 MiB, past the 8 MiB webhook cap payload := `{"version":"4","status":"firing","groupKey":"huge-group",` + `"groupLabels":{"alertname":"HugeAlert"},"alerts":[{"status":"firing",` + `"labels":{"alertname":"HugeAlert"},"annotations":{"note":"` + huge + `"},` + `"startsAt":"2026-05-20T10:00:00Z","endsAt":"0001-01-01T00:00:00Z",` + `"fingerprint":"fp-huge"}]}` resp, err := http.Post(s.URL+"/api/integrations/"+s.ingestKey+"/alertmanager", "application/json", strings.NewReader(payload)) if err != nil { t.Fatalf("POST webhook: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("status = %d, want %d (body past the webhook's own cap)", resp.StatusCode, http.StatusBadRequest) } }