ops_service_batch_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestOpsServiceRecordErrorBatch_SanitizesAndBatches(t *testing.T) {
  10. t.Parallel()
  11. var captured []*OpsInsertErrorLogInput
  12. repo := &opsRepoMock{
  13. BatchInsertErrorLogsFn: func(ctx context.Context, inputs []*OpsInsertErrorLogInput) (int64, error) {
  14. captured = append(captured, inputs...)
  15. return int64(len(inputs)), nil
  16. },
  17. }
  18. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  19. msg := " upstream failed: https://example.com?access_token=secret-value "
  20. detail := `{"authorization":"Bearer secret-token"}`
  21. entries := []*OpsInsertErrorLogInput{
  22. {
  23. ErrorBody: `{"error":"bad","access_token":"secret"}`,
  24. UpstreamStatusCode: intPtr(-10),
  25. UpstreamErrorMessage: strPtr(msg),
  26. UpstreamErrorDetail: strPtr(detail),
  27. UpstreamErrors: []*OpsUpstreamErrorEvent{
  28. {
  29. AccountID: -2,
  30. UpstreamStatusCode: 429,
  31. Message: " token leaked ",
  32. Detail: `{"refresh_token":"secret"}`,
  33. },
  34. },
  35. },
  36. {
  37. ErrorPhase: "upstream",
  38. ErrorType: "upstream_error",
  39. CreatedAt: time.Now().UTC(),
  40. },
  41. }
  42. require.NoError(t, svc.RecordErrorBatch(context.Background(), entries))
  43. require.Len(t, captured, 2)
  44. first := captured[0]
  45. require.Equal(t, "internal", first.ErrorPhase)
  46. require.Equal(t, "api_error", first.ErrorType)
  47. require.Nil(t, first.UpstreamStatusCode)
  48. require.NotNil(t, first.UpstreamErrorMessage)
  49. require.NotContains(t, *first.UpstreamErrorMessage, "secret-value")
  50. require.Contains(t, *first.UpstreamErrorMessage, "access_token=***")
  51. require.NotNil(t, first.UpstreamErrorDetail)
  52. require.NotContains(t, *first.UpstreamErrorDetail, "secret-token")
  53. require.NotContains(t, first.ErrorBody, "secret")
  54. require.Nil(t, first.UpstreamErrors)
  55. require.NotNil(t, first.UpstreamErrorsJSON)
  56. require.NotContains(t, *first.UpstreamErrorsJSON, "secret")
  57. require.Contains(t, *first.UpstreamErrorsJSON, "[REDACTED]")
  58. second := captured[1]
  59. require.Equal(t, "upstream", second.ErrorPhase)
  60. require.Equal(t, "upstream_error", second.ErrorType)
  61. require.False(t, second.CreatedAt.IsZero())
  62. }
  63. func TestOpsServiceRecordErrorBatch_DoesNotFallbackToSingleInsertsWhenBatchFails(t *testing.T) {
  64. t.Parallel()
  65. var (
  66. batchCalls int
  67. singleCalls int
  68. )
  69. repo := &opsRepoMock{
  70. BatchInsertErrorLogsFn: func(ctx context.Context, inputs []*OpsInsertErrorLogInput) (int64, error) {
  71. batchCalls++
  72. return 0, errors.New("batch failed")
  73. },
  74. InsertErrorLogFn: func(ctx context.Context, input *OpsInsertErrorLogInput) (int64, error) {
  75. singleCalls++
  76. return int64(singleCalls), nil
  77. },
  78. }
  79. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  80. err := svc.RecordErrorBatch(context.Background(), []*OpsInsertErrorLogInput{
  81. {ErrorMessage: "first"},
  82. {ErrorMessage: "second"},
  83. })
  84. require.Error(t, err)
  85. require.Equal(t, 1, batchCalls)
  86. require.Zero(t, singleCalls)
  87. }
  88. func TestOpsServiceRecordErrorPersistsExplicitAccountAuthStatusZero(t *testing.T) {
  89. t.Parallel()
  90. var captured *OpsInsertErrorLogInput
  91. repo := &opsRepoMock{
  92. InsertErrorLogFn: func(_ context.Context, input *OpsInsertErrorLogInput) (int64, error) {
  93. captured = input
  94. return 1, nil
  95. },
  96. }
  97. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  98. staleStatus := 403
  99. staleMessage := "stale inference message"
  100. staleDetail := "stale inference detail"
  101. err := svc.RecordError(context.Background(), &OpsInsertErrorLogInput{
  102. ErrorPhase: "upstream",
  103. ErrorType: "upstream_error",
  104. ErrorOwner: "provider",
  105. ErrorSource: "upstream_http",
  106. UpstreamStatusCode: &staleStatus,
  107. UpstreamErrorMessage: &staleMessage,
  108. UpstreamErrorDetail: &staleDetail,
  109. UpstreamErrors: []*OpsUpstreamErrorEvent{
  110. {Stage: string(GatewayFailureStageInference), UpstreamStatusCode: 403, Message: staleMessage, Detail: staleDetail},
  111. {
  112. Stage: string(GatewayFailureStageAccountAuth), Scope: string(GatewayFailureScopeAccount),
  113. Reason: string(GrokCredentialReasonRevoked), Message: "Grok OAuth credentials require account action",
  114. },
  115. },
  116. })
  117. require.NoError(t, err)
  118. require.NotNil(t, captured)
  119. require.Equal(t, "account_auth", captured.ErrorPhase)
  120. require.Equal(t, "provider", captured.ErrorOwner)
  121. require.Equal(t, "gateway", captured.ErrorSource)
  122. require.NotNil(t, captured.UpstreamStatusCode)
  123. require.Zero(t, *captured.UpstreamStatusCode)
  124. require.NotNil(t, captured.UpstreamErrorMessage)
  125. require.Equal(t, "Grok OAuth credentials require account action", *captured.UpstreamErrorMessage)
  126. require.Nil(t, captured.UpstreamErrorDetail)
  127. require.Nil(t, captured.UpstreamErrors)
  128. require.NotNil(t, captured.UpstreamErrorsJSON)
  129. require.Contains(t, *captured.UpstreamErrorsJSON, `"upstream_status_code":403`)
  130. require.Contains(t, *captured.UpstreamErrorsJSON, `"stage":"account_auth"`)
  131. }
  132. func strPtr(v string) *string {
  133. return &v
  134. }