admin_helpers_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package admin
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "net/netip"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func TestParseTimeRange(t *testing.T) {
  15. gin.SetMode(gin.TestMode)
  16. w := httptest.NewRecorder()
  17. c, _ := gin.CreateTestContext(w)
  18. req := httptest.NewRequest(http.MethodGet, "/?start_date=2024-01-01&end_date=2024-01-02&timezone=UTC", nil)
  19. c.Request = req
  20. start, end := parseTimeRange(c)
  21. require.Equal(t, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), start)
  22. require.Equal(t, time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC), end)
  23. req = httptest.NewRequest(http.MethodGet, "/?start_date=bad&timezone=UTC", nil)
  24. c.Request = req
  25. start, end = parseTimeRange(c)
  26. require.False(t, start.IsZero())
  27. require.False(t, end.IsZero())
  28. }
  29. func TestParseOpsViewParam(t *testing.T) {
  30. gin.SetMode(gin.TestMode)
  31. w := httptest.NewRecorder()
  32. c, _ := gin.CreateTestContext(w)
  33. c.Request = httptest.NewRequest(http.MethodGet, "/?view=excluded", nil)
  34. require.Equal(t, opsListViewExcluded, parseOpsViewParam(c))
  35. c2, _ := gin.CreateTestContext(w)
  36. c2.Request = httptest.NewRequest(http.MethodGet, "/?view=all", nil)
  37. require.Equal(t, opsListViewAll, parseOpsViewParam(c2))
  38. c3, _ := gin.CreateTestContext(w)
  39. c3.Request = httptest.NewRequest(http.MethodGet, "/?view=unknown", nil)
  40. require.Equal(t, opsListViewErrors, parseOpsViewParam(c3))
  41. require.Equal(t, "", parseOpsViewParam(nil))
  42. }
  43. func TestParseOpsDuration(t *testing.T) {
  44. dur, ok := parseOpsDuration("1h")
  45. require.True(t, ok)
  46. require.Equal(t, time.Hour, dur)
  47. _, ok = parseOpsDuration("invalid")
  48. require.False(t, ok)
  49. }
  50. func TestParseOpsOpenAITokenStatsDuration(t *testing.T) {
  51. tests := []struct {
  52. input string
  53. want time.Duration
  54. ok bool
  55. }{
  56. {input: "30m", want: 30 * time.Minute, ok: true},
  57. {input: "1h", want: time.Hour, ok: true},
  58. {input: "1d", want: 24 * time.Hour, ok: true},
  59. {input: "15d", want: 15 * 24 * time.Hour, ok: true},
  60. {input: "30d", want: 30 * 24 * time.Hour, ok: true},
  61. {input: "7d", want: 0, ok: false},
  62. }
  63. for _, tt := range tests {
  64. got, ok := parseOpsOpenAITokenStatsDuration(tt.input)
  65. require.Equal(t, tt.ok, ok, "input=%s", tt.input)
  66. require.Equal(t, tt.want, got, "input=%s", tt.input)
  67. }
  68. }
  69. func TestParseOpsOpenAITokenStatsFilter_Defaults(t *testing.T) {
  70. gin.SetMode(gin.TestMode)
  71. w := httptest.NewRecorder()
  72. c, _ := gin.CreateTestContext(w)
  73. c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
  74. before := time.Now().UTC()
  75. filter, err := parseOpsOpenAITokenStatsFilter(c)
  76. after := time.Now().UTC()
  77. require.NoError(t, err)
  78. require.NotNil(t, filter)
  79. require.Equal(t, "30d", filter.TimeRange)
  80. require.Equal(t, 1, filter.Page)
  81. require.Equal(t, 20, filter.PageSize)
  82. require.Equal(t, 0, filter.TopN)
  83. require.Nil(t, filter.GroupID)
  84. require.Equal(t, "", filter.Platform)
  85. require.True(t, filter.StartTime.Before(filter.EndTime))
  86. require.WithinDuration(t, before.Add(-30*24*time.Hour), filter.StartTime, 2*time.Second)
  87. require.WithinDuration(t, after, filter.EndTime, 2*time.Second)
  88. }
  89. func TestParseOpsOpenAITokenStatsFilter_WithTopN(t *testing.T) {
  90. gin.SetMode(gin.TestMode)
  91. w := httptest.NewRecorder()
  92. c, _ := gin.CreateTestContext(w)
  93. c.Request = httptest.NewRequest(
  94. http.MethodGet,
  95. "/?time_range=1h&platform=openai&group_id=12&top_n=50",
  96. nil,
  97. )
  98. filter, err := parseOpsOpenAITokenStatsFilter(c)
  99. require.NoError(t, err)
  100. require.Equal(t, "1h", filter.TimeRange)
  101. require.Equal(t, "openai", filter.Platform)
  102. require.NotNil(t, filter.GroupID)
  103. require.Equal(t, int64(12), *filter.GroupID)
  104. require.Equal(t, 50, filter.TopN)
  105. require.Equal(t, 0, filter.Page)
  106. require.Equal(t, 0, filter.PageSize)
  107. }
  108. func TestParseOpsOpenAITokenStatsFilter_InvalidParams(t *testing.T) {
  109. tests := []string{
  110. "/?time_range=7d",
  111. "/?group_id=0",
  112. "/?group_id=abc",
  113. "/?top_n=0",
  114. "/?top_n=101",
  115. "/?top_n=10&page=1",
  116. "/?top_n=10&page_size=20",
  117. "/?page=0",
  118. "/?page_size=0",
  119. "/?page_size=101",
  120. }
  121. gin.SetMode(gin.TestMode)
  122. for _, rawURL := range tests {
  123. w := httptest.NewRecorder()
  124. c, _ := gin.CreateTestContext(w)
  125. c.Request = httptest.NewRequest(http.MethodGet, rawURL, nil)
  126. _, err := parseOpsOpenAITokenStatsFilter(c)
  127. require.Error(t, err, "url=%s", rawURL)
  128. }
  129. }
  130. func TestParseOpsTimeRange(t *testing.T) {
  131. gin.SetMode(gin.TestMode)
  132. w := httptest.NewRecorder()
  133. c, _ := gin.CreateTestContext(w)
  134. now := time.Now().UTC()
  135. startStr := now.Add(-time.Hour).Format(time.RFC3339)
  136. endStr := now.Format(time.RFC3339)
  137. c.Request = httptest.NewRequest(http.MethodGet, "/?start_time="+startStr+"&end_time="+endStr, nil)
  138. start, end, err := parseOpsTimeRange(c, "1h")
  139. require.NoError(t, err)
  140. require.True(t, start.Before(end))
  141. c2, _ := gin.CreateTestContext(w)
  142. c2.Request = httptest.NewRequest(http.MethodGet, "/?start_time=bad", nil)
  143. _, _, err = parseOpsTimeRange(c2, "1h")
  144. require.Error(t, err)
  145. }
  146. func TestParseOpsRealtimeWindow(t *testing.T) {
  147. dur, label, ok := parseOpsRealtimeWindow("5m")
  148. require.True(t, ok)
  149. require.Equal(t, 5*time.Minute, dur)
  150. require.Equal(t, "5min", label)
  151. _, _, ok = parseOpsRealtimeWindow("invalid")
  152. require.False(t, ok)
  153. }
  154. func TestPickThroughputBucketSeconds(t *testing.T) {
  155. require.Equal(t, 60, pickThroughputBucketSeconds(30*time.Minute))
  156. require.Equal(t, 300, pickThroughputBucketSeconds(6*time.Hour))
  157. require.Equal(t, 3600, pickThroughputBucketSeconds(48*time.Hour))
  158. }
  159. func TestParseOpsQueryMode(t *testing.T) {
  160. gin.SetMode(gin.TestMode)
  161. w := httptest.NewRecorder()
  162. c, _ := gin.CreateTestContext(w)
  163. c.Request = httptest.NewRequest(http.MethodGet, "/?mode=raw", nil)
  164. require.Equal(t, service.ParseOpsQueryMode("raw"), parseOpsQueryMode(c))
  165. require.Equal(t, service.OpsQueryMode(""), parseOpsQueryMode(nil))
  166. }
  167. func TestOpsAlertRuleValidation(t *testing.T) {
  168. raw := map[string]json.RawMessage{
  169. "name": json.RawMessage(`"High error rate"`),
  170. "metric_type": json.RawMessage(`"error_rate"`),
  171. "operator": json.RawMessage(`">"`),
  172. "threshold": json.RawMessage(`90`),
  173. }
  174. validated, err := validateOpsAlertRulePayload(raw)
  175. require.NoError(t, err)
  176. require.Equal(t, "High error rate", validated.Name)
  177. _, err = validateOpsAlertRulePayload(map[string]json.RawMessage{})
  178. require.Error(t, err)
  179. require.True(t, isPercentOrRateMetric("error_rate"))
  180. require.False(t, isPercentOrRateMetric("concurrency_queue_depth"))
  181. }
  182. func TestOpsWSHelpers(t *testing.T) {
  183. prefixes, invalid := parseTrustedProxyList("10.0.0.0/8,invalid")
  184. require.Len(t, prefixes, 1)
  185. require.Len(t, invalid, 1)
  186. host := hostWithoutPort("example.com:443")
  187. require.Equal(t, "example.com", host)
  188. addr := netip.MustParseAddr("10.0.0.1")
  189. require.True(t, isAddrInTrustedProxies(addr, prefixes))
  190. require.False(t, isAddrInTrustedProxies(netip.MustParseAddr("192.168.0.1"), prefixes))
  191. }
  192. // TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier 验证 admin
  193. // 写入路径会把 ServiceTier 的空字符串/空白/大小写归一化为
  194. // service.OpenAIFastTierAny ("all"),避免落盘时 "" 与 "all" 双语义。
  195. func TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier(t *testing.T) {
  196. t.Run("nil input returns nil", func(t *testing.T) {
  197. require.Nil(t, openaiFastPolicySettingsFromDTO(nil))
  198. })
  199. t.Run("empty service_tier becomes 'all'", func(t *testing.T) {
  200. in := &dto.OpenAIFastPolicySettings{
  201. Rules: []dto.OpenAIFastPolicyRule{{
  202. ServiceTier: "",
  203. Action: "filter",
  204. Scope: "all",
  205. }},
  206. }
  207. out := openaiFastPolicySettingsFromDTO(in)
  208. require.NotNil(t, out)
  209. require.Len(t, out.Rules, 1)
  210. require.Equal(t, service.OpenAIFastTierAny, out.Rules[0].ServiceTier)
  211. require.Equal(t, "all", out.Rules[0].ServiceTier)
  212. })
  213. t.Run("whitespace-only service_tier becomes 'all'", func(t *testing.T) {
  214. in := &dto.OpenAIFastPolicySettings{
  215. Rules: []dto.OpenAIFastPolicyRule{{
  216. ServiceTier: " ",
  217. Action: "pass",
  218. Scope: "all",
  219. }},
  220. }
  221. out := openaiFastPolicySettingsFromDTO(in)
  222. require.Equal(t, service.OpenAIFastTierAny, out.Rules[0].ServiceTier)
  223. })
  224. t.Run("uppercase service_tier is lowercased", func(t *testing.T) {
  225. in := &dto.OpenAIFastPolicySettings{
  226. Rules: []dto.OpenAIFastPolicyRule{{
  227. ServiceTier: "PRIORITY",
  228. Action: "filter",
  229. Scope: "all",
  230. UserIDs: []int64{42},
  231. }},
  232. }
  233. out := openaiFastPolicySettingsFromDTO(in)
  234. require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
  235. require.Equal(t, []int64{42}, out.Rules[0].UserIDs)
  236. })
  237. t.Run("non-empty values pass through (lowercased)", func(t *testing.T) {
  238. in := &dto.OpenAIFastPolicySettings{
  239. Rules: []dto.OpenAIFastPolicyRule{
  240. {ServiceTier: "priority", Action: "filter", Scope: "all"},
  241. {ServiceTier: "flex", Action: "block", Scope: "oauth"},
  242. {ServiceTier: "ultrafast", Action: "pass", Scope: "all"},
  243. {ServiceTier: "all", Action: "pass", Scope: "apikey"},
  244. },
  245. }
  246. out := openaiFastPolicySettingsFromDTO(in)
  247. require.Len(t, out.Rules, 4)
  248. require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
  249. require.Equal(t, service.OpenAIFastTierFlex, out.Rules[1].ServiceTier)
  250. require.Equal(t, service.OpenAIFastTierUltrafast, out.Rules[2].ServiceTier)
  251. require.Equal(t, service.OpenAIFastTierAny, out.Rules[3].ServiceTier)
  252. })
  253. }