Files
ARTEX/notify/http_test.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

128 lines
4.7 KiB
Go

package notify
import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
)
// This file covers doJSON's HTTP-layer error classification.
//
// Why test it separately: each channel adapter only handles its own platform's
// business error codes (DingTalk errcode, Feishu code, Telegram ok field), while
// the **HTTP-layer** classification is done uniformly by doJSON — two independent
// lines of defense. Without this one, a relay gateway returning 503 would be
// treated as a permanent failure and retries abandoned outright; and a 403 would
// be treated as retryable, backing off three rounds for nothing.
func replyServer(t *testing.T, status int, body string) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
return srv
}
func TestDoJSONClassifiesHTTPStatus(t *testing.T) {
cases := []struct {
name string
status int
permanent bool
}{
{"200 success is not an error", 200, false},
{"429 rate limit is retryable", 429, false},
{"408 request timeout is retryable", 408, false},
{"500 server error is retryable", 500, false},
{"502 gateway error is retryable", 502, false},
{"503 service unavailable is retryable", 503, false},
{"400 bad parameters is permanent", 400, true},
{"401 auth failure is permanent", 401, true},
{"403 forbidden is permanent", 403, true},
{"404 not found is permanent", 404, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := replyServer(t, tc.status, `{"detail":"upstream says no"}`)
_, err := doJSON(context.Background(), "GET", srv.URL, nil, nil)
if tc.status < 300 {
if err != nil {
t.Fatalf("2xx should not error: %v", err)
}
return
}
if err == nil {
t.Fatal("non-2xx should error")
}
if got := IsPermanent(err); got != tc.permanent {
t.Fatalf("HTTP %d permanent classification wrong: want %v got %v (%v)",
tc.status, tc.permanent, got, err)
}
// The status code must appear in the error, otherwise the user cannot
// tell their own misconfiguration from a peer outage. Assert the
// number rather than Go's English StatusText: the wording of this
// package's messages may change, while the number is the
// language-independent, stable part to assert on.
if !strings.Contains(err.Error(), strconv.Itoa(tc.status)) {
t.Errorf("error message should carry HTTP status code %d, got %v", tc.status, err)
}
})
}
}
// TestDoJSONIncludesResponseSnippet covers snippet: the peer's error description
// must come back, otherwise the user only knows "it failed" without knowing why
// the peer refused.
func TestDoJSONIncludesResponseSnippet(t *testing.T) {
srv := replyServer(t, 400, `{"error":"invalid webhook token"}`)
_, err := doJSON(context.Background(), "GET", srv.URL, nil, nil)
if err == nil {
t.Fatal("should error")
}
if !strings.Contains(err.Error(), "invalid webhook token") {
t.Errorf("error message should bring back the peer's description, got %v", err)
}
}
// TestDoJSONSnippetIsSingleLineAndBounded constrains the shape of snippet: the
// peer response goes verbatim into the last_error column and the frontend table,
// and multiple lines / excessive length would break layout and payload.
func TestDoJSONSnippetIsSingleLineAndBounded(t *testing.T) {
// A response with newlines, tabs, and 5000 characters of oversized content.
long := strings.Repeat("x", 5000)
srv := replyServer(t, 500, "line1\nline2\r\n\tline3 "+long)
_, err := doJSON(context.Background(), "GET", srv.URL, nil, nil)
if err == nil {
t.Fatal("should error")
}
msg := err.Error()
if strings.ContainsAny(msg, "\r\n\t") {
t.Errorf("error message should be compressed to a single line, got %q", msg)
}
// snippet cap of 200 characters + fixed prefix; the total must be far smaller
// than the original response.
if len(msg) > 400 {
t.Errorf("error message too long (%d bytes), should have been truncated by snippet: %q", len(msg), msg)
}
}
// TestDoJSONRejectsOversizedResponse confirms reads are capped: when a peer
// abnormally returns oversized content, the whole response must not be read into
// memory (every entry in the delivery history stores a copy of last_error).
func TestDoJSONRejectsOversizedResponse(t *testing.T) {
huge := strings.Repeat("A", 1<<20) // 1 MiB
srv := replyServer(t, 400, huge)
_, err := doJSON(context.Background(), "GET", srv.URL, nil, nil)
if err == nil {
t.Fatal("should error")
}
if len(err.Error()) > 400 {
t.Errorf("oversized responses should be read length-capped and truncated, error message length %d", len(err.Error()))
}
}