channel_repo_pricing_time_test.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. //go:build unit
  2. package repository
  3. import (
  4. "context"
  5. "regexp"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/DATA-DOG/go-sqlmock"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/stretchr/testify/require"
  12. )
  13. var channelModelPricingTimePricingColumns = []string{
  14. "id", "channel_id", "platform", "models", "billing_mode", "input_price", "output_price",
  15. "cache_write_price", "cache_write_1h_price", "cache_read_price", "fast_multiplier", "flex_multiplier", "max_reasoning_effort_multiplier", "image_input_price", "image_output_price",
  16. "per_request_price", "time_pricing", "created_at", "updated_at",
  17. }
  18. const channelModelPricingTimePricingJSON = `{"timezone":"Asia/Shanghai","periods":[{"start_time":"09:00","end_time":"12:00","multiplier":2}]}`
  19. func newChannelModelPricingTimePricingRepo(t *testing.T) (*channelRepository, sqlmock.Sqlmock) {
  20. t.Helper()
  21. db, mock, err := sqlmock.New()
  22. require.NoError(t, err)
  23. t.Cleanup(func() { _ = db.Close() })
  24. return &channelRepository{db: db}, mock
  25. }
  26. func modelPricingTimePricingRow(timePricing any) *sqlmock.Rows {
  27. return sqlmock.NewRows(channelModelPricingTimePricingColumns).AddRow(
  28. int64(11), int64(7), "openai", `["gpt-5"]`, service.BillingModeToken,
  29. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, timePricing,
  30. time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC), time.Date(2026, 8, 17, 1, 0, 0, 0, time.UTC),
  31. )
  32. }
  33. func expectEmptyModelPricingIntervals(mock sqlmock.Sqlmock) {
  34. mock.ExpectQuery(`SELECT id, pricing_id, min_tokens, max_tokens, tier_label`).
  35. WithArgs(sqlmock.AnyArg()).
  36. WillReturnRows(sqlmock.NewRows([]string{"id"}))
  37. }
  38. func TestChannelModelPricingTimePricingListRoundTrip(t *testing.T) {
  39. repo, mock := newChannelModelPricingTimePricingRepo(t)
  40. mock.ExpectQuery(`(?s)SELECT .*per_request_price, time_pricing, created_at, updated_at.*FROM channel_model_pricing.*channel_id = \$1`).
  41. WithArgs(int64(7)).
  42. WillReturnRows(modelPricingTimePricingRow(channelModelPricingTimePricingJSON))
  43. expectEmptyModelPricingIntervals(mock)
  44. pricing, err := repo.ListModelPricing(context.Background(), 7)
  45. require.NoError(t, err)
  46. require.Len(t, pricing, 1)
  47. require.NotNil(t, pricing[0].TimePricing)
  48. require.Equal(t, "Asia/Shanghai", pricing[0].TimePricing.Timezone)
  49. require.Len(t, pricing[0].TimePricing.Periods, 1)
  50. require.Equal(t, 2.0, pricing[0].TimePricing.Periods[0].Multiplier)
  51. require.NoError(t, mock.ExpectationsWereMet())
  52. }
  53. func TestChannelModelPricingTimePricingListNullAndMalformed(t *testing.T) {
  54. t.Run("SQL NULL maps to nil", func(t *testing.T) {
  55. repo, mock := newChannelModelPricingTimePricingRepo(t)
  56. mock.ExpectQuery(`(?s)SELECT .*per_request_price, time_pricing, created_at, updated_at.*FROM channel_model_pricing.*channel_id = \$1`).
  57. WithArgs(int64(7)).
  58. WillReturnRows(modelPricingTimePricingRow(nil))
  59. expectEmptyModelPricingIntervals(mock)
  60. pricing, err := repo.ListModelPricing(context.Background(), 7)
  61. require.NoError(t, err)
  62. require.Len(t, pricing, 1)
  63. require.Nil(t, pricing[0].TimePricing)
  64. require.NoError(t, mock.ExpectationsWereMet())
  65. })
  66. t.Run("malformed JSON returns repository error", func(t *testing.T) {
  67. repo, mock := newChannelModelPricingTimePricingRepo(t)
  68. mock.ExpectQuery(`(?s)SELECT .*per_request_price, time_pricing, created_at, updated_at.*FROM channel_model_pricing.*channel_id = \$1`).
  69. WithArgs(int64(7)).
  70. WillReturnRows(modelPricingTimePricingRow(`{"timezone":`))
  71. _, err := repo.ListModelPricing(context.Background(), 7)
  72. require.Error(t, err)
  73. require.True(t, strings.Contains(err.Error(), "unmarshal time pricing"), "unexpected error: %v", err)
  74. require.NoError(t, mock.ExpectationsWereMet())
  75. })
  76. }
  77. func TestChannelModelPricingTimePricingCreateAndUpdateRoundTrip(t *testing.T) {
  78. pricing := &service.ChannelModelPricing{
  79. ID: 11,
  80. ChannelID: 7,
  81. Platform: "openai",
  82. Models: []string{"gpt-5"},
  83. TimePricing: &service.ChannelTimePricing{
  84. Timezone: "Asia/Shanghai",
  85. Periods: []service.ChannelTimePricingPeriod{{
  86. StartTime: "09:00", EndTime: "12:00", Multiplier: 2,
  87. }},
  88. },
  89. }
  90. t.Run("create writes JSON", func(t *testing.T) {
  91. repo, mock := newChannelModelPricingTimePricingRepo(t)
  92. mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")).
  93. WithArgs(
  94. int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken,
  95. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON,
  96. ).
  97. WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{}))
  98. require.NoError(t, repo.CreateModelPricing(context.Background(), pricing))
  99. require.NoError(t, mock.ExpectationsWereMet())
  100. })
  101. t.Run("update writes JSON and entry ID", func(t *testing.T) {
  102. repo, mock := newChannelModelPricingTimePricingRepo(t)
  103. mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$13, time_pricing = \$14, platform = \$15.*WHERE id = \$16`).
  104. WithArgs(
  105. []byte(`["gpt-5"]`), service.BillingModeToken,
  106. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON, "openai", int64(11),
  107. ).
  108. WillReturnResult(sqlmock.NewResult(0, 1))
  109. require.NoError(t, repo.UpdateModelPricing(context.Background(), pricing))
  110. require.NoError(t, mock.ExpectationsWereMet())
  111. })
  112. }
  113. func TestChannelModelPricingTimePricingCreateAndUpdateWriteNullWhenDisabled(t *testing.T) {
  114. tests := []struct {
  115. name string
  116. timePricing *service.ChannelTimePricing
  117. }{
  118. {name: "nil", timePricing: nil},
  119. {name: "empty periods", timePricing: &service.ChannelTimePricing{Timezone: "Asia/Shanghai"}},
  120. }
  121. for _, tt := range tests {
  122. t.Run(tt.name, func(t *testing.T) {
  123. newPricing := func() *service.ChannelModelPricing {
  124. return &service.ChannelModelPricing{
  125. ID: 11,
  126. ChannelID: 7,
  127. Platform: "openai",
  128. Models: []string{"gpt-5"},
  129. TimePricing: tt.timePricing,
  130. }
  131. }
  132. t.Run("create writes SQL NULL", func(t *testing.T) {
  133. repo, mock := newChannelModelPricingTimePricingRepo(t)
  134. mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")).
  135. WithArgs(
  136. int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken,
  137. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
  138. ).
  139. WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{}))
  140. require.NoError(t, repo.CreateModelPricing(context.Background(), newPricing()))
  141. require.NoError(t, mock.ExpectationsWereMet())
  142. })
  143. t.Run("update writes SQL NULL", func(t *testing.T) {
  144. repo, mock := newChannelModelPricingTimePricingRepo(t)
  145. mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$13, time_pricing = \$14, platform = \$15.*WHERE id = \$16`).
  146. WithArgs(
  147. []byte(`["gpt-5"]`), service.BillingModeToken,
  148. nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "openai", int64(11),
  149. ).
  150. WillReturnResult(sqlmock.NewResult(0, 1))
  151. require.NoError(t, repo.UpdateModelPricing(context.Background(), newPricing()))
  152. require.NoError(t, mock.ExpectationsWereMet())
  153. })
  154. })
  155. }
  156. }