mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
128 lines
4.7 KiB
Go
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()))
|
|
}
|
|
}
|