ops_user_error_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "time"
  7. )
  8. func TestMapUserErrorCategory(t *testing.T) {
  9. cases := []struct {
  10. phase, etype, want string
  11. }{
  12. {"auth", "authentication_error", "auth"},
  13. {"request", "rate_limit_error", "rate_limit"},
  14. {"request", "billing_error", "quota"},
  15. {"request", "subscription_error", "quota"},
  16. {"request", "invalid_request_error", "invalid_request"},
  17. {"routing", "api_error", "service_unavailable"},
  18. {"account_auth", "upstream_error", "upstream"},
  19. {"upstream", "upstream_error", "upstream"},
  20. {"network", "api_error", "upstream"},
  21. {"internal", "api_error", "internal"},
  22. {"weird", "weird", "other"},
  23. }
  24. for _, c := range cases {
  25. if got := MapUserErrorCategory(c.phase, c.etype); got != c.want {
  26. t.Errorf("MapUserErrorCategory(%q,%q)=%q want %q", c.phase, c.etype, got, c.want)
  27. }
  28. }
  29. }
  30. func TestCategoryToFilter(t *testing.T) {
  31. phases, types := CategoryToFilter("rate_limit")
  32. if len(types) != 1 || types[0] != "rate_limit_error" || len(phases) != 0 {
  33. t.Fatalf("rate_limit => phases=%v types=%v", phases, types)
  34. }
  35. phases, types = CategoryToFilter("auth")
  36. if len(phases) != 1 || phases[0] != "auth" || len(types) != 0 {
  37. t.Fatalf("auth => phases=%v types=%v", phases, types)
  38. }
  39. phases, types = CategoryToFilter("service_unavailable")
  40. if len(phases) != 1 || phases[0] != "routing" || len(types) != 0 {
  41. t.Fatalf("service_unavailable => phases=%v types=%v", phases, types)
  42. }
  43. phases, types = CategoryToFilter("upstream")
  44. if len(phases) != 3 || phases[0] != "account_auth" || phases[1] != "upstream" || phases[2] != "network" || len(types) != 0 {
  45. t.Fatalf("upstream => phases=%v types=%v", phases, types)
  46. }
  47. phases, types = CategoryToFilter("internal")
  48. if len(phases) != 1 || phases[0] != "internal" || len(types) != 0 {
  49. t.Fatalf("internal => phases=%v types=%v", phases, types)
  50. }
  51. phases, types = CategoryToFilter("quota")
  52. if len(types) != 2 || types[0] != "billing_error" || types[1] != "subscription_error" || len(phases) != 0 {
  53. t.Fatalf("quota => phases=%v types=%v", phases, types)
  54. }
  55. phases, types = CategoryToFilter("invalid_request")
  56. if len(types) != 1 || types[0] != "invalid_request_error" || len(phases) != 0 {
  57. t.Fatalf("invalid_request => phases=%v types=%v", phases, types)
  58. }
  59. phases, types = CategoryToFilter("other")
  60. if len(phases) != 0 || len(types) != 0 {
  61. t.Fatalf("other => phases=%v types=%v", phases, types)
  62. }
  63. }
  64. func TestToUserErrorRequest_RedactsSensitiveFields(t *testing.T) {
  65. src := &OpsErrorLog{
  66. ID: 123,
  67. CreatedAt: time.Unix(0, 0).UTC(),
  68. Model: "m",
  69. RequestedModel: "rm",
  70. InboundEndpoint: "/v1/chat/completions",
  71. StatusCode: 429,
  72. Platform: "openai",
  73. Phase: "request",
  74. Type: "rate_limit_error",
  75. Message: "rate limit exceeded",
  76. APIKeyName: "my-key",
  77. APIKeyDeleted: true,
  78. }
  79. out := ToUserErrorRequest(src)
  80. if out.ID != 123 {
  81. t.Errorf("want ID=123, got %d", out.ID)
  82. }
  83. if out.Model != "rm" {
  84. t.Errorf("want requested_model preferred, got %q", out.Model)
  85. }
  86. if out.Category != "rate_limit" {
  87. t.Errorf("category=%q", out.Category)
  88. }
  89. if out.StatusCode != 429 || out.InboundEndpoint != "/v1/chat/completions" || out.Platform != "openai" {
  90. t.Errorf("basic fields wrong: %+v", out)
  91. }
  92. if out.Message != "rate limit exceeded" {
  93. t.Errorf("want message=%q, got %q", "rate limit exceeded", out.Message)
  94. }
  95. if out.KeyName != "my-key" {
  96. t.Errorf("want key_name=my-key, got %q", out.KeyName)
  97. }
  98. if !out.KeyDeleted {
  99. t.Error("want key_deleted=true")
  100. }
  101. }
  102. func TestToUserErrorRequestDetail_WhitelistAndRedacts(t *testing.T) {
  103. uid := int64(42)
  104. upstreamStatus := 503
  105. src := &OpsErrorLogDetail{
  106. OpsErrorLog: OpsErrorLog{
  107. ID: 999,
  108. CreatedAt: time.Unix(1000, 0).UTC(),
  109. Model: "gpt-4",
  110. RequestedModel: "gpt-4-turbo",
  111. InboundEndpoint: "/v1/chat/completions",
  112. StatusCode: 502,
  113. Platform: "openai",
  114. Phase: "upstream",
  115. Type: "api_error",
  116. Message: "upstream error",
  117. UserID: &uid,
  118. UserEmail: "secret@example.com",
  119. ClientIP: func() *string { s := "1.2.3.4"; return &s }(),
  120. UpstreamEndpoint: "https://api.openai.com/v1/chat/completions",
  121. UserAgent: "codex_cli_rs/0.125.0",
  122. GroupName: "grp-a",
  123. Stream: true,
  124. },
  125. ErrorBody: `{"error":{"message":"upstream failed","type":"server_error"}}`,
  126. UpstreamStatusCode: &upstreamStatus,
  127. }
  128. out := ToUserErrorRequestDetail(src)
  129. if out == nil {
  130. t.Fatal("expected non-nil detail")
  131. }
  132. // 基础字段正确映射
  133. if out.ID != 999 {
  134. t.Errorf("want ID=999, got %d", out.ID)
  135. }
  136. if out.Message != "upstream error" {
  137. t.Errorf("want message=%q, got %q", "upstream error", out.Message)
  138. }
  139. if out.ErrorBody != src.ErrorBody {
  140. t.Errorf("ErrorBody mismatch")
  141. }
  142. if out.UpstreamStatusCode == nil || *out.UpstreamStatusCode != 503 {
  143. t.Errorf("UpstreamStatusCode mismatch")
  144. }
  145. // client_ip / user_agent / group_name / stream 经产品决策开放(与用量明细口径对齐)
  146. if out.ClientIP != "1.2.3.4" {
  147. t.Errorf("want client_ip=1.2.3.4, got %q", out.ClientIP)
  148. }
  149. if out.UserAgent != "codex_cli_rs/0.125.0" {
  150. t.Errorf("want user_agent=codex_cli_rs/0.125.0, got %q", out.UserAgent)
  151. }
  152. if out.GroupName != "grp-a" {
  153. t.Errorf("want group_name=grp-a, got %q", out.GroupName)
  154. }
  155. if !out.Stream {
  156. t.Errorf("want stream=true")
  157. }
  158. // 序列化后不含敏感字段
  159. b, err := json.Marshal(out)
  160. if err != nil {
  161. t.Fatalf("json.Marshal failed: %v", err)
  162. }
  163. raw := string(b)
  164. for _, forbidden := range []string{"user_email", "upstream_endpoint"} {
  165. if strings.Contains(raw, forbidden) {
  166. t.Errorf("sensitive field %q leaked in JSON output: %s", forbidden, raw)
  167. }
  168. }
  169. }
  170. func TestToUserErrorRequestDetail_Nil(t *testing.T) {
  171. if out := ToUserErrorRequestDetail(nil); out != nil {
  172. t.Errorf("expected nil for nil input, got %+v", out)
  173. }
  174. }