crs_sync_helpers_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package service
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestBuildSelectedSet(t *testing.T) {
  7. tests := []struct {
  8. name string
  9. ids []string
  10. wantNil bool
  11. wantSize int
  12. }{
  13. {
  14. name: "nil input returns nil (backward compatible: create all)",
  15. ids: nil,
  16. wantNil: true,
  17. },
  18. {
  19. name: "empty slice returns empty map (create none)",
  20. ids: []string{},
  21. wantNil: false,
  22. wantSize: 0,
  23. },
  24. {
  25. name: "single ID",
  26. ids: []string{"abc-123"},
  27. wantNil: false,
  28. wantSize: 1,
  29. },
  30. {
  31. name: "multiple IDs",
  32. ids: []string{"a", "b", "c"},
  33. wantNil: false,
  34. wantSize: 3,
  35. },
  36. {
  37. name: "duplicate IDs are deduplicated",
  38. ids: []string{"a", "a", "b"},
  39. wantNil: false,
  40. wantSize: 2,
  41. },
  42. }
  43. for _, tt := range tests {
  44. t.Run(tt.name, func(t *testing.T) {
  45. got := buildSelectedSet(tt.ids)
  46. if tt.wantNil {
  47. if got != nil {
  48. t.Errorf("buildSelectedSet(%v) = %v, want nil", tt.ids, got)
  49. }
  50. return
  51. }
  52. if got == nil {
  53. t.Fatalf("buildSelectedSet(%v) = nil, want non-nil map", tt.ids)
  54. }
  55. if len(got) != tt.wantSize {
  56. t.Errorf("buildSelectedSet(%v) has %d entries, want %d", tt.ids, len(got), tt.wantSize)
  57. }
  58. // Verify all unique IDs are present
  59. for _, id := range tt.ids {
  60. if _, ok := got[id]; !ok {
  61. t.Errorf("buildSelectedSet(%v) missing key %q", tt.ids, id)
  62. }
  63. }
  64. })
  65. }
  66. }
  67. func TestShouldCreateAccount(t *testing.T) {
  68. tests := []struct {
  69. name string
  70. crsID string
  71. selectedSet map[string]struct{}
  72. want bool
  73. }{
  74. {
  75. name: "nil set allows all (backward compatible)",
  76. crsID: "any-id",
  77. selectedSet: nil,
  78. want: true,
  79. },
  80. {
  81. name: "empty set blocks all",
  82. crsID: "any-id",
  83. selectedSet: map[string]struct{}{},
  84. want: false,
  85. },
  86. {
  87. name: "ID in set is allowed",
  88. crsID: "abc-123",
  89. selectedSet: map[string]struct{}{"abc-123": {}, "def-456": {}},
  90. want: true,
  91. },
  92. {
  93. name: "ID not in set is blocked",
  94. crsID: "xyz-789",
  95. selectedSet: map[string]struct{}{"abc-123": {}, "def-456": {}},
  96. want: false,
  97. },
  98. }
  99. for _, tt := range tests {
  100. t.Run(tt.name, func(t *testing.T) {
  101. got := shouldCreateAccount(tt.crsID, tt.selectedSet)
  102. if got != tt.want {
  103. t.Errorf("shouldCreateAccount(%q, %v) = %v, want %v",
  104. tt.crsID, tt.selectedSet, got, tt.want)
  105. }
  106. })
  107. }
  108. }
  109. func TestReconcileCRSUpstreamBillingProbeExtra(t *testing.T) {
  110. remote := map[string]any{
  111. "crs_account_id": "remote-1",
  112. UpstreamBillingProbeEnabledExtraKey: true,
  113. UpstreamBillingRateSyncEnabledExtraKey: true,
  114. UpstreamBillingProbeExtraKey: map[string]any{"status": "remote"},
  115. }
  116. t.Run("create drops remote managed fields", func(t *testing.T) {
  117. extra := mergeMap(nil, remote)
  118. reconcileCRSUpstreamBillingProbeExtra(nil, PlatformOpenAI, AccountTypeAPIKey, map[string]any{"api_key": "new"}, extra)
  119. require.NotContains(t, extra, UpstreamBillingProbeEnabledExtraKey)
  120. require.NotContains(t, extra, UpstreamBillingRateSyncEnabledExtraKey)
  121. require.NotContains(t, extra, UpstreamBillingProbeExtraKey)
  122. })
  123. existing := &Account{
  124. Platform: PlatformOpenAI,
  125. Type: AccountTypeAPIKey,
  126. Credentials: map[string]any{"api_key": "local", "base_url": "http://127.0.0.1:8080"},
  127. Extra: map[string]any{
  128. UpstreamBillingProbeEnabledExtraKey: false,
  129. UpstreamBillingRateSyncEnabledExtraKey: false,
  130. UpstreamBillingProbeExtraKey: map[string]any{"status": "local"},
  131. },
  132. }
  133. t.Run("same identity keeps local state", func(t *testing.T) {
  134. extra := mergeMap(existing.Extra, remote)
  135. reconcileCRSUpstreamBillingProbeExtra(existing, existing.Platform, existing.Type, mergeMap(existing.Credentials, nil), extra)
  136. require.Equal(t, false, extra[UpstreamBillingProbeEnabledExtraKey])
  137. require.Equal(t, false, extra[UpstreamBillingRateSyncEnabledExtraKey])
  138. require.Equal(t, map[string]any{"status": "local"}, extra[UpstreamBillingProbeExtraKey])
  139. })
  140. t.Run("same identity preserves enabled rate sync", func(t *testing.T) {
  141. enabled := *existing
  142. enabled.Extra = mergeMap(existing.Extra, map[string]any{
  143. UpstreamBillingProbeEnabledExtraKey: true,
  144. UpstreamBillingRateSyncEnabledExtraKey: true,
  145. })
  146. extra := mergeMap(enabled.Extra, remote)
  147. reconcileCRSUpstreamBillingProbeExtra(&enabled, enabled.Platform, enabled.Type, mergeMap(enabled.Credentials, nil), extra)
  148. require.Equal(t, true, extra[UpstreamBillingProbeEnabledExtraKey])
  149. require.Equal(t, true, extra[UpstreamBillingRateSyncEnabledExtraKey])
  150. })
  151. t.Run("identity change keeps enabled and clears snapshot", func(t *testing.T) {
  152. extra := mergeMap(existing.Extra, remote)
  153. reconcileCRSUpstreamBillingProbeExtra(existing, PlatformOpenAI, AccountTypeAPIKey, map[string]any{"api_key": "changed"}, extra)
  154. require.Equal(t, false, extra[UpstreamBillingProbeEnabledExtraKey])
  155. require.Equal(t, false, extra[UpstreamBillingRateSyncEnabledExtraKey])
  156. require.NotContains(t, extra, UpstreamBillingProbeExtraKey)
  157. })
  158. // API-key 平台间切换:探测资格保留(放宽后不再限 OpenAI),开关沿用本地值,
  159. // 但平台属于探测身份,快照必须作废。
  160. for _, target := range []struct {
  161. name string
  162. platform string
  163. }{
  164. {name: "anthropic api key", platform: PlatformAnthropic},
  165. {name: "gemini api key", platform: PlatformGemini},
  166. } {
  167. t.Run(target.name+" keeps enabled and clears snapshot", func(t *testing.T) {
  168. extra := mergeMap(existing.Extra, remote)
  169. reconcileCRSUpstreamBillingProbeExtra(existing, target.platform, AccountTypeAPIKey, existing.Credentials, extra)
  170. require.Equal(t, false, extra[UpstreamBillingProbeEnabledExtraKey])
  171. require.Equal(t, false, extra[UpstreamBillingRateSyncEnabledExtraKey])
  172. require.NotContains(t, extra, UpstreamBillingProbeExtraKey)
  173. })
  174. }
  175. for _, target := range []struct {
  176. name string
  177. platform string
  178. typeName string
  179. }{
  180. {name: "anthropic oauth", platform: PlatformAnthropic, typeName: AccountTypeOAuth},
  181. {name: "openai oauth", platform: PlatformOpenAI, typeName: AccountTypeOAuth},
  182. {name: "gemini oauth", platform: PlatformGemini, typeName: AccountTypeOAuth},
  183. } {
  184. t.Run(target.name+" removes inapplicable state", func(t *testing.T) {
  185. extra := mergeMap(existing.Extra, remote)
  186. reconcileCRSUpstreamBillingProbeExtra(existing, target.platform, target.typeName, existing.Credentials, extra)
  187. require.NotContains(t, extra, UpstreamBillingProbeEnabledExtraKey)
  188. require.NotContains(t, extra, UpstreamBillingRateSyncEnabledExtraKey)
  189. require.NotContains(t, extra, UpstreamBillingProbeExtraKey)
  190. })
  191. }
  192. }