| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- package admin
- import (
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "net/netip"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/handler/dto"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func TestParseTimeRange(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- req := httptest.NewRequest(http.MethodGet, "/?start_date=2024-01-01&end_date=2024-01-02&timezone=UTC", nil)
- c.Request = req
- start, end := parseTimeRange(c)
- require.Equal(t, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), start)
- require.Equal(t, time.Date(2024, 1, 3, 0, 0, 0, 0, time.UTC), end)
- req = httptest.NewRequest(http.MethodGet, "/?start_date=bad&timezone=UTC", nil)
- c.Request = req
- start, end = parseTimeRange(c)
- require.False(t, start.IsZero())
- require.False(t, end.IsZero())
- }
- func TestParseOpsViewParam(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/?view=excluded", nil)
- require.Equal(t, opsListViewExcluded, parseOpsViewParam(c))
- c2, _ := gin.CreateTestContext(w)
- c2.Request = httptest.NewRequest(http.MethodGet, "/?view=all", nil)
- require.Equal(t, opsListViewAll, parseOpsViewParam(c2))
- c3, _ := gin.CreateTestContext(w)
- c3.Request = httptest.NewRequest(http.MethodGet, "/?view=unknown", nil)
- require.Equal(t, opsListViewErrors, parseOpsViewParam(c3))
- require.Equal(t, "", parseOpsViewParam(nil))
- }
- func TestParseOpsDuration(t *testing.T) {
- dur, ok := parseOpsDuration("1h")
- require.True(t, ok)
- require.Equal(t, time.Hour, dur)
- _, ok = parseOpsDuration("invalid")
- require.False(t, ok)
- }
- func TestParseOpsOpenAITokenStatsDuration(t *testing.T) {
- tests := []struct {
- input string
- want time.Duration
- ok bool
- }{
- {input: "30m", want: 30 * time.Minute, ok: true},
- {input: "1h", want: time.Hour, ok: true},
- {input: "1d", want: 24 * time.Hour, ok: true},
- {input: "15d", want: 15 * 24 * time.Hour, ok: true},
- {input: "30d", want: 30 * 24 * time.Hour, ok: true},
- {input: "7d", want: 0, ok: false},
- }
- for _, tt := range tests {
- got, ok := parseOpsOpenAITokenStatsDuration(tt.input)
- require.Equal(t, tt.ok, ok, "input=%s", tt.input)
- require.Equal(t, tt.want, got, "input=%s", tt.input)
- }
- }
- func TestParseOpsOpenAITokenStatsFilter_Defaults(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
- before := time.Now().UTC()
- filter, err := parseOpsOpenAITokenStatsFilter(c)
- after := time.Now().UTC()
- require.NoError(t, err)
- require.NotNil(t, filter)
- require.Equal(t, "30d", filter.TimeRange)
- require.Equal(t, 1, filter.Page)
- require.Equal(t, 20, filter.PageSize)
- require.Equal(t, 0, filter.TopN)
- require.Nil(t, filter.GroupID)
- require.Equal(t, "", filter.Platform)
- require.True(t, filter.StartTime.Before(filter.EndTime))
- require.WithinDuration(t, before.Add(-30*24*time.Hour), filter.StartTime, 2*time.Second)
- require.WithinDuration(t, after, filter.EndTime, 2*time.Second)
- }
- func TestParseOpsOpenAITokenStatsFilter_WithTopN(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(
- http.MethodGet,
- "/?time_range=1h&platform=openai&group_id=12&top_n=50",
- nil,
- )
- filter, err := parseOpsOpenAITokenStatsFilter(c)
- require.NoError(t, err)
- require.Equal(t, "1h", filter.TimeRange)
- require.Equal(t, "openai", filter.Platform)
- require.NotNil(t, filter.GroupID)
- require.Equal(t, int64(12), *filter.GroupID)
- require.Equal(t, 50, filter.TopN)
- require.Equal(t, 0, filter.Page)
- require.Equal(t, 0, filter.PageSize)
- }
- func TestParseOpsOpenAITokenStatsFilter_InvalidParams(t *testing.T) {
- tests := []string{
- "/?time_range=7d",
- "/?group_id=0",
- "/?group_id=abc",
- "/?top_n=0",
- "/?top_n=101",
- "/?top_n=10&page=1",
- "/?top_n=10&page_size=20",
- "/?page=0",
- "/?page_size=0",
- "/?page_size=101",
- }
- gin.SetMode(gin.TestMode)
- for _, rawURL := range tests {
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, rawURL, nil)
- _, err := parseOpsOpenAITokenStatsFilter(c)
- require.Error(t, err, "url=%s", rawURL)
- }
- }
- func TestParseOpsTimeRange(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- now := time.Now().UTC()
- startStr := now.Add(-time.Hour).Format(time.RFC3339)
- endStr := now.Format(time.RFC3339)
- c.Request = httptest.NewRequest(http.MethodGet, "/?start_time="+startStr+"&end_time="+endStr, nil)
- start, end, err := parseOpsTimeRange(c, "1h")
- require.NoError(t, err)
- require.True(t, start.Before(end))
- c2, _ := gin.CreateTestContext(w)
- c2.Request = httptest.NewRequest(http.MethodGet, "/?start_time=bad", nil)
- _, _, err = parseOpsTimeRange(c2, "1h")
- require.Error(t, err)
- }
- func TestParseOpsRealtimeWindow(t *testing.T) {
- dur, label, ok := parseOpsRealtimeWindow("5m")
- require.True(t, ok)
- require.Equal(t, 5*time.Minute, dur)
- require.Equal(t, "5min", label)
- _, _, ok = parseOpsRealtimeWindow("invalid")
- require.False(t, ok)
- }
- func TestPickThroughputBucketSeconds(t *testing.T) {
- require.Equal(t, 60, pickThroughputBucketSeconds(30*time.Minute))
- require.Equal(t, 300, pickThroughputBucketSeconds(6*time.Hour))
- require.Equal(t, 3600, pickThroughputBucketSeconds(48*time.Hour))
- }
- func TestParseOpsQueryMode(t *testing.T) {
- gin.SetMode(gin.TestMode)
- w := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(w)
- c.Request = httptest.NewRequest(http.MethodGet, "/?mode=raw", nil)
- require.Equal(t, service.ParseOpsQueryMode("raw"), parseOpsQueryMode(c))
- require.Equal(t, service.OpsQueryMode(""), parseOpsQueryMode(nil))
- }
- func TestOpsAlertRuleValidation(t *testing.T) {
- raw := map[string]json.RawMessage{
- "name": json.RawMessage(`"High error rate"`),
- "metric_type": json.RawMessage(`"error_rate"`),
- "operator": json.RawMessage(`">"`),
- "threshold": json.RawMessage(`90`),
- }
- validated, err := validateOpsAlertRulePayload(raw)
- require.NoError(t, err)
- require.Equal(t, "High error rate", validated.Name)
- _, err = validateOpsAlertRulePayload(map[string]json.RawMessage{})
- require.Error(t, err)
- require.True(t, isPercentOrRateMetric("error_rate"))
- require.False(t, isPercentOrRateMetric("concurrency_queue_depth"))
- }
- func TestOpsWSHelpers(t *testing.T) {
- prefixes, invalid := parseTrustedProxyList("10.0.0.0/8,invalid")
- require.Len(t, prefixes, 1)
- require.Len(t, invalid, 1)
- host := hostWithoutPort("example.com:443")
- require.Equal(t, "example.com", host)
- addr := netip.MustParseAddr("10.0.0.1")
- require.True(t, isAddrInTrustedProxies(addr, prefixes))
- require.False(t, isAddrInTrustedProxies(netip.MustParseAddr("192.168.0.1"), prefixes))
- }
- // TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier 验证 admin
- // 写入路径会把 ServiceTier 的空字符串/空白/大小写归一化为
- // service.OpenAIFastTierAny ("all"),避免落盘时 "" 与 "all" 双语义。
- func TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier(t *testing.T) {
- t.Run("nil input returns nil", func(t *testing.T) {
- require.Nil(t, openaiFastPolicySettingsFromDTO(nil))
- })
- t.Run("empty service_tier becomes 'all'", func(t *testing.T) {
- in := &dto.OpenAIFastPolicySettings{
- Rules: []dto.OpenAIFastPolicyRule{{
- ServiceTier: "",
- Action: "filter",
- Scope: "all",
- }},
- }
- out := openaiFastPolicySettingsFromDTO(in)
- require.NotNil(t, out)
- require.Len(t, out.Rules, 1)
- require.Equal(t, service.OpenAIFastTierAny, out.Rules[0].ServiceTier)
- require.Equal(t, "all", out.Rules[0].ServiceTier)
- })
- t.Run("whitespace-only service_tier becomes 'all'", func(t *testing.T) {
- in := &dto.OpenAIFastPolicySettings{
- Rules: []dto.OpenAIFastPolicyRule{{
- ServiceTier: " ",
- Action: "pass",
- Scope: "all",
- }},
- }
- out := openaiFastPolicySettingsFromDTO(in)
- require.Equal(t, service.OpenAIFastTierAny, out.Rules[0].ServiceTier)
- })
- t.Run("uppercase service_tier is lowercased", func(t *testing.T) {
- in := &dto.OpenAIFastPolicySettings{
- Rules: []dto.OpenAIFastPolicyRule{{
- ServiceTier: "PRIORITY",
- Action: "filter",
- Scope: "all",
- UserIDs: []int64{42},
- }},
- }
- out := openaiFastPolicySettingsFromDTO(in)
- require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
- require.Equal(t, []int64{42}, out.Rules[0].UserIDs)
- })
- t.Run("non-empty values pass through (lowercased)", func(t *testing.T) {
- in := &dto.OpenAIFastPolicySettings{
- Rules: []dto.OpenAIFastPolicyRule{
- {ServiceTier: "priority", Action: "filter", Scope: "all"},
- {ServiceTier: "flex", Action: "block", Scope: "oauth"},
- {ServiceTier: "ultrafast", Action: "pass", Scope: "all"},
- {ServiceTier: "all", Action: "pass", Scope: "apikey"},
- },
- }
- out := openaiFastPolicySettingsFromDTO(in)
- require.Len(t, out.Rules, 4)
- require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier)
- require.Equal(t, service.OpenAIFastTierFlex, out.Rules[1].ServiceTier)
- require.Equal(t, service.OpenAIFastTierUltrafast, out.Rules[2].ServiceTier)
- require.Equal(t, service.OpenAIFastTierAny, out.Rules[3].ServiceTier)
- })
- }
|