Files
sub2api/backend/internal/service/ops_queue_sanitize_test.go
T

202 lines
7.8 KiB
Go

package service
import (
"strings"
"testing"
)
func TestSanitizeOpsUpstreamErrorsForQueueBoundsAndRedacts(t *testing.T) {
entry := &OpsInsertErrorLogInput{}
for i := 0; i < 20; i++ {
proxyID := int64(i + 1)
entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
ProxyID: &proxyID,
ProxyName: "proxy-" + string(rune('a'+i)),
Platform: strings.Repeat("p", 100),
AccountName: strings.Repeat("a", 300),
UpstreamStatusCode: 500,
UpstreamURL: strings.Repeat("u", 3000),
UpstreamResponseBody: `{"authorization":"Bearer secret","message":"` + strings.Repeat("x", 10_000) + `"}`,
Message: strings.Repeat("m", 3000),
Detail: `{"api_key":"secret","detail":"` + strings.Repeat("y", 10_000) + `"}`,
})
}
if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
t.Fatal(err)
}
if entry.UpstreamErrors != nil {
t.Fatal("raw upstream event slice must be released before queueing")
}
if entry.UpstreamErrorsJSON == nil {
t.Fatal("sanitized upstream event JSON is missing")
}
events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
if err != nil {
t.Fatal(err)
}
if len(events) != 20 {
t.Fatalf("event count = %d, want 20", len(events))
}
for i, event := range events {
if event.ProxyID == nil || *event.ProxyID != int64(i+1) {
t.Fatalf("event %d proxy id = %v, want %d", i, event.ProxyID, i+1)
}
if len(event.Platform) > 32 || len(event.AccountName) > 128 || len(event.UpstreamURL) > 2048 || len(event.Message) > 2048 {
t.Fatalf("event fields were not bounded: %+v", event)
}
if i < 4 && (len(event.UpstreamURL) > opsUpstreamErrorsOlderURLMaxLen || len(event.Message) > opsUpstreamErrorsOlderMessageMaxLen) {
t.Fatalf("older event %d kept a full-size url/message: url=%d message=%d", i, len(event.UpstreamURL), len(event.Message))
}
if event.DroppedEarlierAttempts != 0 {
t.Fatalf("event %d reported dropped attempts without any drop: %d", i, event.DroppedEarlierAttempts)
}
if len(event.UpstreamResponseBody) > OpsErrorLogQueueBodyMaxBytes || len(event.Detail) > OpsErrorLogQueueBodyMaxBytes {
t.Fatal("event body/detail exceeded queue limit")
}
if strings.Contains(event.UpstreamResponseBody, "Bearer secret") || strings.Contains(event.Detail, `"secret"`) {
t.Fatal("credential material was not redacted")
}
if i < 4 && (event.UpstreamResponseBody != "" || event.Detail != "") {
t.Fatal("older event retained a large body outside the queue body window")
}
}
}
func TestSanitizeOpsUpstreamErrorsForQueueHardBoundsEventCount(t *testing.T) {
entry := &OpsInsertErrorLogInput{}
total := opsUpstreamErrorsMaxEvents + 40
for i := 0; i < total; i++ {
entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
AtUnixMs: int64(i + 1),
ProxyName: opsProxyNameDirect,
UpstreamStatusCode: 500,
Message: "attempt",
})
}
if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
t.Fatal(err)
}
events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
if err != nil {
t.Fatal(err)
}
if len(events) != opsUpstreamErrorsMaxEvents {
t.Fatalf("event count = %d, want %d", len(events), opsUpstreamErrorsMaxEvents)
}
// Newest attempts win; the oldest retained one carries the drop count.
if events[len(events)-1].AtUnixMs != int64(total) {
t.Fatalf("newest event lost: at=%d want %d", events[len(events)-1].AtUnixMs, total)
}
if events[0].AtUnixMs != int64(total-opsUpstreamErrorsMaxEvents+1) {
t.Fatalf("oldest retained at=%d", events[0].AtUnixMs)
}
if events[0].DroppedEarlierAttempts != 40 {
t.Fatalf("dropped_earlier_attempts = %d, want 40", events[0].DroppedEarlierAttempts)
}
if events[1].DroppedEarlierAttempts != 0 {
t.Fatal("drop count must only be stamped on the oldest retained event")
}
}
func TestSanitizeOpsUpstreamErrorsForQueueHardBoundsSerializedBytes(t *testing.T) {
entry := &OpsInsertErrorLogInput{}
// Every bounded field at its cap: the 16 newest attempts weigh ~20KB each
// (8KB body + 8KB detail + 2KB url + 2KB message), older ones ~2KB. At the
// count cap that is well past the byte budget, so bytes must win here.
const total = opsUpstreamErrorsMaxEvents
for i := 0; i < total; i++ {
entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
AtUnixMs: int64(i + 1),
Platform: strings.Repeat("p", 40),
AccountName: strings.Repeat("a", 200),
ProxyName: strings.Repeat("n", 200),
UpstreamRequestID: strings.Repeat("r", 200),
UpstreamStatusCode: 502,
UpstreamURL: "https://example.com/" + strings.Repeat("u", 3000),
// Plain-text payloads are truncated, not JSON-trimmed, so they keep
// their full queue-limit weight.
UpstreamResponseBody: strings.Repeat("b", 10_000),
Kind: strings.Repeat("k", 80),
Stage: strings.Repeat("s", 80),
Scope: strings.Repeat("c", 80),
Reason: strings.Repeat("e", 200),
Message: strings.Repeat("m", 3000),
Detail: strings.Repeat("d", 10_000),
})
}
if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
t.Fatal(err)
}
// The newest event is always kept even when it alone exceeds the remaining
// budget, so allow one max-size event of slack.
if len(*entry.UpstreamErrorsJSON) > opsUpstreamErrorsQueueMaxBytes+24*1024 {
t.Fatalf("serialized upstream_errors = %d bytes, budget %d", len(*entry.UpstreamErrorsJSON), opsUpstreamErrorsQueueMaxBytes)
}
events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
if err != nil {
t.Fatal(err)
}
if len(events) == 0 || len(events) >= total {
t.Fatalf("expected the byte budget to drop some events, kept %d of %d", len(events), total)
}
if events[len(events)-1].AtUnixMs != int64(total) {
t.Fatal("newest attempt must always be retained")
}
if events[0].DroppedEarlierAttempts != total-len(events) {
t.Fatalf("dropped_earlier_attempts = %d, want %d", events[0].DroppedEarlierAttempts, total-len(events))
}
}
func TestSanitizeOpsUpstreamErrorsForQueueDropsEmptyButKeepsOlderTrimmedEvents(t *testing.T) {
entry := &OpsInsertErrorLogInput{}
// 20 events: the first 4 fall outside the body window. Event 0 is a real
// attempt whose only payload is a detail (status 0, no message); it must
// survive the window even though its detail is cleared. Event 1 is truly
// empty and must be dropped.
for i := 0; i < 20; i++ {
ev := &OpsUpstreamErrorEvent{AtUnixMs: int64(i + 1), ProxyName: opsProxyNameDirect}
switch i {
case 0:
ev.Detail = "proxy handshake failed"
case 1:
// fully empty
default:
ev.UpstreamStatusCode = 500
}
entry.UpstreamErrors = append(entry.UpstreamErrors, ev)
}
if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
t.Fatal(err)
}
events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
if err != nil {
t.Fatal(err)
}
if len(events) != 19 {
t.Fatalf("event count = %d, want 19", len(events))
}
if events[0].AtUnixMs != 1 || events[0].Detail != "" {
t.Fatalf("detail-only older attempt must be kept with detail cleared: %+v", events[0])
}
if events[1].AtUnixMs != 3 {
t.Fatalf("fully-empty attempt must be dropped, got at=%d", events[1].AtUnixMs)
}
}
func TestSanitizeOpsUpstreamErrorsForQueueIgnoresCallerSuppliedDropCount(t *testing.T) {
entry := &OpsInsertErrorLogInput{UpstreamErrors: []*OpsUpstreamErrorEvent{
{UpstreamStatusCode: 500, ProxyName: opsProxyNameDirect, DroppedEarlierAttempts: 99},
}}
if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
t.Fatal(err)
}
events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
if err != nil {
t.Fatal(err)
}
if len(events) != 1 || events[0].DroppedEarlierAttempts != 0 {
t.Fatalf("caller-supplied drop count must be reset: %+v", events[0])
}
}