gateway_channel_restriction_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // --- billingModelForRestriction ---
  9. func TestBillingModelForRestriction_Requested(t *testing.T) {
  10. t.Parallel()
  11. got := billingModelForRestriction(BillingModelSourceRequested, "claude-sonnet-4-5", "claude-sonnet-4-6")
  12. require.Equal(t, "claude-sonnet-4-5", got)
  13. }
  14. func TestBillingModelForRestriction_ChannelMapped(t *testing.T) {
  15. t.Parallel()
  16. got := billingModelForRestriction(BillingModelSourceChannelMapped, "claude-sonnet-4-5", "claude-sonnet-4-6")
  17. require.Equal(t, "claude-sonnet-4-6", got)
  18. }
  19. func TestBillingModelForRestriction_Upstream(t *testing.T) {
  20. t.Parallel()
  21. got := billingModelForRestriction(BillingModelSourceUpstream, "claude-sonnet-4-5", "claude-sonnet-4-6")
  22. require.Equal(t, "", got, "upstream should return empty (per-account check needed)")
  23. }
  24. func TestBillingModelForRestriction_ResponseModelUsesMappedPrecheck(t *testing.T) {
  25. t.Parallel()
  26. got := billingModelForRestriction(BillingModelSourceResponse, "claude-fable-5", "claude-fable-5")
  27. require.Equal(t, "claude-fable-5", got)
  28. }
  29. func TestBillingModelForRestriction_Empty(t *testing.T) {
  30. t.Parallel()
  31. got := billingModelForRestriction("", "claude-sonnet-4-5", "claude-sonnet-4-6")
  32. require.Equal(t, "claude-sonnet-4-6", got, "empty source defaults to channel_mapped")
  33. }
  34. // --- resolveAccountUpstreamModel ---
  35. func TestResolveAccountUpstreamModel_Antigravity(t *testing.T) {
  36. t.Parallel()
  37. account := &Account{
  38. Platform: PlatformAntigravity,
  39. }
  40. // Antigravity 平台使用 DefaultAntigravityModelMapping
  41. got := resolveAccountUpstreamModel(account, "claude-sonnet-4-6")
  42. require.Equal(t, "claude-sonnet-4-6", got)
  43. }
  44. func TestResolveAccountUpstreamModel_Antigravity_Unsupported(t *testing.T) {
  45. t.Parallel()
  46. account := &Account{
  47. Platform: PlatformAntigravity,
  48. }
  49. got := resolveAccountUpstreamModel(account, "totally-unknown-model")
  50. require.Equal(t, "", got, "unsupported model should return empty")
  51. }
  52. func TestResolveAccountUpstreamModel_NonAntigravity(t *testing.T) {
  53. t.Parallel()
  54. account := &Account{
  55. Platform: PlatformAnthropic,
  56. }
  57. got := resolveAccountUpstreamModel(account, "claude-sonnet-4-6")
  58. require.Equal(t, "claude-sonnet-4-6", got, "no mapping = passthrough")
  59. }
  60. // --- checkChannelPricingRestriction ---
  61. func TestCheckChannelPricingRestriction_NilGroupID(t *testing.T) {
  62. t.Parallel()
  63. svc := &GatewayService{channelService: &ChannelService{}}
  64. require.False(t, svc.checkChannelPricingRestriction(context.Background(), nil, "claude-sonnet-4"))
  65. }
  66. func TestCheckChannelPricingRestriction_NilChannelService(t *testing.T) {
  67. t.Parallel()
  68. svc := &GatewayService{}
  69. gid := int64(10)
  70. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "claude-sonnet-4"))
  71. }
  72. func TestCheckChannelPricingRestriction_EmptyModel(t *testing.T) {
  73. t.Parallel()
  74. svc := &GatewayService{channelService: &ChannelService{}}
  75. gid := int64(10)
  76. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, ""))
  77. }
  78. func TestCheckChannelPricingRestriction_ChannelMapped_Restricted(t *testing.T) {
  79. t.Parallel()
  80. // 渠道映射 claude-sonnet-4-5 → claude-sonnet-4-6,但定价列表只有 claude-opus-4-6
  81. ch := Channel{
  82. ID: 1,
  83. Status: StatusActive,
  84. GroupIDs: []int64{10},
  85. RestrictModels: true,
  86. BillingModelSource: BillingModelSourceChannelMapped,
  87. ModelPricing: []ChannelModelPricing{
  88. {Platform: "anthropic", Models: []string{"claude-opus-4-6"}},
  89. },
  90. ModelMapping: map[string]map[string]string{
  91. "anthropic": {"claude-sonnet-4-5": "claude-sonnet-4-6"},
  92. },
  93. }
  94. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  95. svc := &GatewayService{channelService: channelSvc}
  96. gid := int64(10)
  97. require.True(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "claude-sonnet-4-5"),
  98. "mapped model claude-sonnet-4-6 is NOT in pricing → restricted")
  99. }
  100. func TestCheckChannelPricingRestriction_ChannelMapped_Allowed(t *testing.T) {
  101. t.Parallel()
  102. // 渠道映射 claude-sonnet-4-5 → claude-sonnet-4-6,定价列表包含 claude-sonnet-4-6
  103. ch := Channel{
  104. ID: 1,
  105. Status: StatusActive,
  106. GroupIDs: []int64{10},
  107. RestrictModels: true,
  108. BillingModelSource: BillingModelSourceChannelMapped,
  109. ModelPricing: []ChannelModelPricing{
  110. {Platform: "anthropic", Models: []string{"claude-sonnet-4-6"}},
  111. },
  112. ModelMapping: map[string]map[string]string{
  113. "anthropic": {"claude-sonnet-4-5": "claude-sonnet-4-6"},
  114. },
  115. }
  116. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  117. svc := &GatewayService{channelService: channelSvc}
  118. gid := int64(10)
  119. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "claude-sonnet-4-5"),
  120. "mapped model claude-sonnet-4-6 IS in pricing → allowed")
  121. }
  122. func TestCheckChannelPricingRestriction_Requested_Restricted(t *testing.T) {
  123. t.Parallel()
  124. // billing_model_source=requested,定价列表有 claude-sonnet-4-6 但请求的是 claude-sonnet-4-5
  125. ch := Channel{
  126. ID: 1,
  127. Status: StatusActive,
  128. GroupIDs: []int64{10},
  129. RestrictModels: true,
  130. BillingModelSource: BillingModelSourceRequested,
  131. ModelPricing: []ChannelModelPricing{
  132. {Platform: "anthropic", Models: []string{"claude-sonnet-4-6"}},
  133. },
  134. }
  135. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  136. svc := &GatewayService{channelService: channelSvc}
  137. gid := int64(10)
  138. require.True(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "claude-sonnet-4-5"),
  139. "requested model claude-sonnet-4-5 is NOT in pricing → restricted")
  140. }
  141. func TestCheckChannelPricingRestriction_Requested_Allowed(t *testing.T) {
  142. t.Parallel()
  143. ch := Channel{
  144. ID: 1,
  145. Status: StatusActive,
  146. GroupIDs: []int64{10},
  147. RestrictModels: true,
  148. BillingModelSource: BillingModelSourceRequested,
  149. ModelPricing: []ChannelModelPricing{
  150. {Platform: "anthropic", Models: []string{"claude-sonnet-4-5"}},
  151. },
  152. }
  153. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  154. svc := &GatewayService{channelService: channelSvc}
  155. gid := int64(10)
  156. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "claude-sonnet-4-5"),
  157. "requested model IS in pricing → allowed")
  158. }
  159. func TestCheckChannelPricingRestriction_Upstream_SkipsPreCheck(t *testing.T) {
  160. t.Parallel()
  161. // upstream 模式:预检查始终跳过(返回 false),需逐账号检查
  162. ch := Channel{
  163. ID: 1,
  164. Status: StatusActive,
  165. GroupIDs: []int64{10},
  166. RestrictModels: true,
  167. BillingModelSource: BillingModelSourceUpstream,
  168. ModelPricing: []ChannelModelPricing{
  169. {Platform: "anthropic", Models: []string{"claude-opus-4-6"}},
  170. },
  171. }
  172. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  173. svc := &GatewayService{channelService: channelSvc}
  174. gid := int64(10)
  175. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "unknown-model"),
  176. "upstream mode should skip pre-check (per-account check needed)")
  177. }
  178. func TestCheckChannelPricingRestriction_RestrictModelsDisabled(t *testing.T) {
  179. t.Parallel()
  180. ch := Channel{
  181. ID: 1,
  182. Status: StatusActive,
  183. GroupIDs: []int64{10},
  184. RestrictModels: false, // 未开启模型限制
  185. ModelPricing: []ChannelModelPricing{
  186. {Platform: "anthropic", Models: []string{"claude-opus-4-6"}},
  187. },
  188. }
  189. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  190. svc := &GatewayService{channelService: channelSvc}
  191. gid := int64(10)
  192. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "any-model"),
  193. "RestrictModels=false → always allowed")
  194. }
  195. func TestCheckChannelPricingRestriction_NoChannel(t *testing.T) {
  196. t.Parallel()
  197. // 分组没有关联渠道
  198. repo := &mockChannelRepository{
  199. listAllFn: func(_ context.Context) ([]Channel, error) { return nil, nil },
  200. }
  201. channelSvc := newTestChannelService(repo)
  202. svc := &GatewayService{channelService: channelSvc}
  203. gid := int64(999)
  204. require.False(t, svc.checkChannelPricingRestriction(context.Background(), &gid, "any-model"),
  205. "no channel for group → allowed")
  206. }
  207. // --- isUpstreamModelRestrictedByChannel ---
  208. func TestIsUpstreamModelRestrictedByChannel_Restricted(t *testing.T) {
  209. t.Parallel()
  210. ch := Channel{
  211. ID: 1,
  212. Status: StatusActive,
  213. GroupIDs: []int64{10},
  214. RestrictModels: true,
  215. ModelPricing: []ChannelModelPricing{
  216. {Platform: "anthropic", Models: []string{"claude-opus-4-6"}},
  217. },
  218. }
  219. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  220. svc := &GatewayService{channelService: channelSvc}
  221. account := &Account{Platform: PlatformAntigravity}
  222. // claude-sonnet-4-6 在 DefaultAntigravityModelMapping 中,映射后仍为 claude-sonnet-4-6
  223. // 但定价列表只有 claude-opus-4-6
  224. require.True(t, svc.isUpstreamModelRestrictedByChannel(context.Background(), 10, account, "claude-sonnet-4-6"),
  225. "upstream model claude-sonnet-4-6 NOT in pricing → restricted")
  226. }
  227. func TestIsUpstreamModelRestrictedByChannel_Allowed(t *testing.T) {
  228. t.Parallel()
  229. ch := Channel{
  230. ID: 1,
  231. Status: StatusActive,
  232. GroupIDs: []int64{10},
  233. RestrictModels: true,
  234. ModelPricing: []ChannelModelPricing{
  235. {Platform: "anthropic", Models: []string{"claude-sonnet-4-6"}},
  236. },
  237. }
  238. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  239. svc := &GatewayService{channelService: channelSvc}
  240. account := &Account{Platform: PlatformAntigravity}
  241. require.False(t, svc.isUpstreamModelRestrictedByChannel(context.Background(), 10, account, "claude-sonnet-4-6"),
  242. "upstream model claude-sonnet-4-6 IS in pricing → allowed")
  243. }
  244. func TestIsUpstreamModelRestrictedByChannel_UnsupportedModel(t *testing.T) {
  245. t.Parallel()
  246. ch := Channel{
  247. ID: 1,
  248. Status: StatusActive,
  249. GroupIDs: []int64{10},
  250. RestrictModels: true,
  251. ModelPricing: []ChannelModelPricing{
  252. {Platform: "anthropic", Models: []string{"claude-opus-4-6"}},
  253. },
  254. }
  255. channelSvc := newTestChannelService(makeStandardRepo(ch, map[int64]string{10: "anthropic"}))
  256. svc := &GatewayService{channelService: channelSvc}
  257. account := &Account{Platform: PlatformAntigravity}
  258. // totally-unknown-model 不在 DefaultAntigravityModelMapping 中 → 映射结果为空
  259. require.False(t, svc.isUpstreamModelRestrictedByChannel(context.Background(), 10, account, "totally-unknown-model"),
  260. "unmappable model → upstream model empty → not restricted (account filter handles this)")
  261. }