usage_log_repo_breakdown_test.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. //go:build unit
  2. package repository
  3. import (
  4. "context"
  5. "regexp"
  6. "testing"
  7. "time"
  8. "github.com/DATA-DOG/go-sqlmock"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestResolveEndpointColumn(t *testing.T) {
  14. tests := []struct {
  15. endpointType string
  16. want string
  17. }{
  18. {"inbound", "ul.inbound_endpoint"},
  19. {"upstream", "ul.upstream_endpoint"},
  20. {"path", "ul.inbound_endpoint || ' -> ' || ul.upstream_endpoint"},
  21. {"", "ul.inbound_endpoint"}, // default
  22. {"unknown", "ul.inbound_endpoint"}, // fallback
  23. }
  24. for _, tc := range tests {
  25. t.Run(tc.endpointType, func(t *testing.T) {
  26. got := resolveEndpointColumn(tc.endpointType)
  27. require.Equal(t, tc.want, got)
  28. })
  29. }
  30. }
  31. func TestResolveModelDimensionExpression(t *testing.T) {
  32. tests := []struct {
  33. modelType string
  34. want string
  35. }{
  36. {usagestats.ModelSourceRequested, "COALESCE(NULLIF(TRIM(requested_model), ''), model)"},
  37. {usagestats.ModelSourceUpstream, "COALESCE(NULLIF(TRIM(upstream_model), ''), model)"},
  38. {usagestats.ModelSourceMapping, "(COALESCE(NULLIF(TRIM(requested_model), ''), model) || ' -> ' || COALESCE(NULLIF(TRIM(upstream_model), ''), model))"},
  39. {"", "COALESCE(NULLIF(TRIM(requested_model), ''), model)"},
  40. {"invalid", "COALESCE(NULLIF(TRIM(requested_model), ''), model)"},
  41. }
  42. for _, tc := range tests {
  43. t.Run(tc.modelType, func(t *testing.T) {
  44. got := resolveModelDimensionExpression(tc.modelType)
  45. require.Equal(t, tc.want, got)
  46. })
  47. }
  48. }
  49. func TestGetUserBreakdownStatsRequestTypeIncludesLegacyFallback(t *testing.T) {
  50. db, mock := newSQLMock(t)
  51. repo := &usageLogRepository{sql: db}
  52. start := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
  53. end := start.Add(24 * time.Hour)
  54. requestType := int16(service.RequestTypeStream)
  55. legacyFilter := `(ul.request_type = $3 OR (ul.request_type = 0 AND ul.stream = TRUE AND ul.openai_ws_mode = FALSE))`
  56. mock.ExpectQuery(regexp.QuoteMeta(legacyFilter)).
  57. WithArgs(start, end, requestType).
  58. WillReturnRows(sqlmock.NewRows([]string{
  59. "user_id", "email", "requests", "input_tokens", "output_tokens",
  60. "cache_tokens", "total_tokens", "cost", "actual_cost", "account_cost",
  61. }))
  62. rows, err := repo.GetUserBreakdownStats(context.Background(), start, end, usagestats.UserBreakdownDimension{
  63. RequestType: &requestType,
  64. }, 0)
  65. require.NoError(t, err)
  66. require.Empty(t, rows)
  67. require.NoError(t, mock.ExpectationsWereMet())
  68. }
  69. func TestGetUserBreakdownStatsFiltersNativeCompactionV2(t *testing.T) {
  70. db, mock := newSQLMock(t)
  71. repo := &usageLogRepository{sql: db}
  72. start := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
  73. end := start.Add(24 * time.Hour)
  74. nativeCompactionV2 := true
  75. mock.ExpectQuery(regexp.QuoteMeta("AND ul.native_compaction_v2 = $3")).
  76. WithArgs(start, end, true).
  77. WillReturnRows(sqlmock.NewRows([]string{
  78. "user_id", "email", "requests", "input_tokens", "output_tokens",
  79. "cache_tokens", "total_tokens", "cost", "actual_cost", "account_cost",
  80. }))
  81. rows, err := repo.GetUserBreakdownStats(context.Background(), start, end, usagestats.UserBreakdownDimension{
  82. NativeCompactionV2: &nativeCompactionV2,
  83. }, 0)
  84. require.NoError(t, err)
  85. require.Empty(t, rows)
  86. require.NoError(t, mock.ExpectationsWereMet())
  87. }