ops_queue_sanitize_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package service
  2. import (
  3. "strings"
  4. "testing"
  5. )
  6. func TestSanitizeOpsUpstreamErrorsForQueueBoundsAndRedacts(t *testing.T) {
  7. entry := &OpsInsertErrorLogInput{}
  8. for i := 0; i < 20; i++ {
  9. proxyID := int64(i + 1)
  10. entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
  11. ProxyID: &proxyID,
  12. ProxyName: "proxy-" + string(rune('a'+i)),
  13. Platform: strings.Repeat("p", 100),
  14. AccountName: strings.Repeat("a", 300),
  15. UpstreamStatusCode: 500,
  16. UpstreamURL: strings.Repeat("u", 3000),
  17. UpstreamResponseBody: `{"authorization":"Bearer secret","message":"` + strings.Repeat("x", 10_000) + `"}`,
  18. Message: strings.Repeat("m", 3000),
  19. Detail: `{"api_key":"secret","detail":"` + strings.Repeat("y", 10_000) + `"}`,
  20. })
  21. }
  22. if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
  23. t.Fatal(err)
  24. }
  25. if entry.UpstreamErrors != nil {
  26. t.Fatal("raw upstream event slice must be released before queueing")
  27. }
  28. if entry.UpstreamErrorsJSON == nil {
  29. t.Fatal("sanitized upstream event JSON is missing")
  30. }
  31. events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
  32. if err != nil {
  33. t.Fatal(err)
  34. }
  35. if len(events) != 20 {
  36. t.Fatalf("event count = %d, want 20", len(events))
  37. }
  38. for i, event := range events {
  39. if event.ProxyID == nil || *event.ProxyID != int64(i+1) {
  40. t.Fatalf("event %d proxy id = %v, want %d", i, event.ProxyID, i+1)
  41. }
  42. if len(event.Platform) > 32 || len(event.AccountName) > 128 || len(event.UpstreamURL) > 2048 || len(event.Message) > 2048 {
  43. t.Fatalf("event fields were not bounded: %+v", event)
  44. }
  45. if i < 4 && (len(event.UpstreamURL) > opsUpstreamErrorsOlderURLMaxLen || len(event.Message) > opsUpstreamErrorsOlderMessageMaxLen) {
  46. t.Fatalf("older event %d kept a full-size url/message: url=%d message=%d", i, len(event.UpstreamURL), len(event.Message))
  47. }
  48. if event.DroppedEarlierAttempts != 0 {
  49. t.Fatalf("event %d reported dropped attempts without any drop: %d", i, event.DroppedEarlierAttempts)
  50. }
  51. if len(event.UpstreamResponseBody) > OpsErrorLogQueueBodyMaxBytes || len(event.Detail) > OpsErrorLogQueueBodyMaxBytes {
  52. t.Fatal("event body/detail exceeded queue limit")
  53. }
  54. if strings.Contains(event.UpstreamResponseBody, "Bearer secret") || strings.Contains(event.Detail, `"secret"`) {
  55. t.Fatal("credential material was not redacted")
  56. }
  57. if i < 4 && (event.UpstreamResponseBody != "" || event.Detail != "") {
  58. t.Fatal("older event retained a large body outside the queue body window")
  59. }
  60. }
  61. }
  62. func TestSanitizeOpsUpstreamErrorsForQueueHardBoundsEventCount(t *testing.T) {
  63. entry := &OpsInsertErrorLogInput{}
  64. total := opsUpstreamErrorsMaxEvents + 40
  65. for i := 0; i < total; i++ {
  66. entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
  67. AtUnixMs: int64(i + 1),
  68. ProxyName: opsProxyNameDirect,
  69. UpstreamStatusCode: 500,
  70. Message: "attempt",
  71. })
  72. }
  73. if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
  74. t.Fatal(err)
  75. }
  76. events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
  77. if err != nil {
  78. t.Fatal(err)
  79. }
  80. if len(events) != opsUpstreamErrorsMaxEvents {
  81. t.Fatalf("event count = %d, want %d", len(events), opsUpstreamErrorsMaxEvents)
  82. }
  83. // Newest attempts win; the oldest retained one carries the drop count.
  84. if events[len(events)-1].AtUnixMs != int64(total) {
  85. t.Fatalf("newest event lost: at=%d want %d", events[len(events)-1].AtUnixMs, total)
  86. }
  87. if events[0].AtUnixMs != int64(total-opsUpstreamErrorsMaxEvents+1) {
  88. t.Fatalf("oldest retained at=%d", events[0].AtUnixMs)
  89. }
  90. if events[0].DroppedEarlierAttempts != 40 {
  91. t.Fatalf("dropped_earlier_attempts = %d, want 40", events[0].DroppedEarlierAttempts)
  92. }
  93. if events[1].DroppedEarlierAttempts != 0 {
  94. t.Fatal("drop count must only be stamped on the oldest retained event")
  95. }
  96. }
  97. func TestSanitizeOpsUpstreamErrorsForQueueHardBoundsSerializedBytes(t *testing.T) {
  98. entry := &OpsInsertErrorLogInput{}
  99. // Every bounded field at its cap: the 16 newest attempts weigh ~20KB each
  100. // (8KB body + 8KB detail + 2KB url + 2KB message), older ones ~2KB. At the
  101. // count cap that is well past the byte budget, so bytes must win here.
  102. const total = opsUpstreamErrorsMaxEvents
  103. for i := 0; i < total; i++ {
  104. entry.UpstreamErrors = append(entry.UpstreamErrors, &OpsUpstreamErrorEvent{
  105. AtUnixMs: int64(i + 1),
  106. Platform: strings.Repeat("p", 40),
  107. AccountName: strings.Repeat("a", 200),
  108. ProxyName: strings.Repeat("n", 200),
  109. UpstreamRequestID: strings.Repeat("r", 200),
  110. UpstreamStatusCode: 502,
  111. UpstreamURL: "https://example.com/" + strings.Repeat("u", 3000),
  112. // Plain-text payloads are truncated, not JSON-trimmed, so they keep
  113. // their full queue-limit weight.
  114. UpstreamResponseBody: strings.Repeat("b", 10_000),
  115. Kind: strings.Repeat("k", 80),
  116. Stage: strings.Repeat("s", 80),
  117. Scope: strings.Repeat("c", 80),
  118. Reason: strings.Repeat("e", 200),
  119. Message: strings.Repeat("m", 3000),
  120. Detail: strings.Repeat("d", 10_000),
  121. })
  122. }
  123. if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
  124. t.Fatal(err)
  125. }
  126. // The newest event is always kept even when it alone exceeds the remaining
  127. // budget, so allow one max-size event of slack.
  128. if len(*entry.UpstreamErrorsJSON) > opsUpstreamErrorsQueueMaxBytes+24*1024 {
  129. t.Fatalf("serialized upstream_errors = %d bytes, budget %d", len(*entry.UpstreamErrorsJSON), opsUpstreamErrorsQueueMaxBytes)
  130. }
  131. events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
  132. if err != nil {
  133. t.Fatal(err)
  134. }
  135. if len(events) == 0 || len(events) >= total {
  136. t.Fatalf("expected the byte budget to drop some events, kept %d of %d", len(events), total)
  137. }
  138. if events[len(events)-1].AtUnixMs != int64(total) {
  139. t.Fatal("newest attempt must always be retained")
  140. }
  141. if events[0].DroppedEarlierAttempts != total-len(events) {
  142. t.Fatalf("dropped_earlier_attempts = %d, want %d", events[0].DroppedEarlierAttempts, total-len(events))
  143. }
  144. }
  145. func TestSanitizeOpsUpstreamErrorsForQueueDropsEmptyButKeepsOlderTrimmedEvents(t *testing.T) {
  146. entry := &OpsInsertErrorLogInput{}
  147. // 20 events: the first 4 fall outside the body window. Event 0 is a real
  148. // attempt whose only payload is a detail (status 0, no message); it must
  149. // survive the window even though its detail is cleared. Event 1 is truly
  150. // empty and must be dropped.
  151. for i := 0; i < 20; i++ {
  152. ev := &OpsUpstreamErrorEvent{AtUnixMs: int64(i + 1), ProxyName: opsProxyNameDirect}
  153. switch i {
  154. case 0:
  155. ev.Detail = "proxy handshake failed"
  156. case 1:
  157. // fully empty
  158. default:
  159. ev.UpstreamStatusCode = 500
  160. }
  161. entry.UpstreamErrors = append(entry.UpstreamErrors, ev)
  162. }
  163. if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
  164. t.Fatal(err)
  165. }
  166. events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
  167. if err != nil {
  168. t.Fatal(err)
  169. }
  170. if len(events) != 19 {
  171. t.Fatalf("event count = %d, want 19", len(events))
  172. }
  173. if events[0].AtUnixMs != 1 || events[0].Detail != "" {
  174. t.Fatalf("detail-only older attempt must be kept with detail cleared: %+v", events[0])
  175. }
  176. if events[1].AtUnixMs != 3 {
  177. t.Fatalf("fully-empty attempt must be dropped, got at=%d", events[1].AtUnixMs)
  178. }
  179. }
  180. func TestSanitizeOpsUpstreamErrorsForQueueIgnoresCallerSuppliedDropCount(t *testing.T) {
  181. entry := &OpsInsertErrorLogInput{UpstreamErrors: []*OpsUpstreamErrorEvent{
  182. {UpstreamStatusCode: 500, ProxyName: opsProxyNameDirect, DroppedEarlierAttempts: 99},
  183. }}
  184. if err := SanitizeOpsUpstreamErrorsForQueue(entry); err != nil {
  185. t.Fatal(err)
  186. }
  187. events, err := ParseOpsUpstreamErrors(*entry.UpstreamErrorsJSON)
  188. if err != nil {
  189. t.Fatal(err)
  190. }
  191. if len(events) != 1 || events[0].DroppedEarlierAttempts != 0 {
  192. t.Fatalf("caller-supplied drop count must be reset: %+v", events[0])
  193. }
  194. }