setting_service_update_test.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "math"
  8. "strconv"
  9. "testing"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  12. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  13. "github.com/stretchr/testify/require"
  14. )
  15. type settingUpdateRepoStub struct {
  16. updates map[string]string
  17. setMultipleErr error
  18. }
  19. func (s *settingUpdateRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  20. panic("unexpected Get call")
  21. }
  22. func (s *settingUpdateRepoStub) GetValue(ctx context.Context, key string) (string, error) {
  23. panic("unexpected GetValue call")
  24. }
  25. func (s *settingUpdateRepoStub) Set(ctx context.Context, key, value string) error {
  26. panic("unexpected Set call")
  27. }
  28. func (s *settingUpdateRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  29. panic("unexpected GetMultiple call")
  30. }
  31. func (s *settingUpdateRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  32. s.updates = make(map[string]string, len(settings))
  33. for k, v := range settings {
  34. s.updates[k] = v
  35. }
  36. return s.setMultipleErr
  37. }
  38. func (s *settingUpdateRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  39. panic("unexpected GetAll call")
  40. }
  41. func (s *settingUpdateRepoStub) Delete(ctx context.Context, key string) error {
  42. panic("unexpected Delete call")
  43. }
  44. type settingGetAllRepoStub struct {
  45. values map[string]string
  46. }
  47. func (s *settingGetAllRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  48. panic("unexpected Get call")
  49. }
  50. func (s *settingGetAllRepoStub) GetValue(ctx context.Context, key string) (string, error) {
  51. panic("unexpected GetValue call")
  52. }
  53. func (s *settingGetAllRepoStub) Set(ctx context.Context, key, value string) error {
  54. panic("unexpected Set call")
  55. }
  56. func (s *settingGetAllRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  57. panic("unexpected GetMultiple call")
  58. }
  59. func (s *settingGetAllRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  60. panic("unexpected SetMultiple call")
  61. }
  62. func (s *settingGetAllRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  63. out := make(map[string]string, len(s.values))
  64. for key, value := range s.values {
  65. out[key] = value
  66. }
  67. return out, nil
  68. }
  69. func (s *settingGetAllRepoStub) Delete(ctx context.Context, key string) error {
  70. panic("unexpected Delete call")
  71. }
  72. type forwardedIPMigrationRepoStub struct {
  73. values map[string]string
  74. updates map[string]string
  75. getMultipleErr error
  76. setMultipleErr error
  77. }
  78. func (s *forwardedIPMigrationRepoStub) Get(context.Context, string) (*Setting, error) {
  79. panic("unexpected Get call")
  80. }
  81. func (s *forwardedIPMigrationRepoStub) GetValue(_ context.Context, key string) (string, error) {
  82. value, ok := s.values[key]
  83. if !ok {
  84. return "", ErrSettingNotFound
  85. }
  86. return value, nil
  87. }
  88. func (s *forwardedIPMigrationRepoStub) Set(context.Context, string, string) error {
  89. panic("unexpected Set call")
  90. }
  91. func (s *forwardedIPMigrationRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
  92. if s.getMultipleErr != nil {
  93. return nil, s.getMultipleErr
  94. }
  95. result := make(map[string]string, len(keys))
  96. for _, key := range keys {
  97. if value, ok := s.values[key]; ok {
  98. result[key] = value
  99. }
  100. }
  101. return result, nil
  102. }
  103. func (s *forwardedIPMigrationRepoStub) SetMultiple(_ context.Context, values map[string]string) error {
  104. if s.setMultipleErr != nil {
  105. return s.setMultipleErr
  106. }
  107. s.updates = make(map[string]string, len(values))
  108. for key, value := range values {
  109. s.values[key] = value
  110. s.updates[key] = value
  111. }
  112. return nil
  113. }
  114. func (s *forwardedIPMigrationRepoStub) GetAll(context.Context) (map[string]string, error) {
  115. panic("unexpected GetAll call")
  116. }
  117. func (s *forwardedIPMigrationRepoStub) Delete(context.Context, string) error {
  118. panic("unexpected Delete call")
  119. }
  120. type settingAntigravityUARepoStub struct {
  121. values map[string]string
  122. }
  123. func (s *settingAntigravityUARepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  124. panic("unexpected Get call")
  125. }
  126. func (s *settingAntigravityUARepoStub) GetValue(ctx context.Context, key string) (string, error) {
  127. if value, ok := s.values[key]; ok {
  128. return value, nil
  129. }
  130. return "", ErrSettingNotFound
  131. }
  132. func (s *settingAntigravityUARepoStub) Set(ctx context.Context, key, value string) error {
  133. panic("unexpected Set call")
  134. }
  135. func (s *settingAntigravityUARepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  136. panic("unexpected GetMultiple call")
  137. }
  138. func (s *settingAntigravityUARepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  139. panic("unexpected SetMultiple call")
  140. }
  141. func (s *settingAntigravityUARepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  142. panic("unexpected GetAll call")
  143. }
  144. func (s *settingAntigravityUARepoStub) Delete(ctx context.Context, key string) error {
  145. panic("unexpected Delete call")
  146. }
  147. type defaultSubGroupReaderStub struct {
  148. byID map[int64]*Group
  149. errBy map[int64]error
  150. calls []int64
  151. }
  152. func TestSettingService_AffiliateAdminRechargeSetting(t *testing.T) {
  153. t.Run("missing value defaults to disabled", func(t *testing.T) {
  154. svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{}}, &config.Config{})
  155. settings, err := svc.GetAllSettings(context.Background())
  156. require.NoError(t, err)
  157. require.False(t, settings.AdminRechargeRebateEnabled)
  158. })
  159. t.Run("explicit value is parsed", func(t *testing.T) {
  160. svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
  161. SettingKeyAffiliateAdminRechargeEnabled: "true",
  162. }}, &config.Config{})
  163. settings, err := svc.GetAllSettings(context.Background())
  164. require.NoError(t, err)
  165. require.True(t, settings.AdminRechargeRebateEnabled)
  166. })
  167. t.Run("value is persisted", func(t *testing.T) {
  168. repo := &settingUpdateRepoStub{}
  169. svc := NewSettingService(repo, &config.Config{})
  170. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  171. AdminRechargeRebateEnabled: true,
  172. })
  173. require.NoError(t, err)
  174. require.Equal(t, "true", repo.updates[SettingKeyAffiliateAdminRechargeEnabled])
  175. })
  176. }
  177. func (s *defaultSubGroupReaderStub) GetByID(ctx context.Context, id int64) (*Group, error) {
  178. s.calls = append(s.calls, id)
  179. if err, ok := s.errBy[id]; ok {
  180. return nil, err
  181. }
  182. if g, ok := s.byID[id]; ok {
  183. return g, nil
  184. }
  185. return nil, ErrGroupNotFound
  186. }
  187. func TestSettingService_UpdateSettings_PersistsCompactHomeEnabled(t *testing.T) {
  188. repo := &settingUpdateRepoStub{}
  189. svc := NewSettingService(repo, &config.Config{})
  190. err := svc.UpdateSettings(context.Background(), &SystemSettings{CompactHomeEnabled: true})
  191. require.NoError(t, err)
  192. require.Equal(t, "true", repo.updates[SettingKeyCompactHomeEnabled])
  193. }
  194. func TestSettingService_UpdateSettings_DefaultSubscriptions_ValidGroup(t *testing.T) {
  195. repo := &settingUpdateRepoStub{}
  196. groupReader := &defaultSubGroupReaderStub{
  197. byID: map[int64]*Group{
  198. 11: {ID: 11, SubscriptionType: SubscriptionTypeSubscription},
  199. },
  200. }
  201. svc := NewSettingService(repo, &config.Config{})
  202. svc.SetDefaultSubscriptionGroupReader(groupReader)
  203. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  204. DefaultSubscriptions: []DefaultSubscriptionSetting{
  205. {GroupID: 11, ValidityDays: 30},
  206. },
  207. })
  208. require.NoError(t, err)
  209. require.Equal(t, []int64{11}, groupReader.calls)
  210. raw, ok := repo.updates[SettingKeyDefaultSubscriptions]
  211. require.True(t, ok)
  212. var got []DefaultSubscriptionSetting
  213. require.NoError(t, json.Unmarshal([]byte(raw), &got))
  214. require.Equal(t, []DefaultSubscriptionSetting{
  215. {GroupID: 11, ValidityDays: 30},
  216. }, got)
  217. }
  218. func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsNonSubscriptionGroup(t *testing.T) {
  219. repo := &settingUpdateRepoStub{}
  220. groupReader := &defaultSubGroupReaderStub{
  221. byID: map[int64]*Group{
  222. 12: {ID: 12, SubscriptionType: SubscriptionTypeStandard},
  223. },
  224. }
  225. svc := NewSettingService(repo, &config.Config{})
  226. svc.SetDefaultSubscriptionGroupReader(groupReader)
  227. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  228. DefaultSubscriptions: []DefaultSubscriptionSetting{
  229. {GroupID: 12, ValidityDays: 7},
  230. },
  231. })
  232. require.Error(t, err)
  233. require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_INVALID", infraerrors.Reason(err))
  234. require.Nil(t, repo.updates)
  235. }
  236. func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsNotFoundGroup(t *testing.T) {
  237. repo := &settingUpdateRepoStub{}
  238. groupReader := &defaultSubGroupReaderStub{
  239. errBy: map[int64]error{
  240. 13: ErrGroupNotFound,
  241. },
  242. }
  243. svc := NewSettingService(repo, &config.Config{})
  244. svc.SetDefaultSubscriptionGroupReader(groupReader)
  245. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  246. DefaultSubscriptions: []DefaultSubscriptionSetting{
  247. {GroupID: 13, ValidityDays: 7},
  248. },
  249. })
  250. require.Error(t, err)
  251. require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_INVALID", infraerrors.Reason(err))
  252. require.Equal(t, "13", infraerrors.FromError(err).Metadata["group_id"])
  253. require.Nil(t, repo.updates)
  254. }
  255. func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsDuplicateGroup(t *testing.T) {
  256. repo := &settingUpdateRepoStub{}
  257. groupReader := &defaultSubGroupReaderStub{
  258. byID: map[int64]*Group{
  259. 11: {ID: 11, SubscriptionType: SubscriptionTypeSubscription},
  260. },
  261. }
  262. svc := NewSettingService(repo, &config.Config{})
  263. svc.SetDefaultSubscriptionGroupReader(groupReader)
  264. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  265. DefaultSubscriptions: []DefaultSubscriptionSetting{
  266. {GroupID: 11, ValidityDays: 30},
  267. {GroupID: 11, ValidityDays: 60},
  268. },
  269. })
  270. require.Error(t, err)
  271. require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_DUPLICATE", infraerrors.Reason(err))
  272. require.Equal(t, "11", infraerrors.FromError(err).Metadata["group_id"])
  273. require.Nil(t, repo.updates)
  274. }
  275. func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsDuplicateGroupWithoutGroupReader(t *testing.T) {
  276. repo := &settingUpdateRepoStub{}
  277. svc := NewSettingService(repo, &config.Config{})
  278. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  279. DefaultSubscriptions: []DefaultSubscriptionSetting{
  280. {GroupID: 11, ValidityDays: 30},
  281. {GroupID: 11, ValidityDays: 60},
  282. },
  283. })
  284. require.Error(t, err)
  285. require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_DUPLICATE", infraerrors.Reason(err))
  286. require.Equal(t, "11", infraerrors.FromError(err).Metadata["group_id"])
  287. require.Nil(t, repo.updates)
  288. }
  289. func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Normalized(t *testing.T) {
  290. repo := &settingUpdateRepoStub{}
  291. svc := NewSettingService(repo, &config.Config{})
  292. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  293. RegistrationEmailSuffixWhitelist: []string{"example.com", "@EXAMPLE.com", " @foo.bar ", "*.EDU.CN"},
  294. })
  295. require.NoError(t, err)
  296. require.Equal(t, `["@example.com","@foo.bar","*.edu.cn"]`, repo.updates[SettingKeyRegistrationEmailSuffixWhitelist])
  297. }
  298. func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Invalid(t *testing.T) {
  299. repo := &settingUpdateRepoStub{}
  300. svc := NewSettingService(repo, &config.Config{})
  301. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  302. RegistrationEmailSuffixWhitelist: []string{"@invalid_domain"},
  303. })
  304. require.Error(t, err)
  305. require.Equal(t, "INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", infraerrors.Reason(err))
  306. }
  307. func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) {
  308. got := parseDefaultSubscriptions(`[{"group_id":11,"validity_days":30},{"group_id":11,"validity_days":60},{"group_id":0,"validity_days":10},{"group_id":12,"validity_days":99999}]`)
  309. require.Equal(t, []DefaultSubscriptionSetting{
  310. {GroupID: 11, ValidityDays: 30},
  311. {GroupID: 11, ValidityDays: 60},
  312. {GroupID: 12, ValidityDays: MaxValidityDays},
  313. }, got)
  314. }
  315. func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) {
  316. repo := &settingUpdateRepoStub{}
  317. svc := NewSettingService(repo, &config.Config{})
  318. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  319. TableDefaultPageSize: 50,
  320. TablePageSizeOptions: []int{20, 50, 100},
  321. })
  322. require.NoError(t, err)
  323. require.Equal(t, "50", repo.updates[SettingKeyTableDefaultPageSize])
  324. require.Equal(t, "[20,50,100]", repo.updates[SettingKeyTablePageSizeOptions])
  325. err = svc.UpdateSettings(context.Background(), &SystemSettings{
  326. TableDefaultPageSize: 1000,
  327. TablePageSizeOptions: []int{20, 100},
  328. })
  329. require.NoError(t, err)
  330. require.Equal(t, "1000", repo.updates[SettingKeyTableDefaultPageSize])
  331. require.Equal(t, "[20,100]", repo.updates[SettingKeyTablePageSizeOptions])
  332. }
  333. func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler(t *testing.T) {
  334. resetOpenAIAdvancedSchedulerSettingCacheForTest()
  335. defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
  336. repo := &settingUpdateRepoStub{}
  337. svc := NewSettingService(repo, &config.Config{})
  338. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  339. PaymentVisibleMethodAlipaySource: "alipay",
  340. PaymentVisibleMethodWxpaySource: "easypay",
  341. PaymentVisibleMethodAlipayEnabled: true,
  342. PaymentVisibleMethodWxpayEnabled: false,
  343. OpenAILowUpstreamRatePriorityEnabled: true,
  344. OpenAIOAuthSchedulingRateMultiplier: 0.05,
  345. OpenAIAdvancedSchedulerEnabled: true,
  346. OpenAIAdvancedSchedulerStickyWeightedEnabled: true,
  347. OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: true,
  348. OpenAIAdvancedSchedulerLBTopK: " 3 ",
  349. OpenAIAdvancedSchedulerWeightPriority: "2.50",
  350. OpenAIAdvancedSchedulerWeightLoad: "0",
  351. OpenAIAdvancedSchedulerWeightQueue: "0.75",
  352. OpenAIAdvancedSchedulerWeightErrorRate: "1.25",
  353. OpenAIAdvancedSchedulerWeightTTFT: "0.5",
  354. OpenAIAdvancedSchedulerWeightReset: "",
  355. OpenAIAdvancedSchedulerWeightQuotaHeadroom: "0.2",
  356. OpenAIAdvancedSchedulerWeightUpstreamCost: "1.5",
  357. OpenAIAdvancedSchedulerWeightPreviousResponse: "8",
  358. OpenAIAdvancedSchedulerWeightSessionSticky: "4",
  359. })
  360. require.NoError(t, err)
  361. require.Equal(t, VisibleMethodSourceOfficialAlipay, repo.updates[SettingPaymentVisibleMethodAlipaySource])
  362. require.Equal(t, VisibleMethodSourceEasyPayWechat, repo.updates[SettingPaymentVisibleMethodWxpaySource])
  363. require.Equal(t, "true", repo.updates[SettingPaymentVisibleMethodAlipayEnabled])
  364. require.Equal(t, "false", repo.updates[SettingPaymentVisibleMethodWxpayEnabled])
  365. require.Equal(t, "true", repo.updates[SettingKeyOpenAILowUpstreamRatePriorityEnabled])
  366. require.Equal(t, "0.05", repo.updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
  367. require.Equal(t, "true", repo.updates[openAIAdvancedSchedulerSettingKey])
  368. require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled])
  369. require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled])
  370. require.Equal(t, "3", repo.updates[SettingKeyOpenAIAdvancedSchedulerLBTopK])
  371. require.Equal(t, "2.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority])
  372. require.Equal(t, "0", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad])
  373. require.Equal(t, "0.75", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue])
  374. require.Equal(t, "1.25", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate])
  375. require.Equal(t, "0.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT])
  376. require.Equal(t, "", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightReset])
  377. require.Equal(t, "0.2", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom])
  378. require.Equal(t, "1.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost])
  379. require.Equal(t, "8", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse])
  380. require.Equal(t, "4", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky])
  381. }
  382. func TestSettingService_UpdateSettingsRejectsInvalidOpenAIOAuthSchedulingRateMultiplier(t *testing.T) {
  383. repo := &settingUpdateRepoStub{}
  384. svc := NewSettingService(repo, &config.Config{})
  385. for _, rate := range []float64{-0.01, math.NaN(), math.Inf(1)} {
  386. err := svc.UpdateSettings(context.Background(), &SystemSettings{OpenAIOAuthSchedulingRateMultiplier: rate})
  387. require.Error(t, err)
  388. }
  389. }
  390. func TestSettingService_UpdateSettings_OpenAIAdvancedSchedulerWeightSums(t *testing.T) {
  391. maxFloat := strconv.FormatFloat(math.MaxFloat64, 'g', -1, 64)
  392. tests := []struct {
  393. name string
  394. weights SystemSettings
  395. wantErr bool
  396. }{
  397. {
  398. name: "reset only base is valid",
  399. weights: SystemSettings{
  400. OpenAIAdvancedSchedulerWeightPriority: "0",
  401. OpenAIAdvancedSchedulerWeightLoad: "0",
  402. OpenAIAdvancedSchedulerWeightQueue: "0",
  403. OpenAIAdvancedSchedulerWeightErrorRate: "0",
  404. OpenAIAdvancedSchedulerWeightTTFT: "0",
  405. OpenAIAdvancedSchedulerWeightReset: "1",
  406. OpenAIAdvancedSchedulerWeightQuotaHeadroom: "0",
  407. OpenAIAdvancedSchedulerWeightUpstreamCost: "0",
  408. OpenAIAdvancedSchedulerWeightPreviousResponse: "0",
  409. OpenAIAdvancedSchedulerWeightSessionSticky: "0",
  410. },
  411. },
  412. {
  413. name: "base sum overflow is rejected",
  414. weights: SystemSettings{
  415. OpenAIAdvancedSchedulerWeightPriority: maxFloat,
  416. OpenAIAdvancedSchedulerWeightLoad: maxFloat,
  417. },
  418. wantErr: true,
  419. },
  420. {
  421. name: "sticky total sum overflow is rejected",
  422. weights: SystemSettings{
  423. OpenAIAdvancedSchedulerWeightPriority: maxFloat,
  424. OpenAIAdvancedSchedulerWeightPreviousResponse: maxFloat,
  425. },
  426. wantErr: true,
  427. },
  428. }
  429. for _, tt := range tests {
  430. t.Run(tt.name, func(t *testing.T) {
  431. svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
  432. err := svc.UpdateSettings(context.Background(), &tt.weights)
  433. if tt.wantErr {
  434. require.Error(t, err)
  435. return
  436. }
  437. require.NoError(t, err)
  438. })
  439. }
  440. }
  441. func TestSettingService_ParseSettingsDefaultsOpenAIOAuthSchedulingRateMultiplier(t *testing.T) {
  442. svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
  443. require.Equal(t, 1.0, svc.parseSettings(map[string]string{}).OpenAIOAuthSchedulingRateMultiplier)
  444. require.Equal(t, 0.05, svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05"}).OpenAIOAuthSchedulingRateMultiplier)
  445. }
  446. func TestSettingService_GetAllSettings_OpenAIAdvancedSchedulerEffectiveValuesUseConfig(t *testing.T) {
  447. cfg := &config.Config{}
  448. cfg.Gateway.OpenAIWS.LBTopK = 13
  449. cfg.Gateway.OpenAIWS.SchedulerScoreWeights = config.GatewayOpenAIWSSchedulerScoreWeights{
  450. Priority: 2,
  451. Load: 3,
  452. Queue: 4,
  453. ErrorRate: 5,
  454. TTFT: 6,
  455. Reset: 7,
  456. QuotaHeadroom: 8,
  457. UpstreamCost: 9,
  458. PreviousResponse: 10,
  459. SessionSticky: 11,
  460. }
  461. svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
  462. SettingKeyOpenAIAdvancedSchedulerLBTopK: "3",
  463. SettingKeyOpenAIAdvancedSchedulerWeightPriority: "99",
  464. SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "88",
  465. }}, cfg)
  466. settings, err := svc.GetAllSettings(context.Background())
  467. require.NoError(t, err)
  468. require.Equal(t, "3", settings.OpenAIAdvancedSchedulerLBTopK)
  469. require.Equal(t, "99", settings.OpenAIAdvancedSchedulerWeightPriority)
  470. require.Equal(t, "88", settings.OpenAIAdvancedSchedulerWeightSessionSticky)
  471. require.Equal(t, "13", settings.OpenAIAdvancedSchedulerEffectiveLBTopK)
  472. require.Equal(t, "2", settings.OpenAIAdvancedSchedulerEffectiveWeightPriority)
  473. require.Equal(t, "3", settings.OpenAIAdvancedSchedulerEffectiveWeightLoad)
  474. require.Equal(t, "9", settings.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost)
  475. require.Equal(t, "11", settings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky)
  476. }
  477. func TestSettingService_UpdateSettings_AntigravityUserAgentVersion(t *testing.T) {
  478. repo := &settingUpdateRepoStub{}
  479. svc := NewSettingService(repo, &config.Config{})
  480. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  481. AntigravityUserAgentVersion: "1.23.2",
  482. })
  483. require.NoError(t, err)
  484. require.Equal(t, "1.23.2", repo.updates[SettingKeyAntigravityUserAgentVersion])
  485. }
  486. func TestSettingService_InitializeDefaultSettingsPersistsConfiguredForwardedClientIPHeaders(t *testing.T) {
  487. repo := &forwardedIPMigrationRepoStub{values: map[string]string{}}
  488. cfg := &config.Config{}
  489. cfg.SetForwardedClientIPSettings(true, []string{"X-Cdn-Ip", "True-Client-Ip"})
  490. svc := NewSettingService(repo, cfg)
  491. require.NoError(t, svc.InitializeDefaultSettings(context.Background()))
  492. require.JSONEq(t, `["X-Cdn-Ip","True-Client-Ip"]`, repo.values[SettingKeyForwardedClientIPHeaders])
  493. }
  494. func TestSettingService_UpdateSettings_APIKeyACLTrustForwardedIPRefreshesConfig(t *testing.T) {
  495. repo := &settingUpdateRepoStub{}
  496. cfg := &config.Config{}
  497. svc := NewSettingService(repo, cfg)
  498. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  499. APIKeyACLTrustForwardedIP: true,
  500. ForwardedClientIPHeaders: []string{" x-cdn-ip ", "X-CDN-IP", "true-client-ip"},
  501. })
  502. require.NoError(t, err)
  503. require.Equal(t, "true", repo.updates[SettingKeyAPIKeyACLTrustForwardedIP])
  504. require.JSONEq(t, `["X-Cdn-Ip","True-Client-Ip"]`, repo.updates[SettingKeyForwardedClientIPHeaders])
  505. runtimeSettings := cfg.ForwardedClientIPSettings()
  506. require.True(t, runtimeSettings.TrustForwardedIP)
  507. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, runtimeSettings.Headers)
  508. runtimeSettings.Headers[0] = "X-Mutated"
  509. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
  510. }
  511. func TestSettingService_UpdateSettings_RejectsInvalidForwardedClientIPHeadersWithoutRefreshing(t *testing.T) {
  512. repo := &settingUpdateRepoStub{}
  513. cfg := &config.Config{}
  514. cfg.SetForwardedClientIPSettings(true, []string{"X-Existing-IP"})
  515. svc := NewSettingService(repo, cfg)
  516. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  517. ForwardedClientIPHeaders: []string{"X Invalid"},
  518. })
  519. require.Error(t, err)
  520. require.Nil(t, repo.updates)
  521. runtimeSettings := cfg.ForwardedClientIPSettings()
  522. require.True(t, runtimeSettings.TrustForwardedIP)
  523. require.Equal(t, []string{"X-Existing-IP"}, runtimeSettings.Headers)
  524. }
  525. func TestSettingService_UpdateSettings_WriteFailureDoesNotRefreshForwardedIPRuntime(t *testing.T) {
  526. repo := &settingUpdateRepoStub{setMultipleErr: errors.New("database unavailable")}
  527. cfg := &config.Config{}
  528. cfg.SetForwardedClientIPSettings(false, []string{"X-Existing-IP"})
  529. svc := NewSettingService(repo, cfg)
  530. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  531. APIKeyACLTrustForwardedIP: true,
  532. ForwardedClientIPHeaders: []string{"X-New-IP"},
  533. })
  534. require.ErrorContains(t, err, "database unavailable")
  535. runtimeSettings := cfg.ForwardedClientIPSettings()
  536. require.False(t, runtimeSettings.TrustForwardedIP)
  537. require.Equal(t, []string{"X-Existing-IP"}, runtimeSettings.Headers)
  538. }
  539. func TestSettingService_ParseSettings_APIKeyACLTrustForwardedIPFallsBackToConfigWhenMissing(t *testing.T) {
  540. cfg := &config.Config{}
  541. cfg.Security.TrustForwardedIPForAPIKeyACL = true
  542. svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
  543. got := svc.parseSettings(map[string]string{})
  544. require.True(t, got.APIKeyACLTrustForwardedIP)
  545. }
  546. func TestSettingService_ParseSettings_APIKeyACLTrustForwardedIPUsesStoredValue(t *testing.T) {
  547. cfg := &config.Config{}
  548. cfg.SetTrustForwardedIPForAPIKeyACL(true)
  549. svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
  550. got := svc.parseSettings(map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"})
  551. require.False(t, got.APIKeyACLTrustForwardedIP)
  552. }
  553. func TestSettingService_ParseSettings_ForwardedClientIPHeaders(t *testing.T) {
  554. cfg := &config.Config{}
  555. cfg.SetForwardedClientIPSettings(true, []string{"X-Config-IP"})
  556. svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
  557. t.Run("stored value is normalized", func(t *testing.T) {
  558. got := svc.parseSettings(map[string]string{
  559. SettingKeyForwardedClientIPHeaders: `[" x-cdn-ip ","X-CDN-IP","true-client-ip"]`,
  560. })
  561. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, got.ForwardedClientIPHeaders)
  562. })
  563. t.Run("missing value falls back to config", func(t *testing.T) {
  564. got := svc.parseSettings(map[string]string{})
  565. require.Equal(t, []string{"X-Config-IP"}, got.ForwardedClientIPHeaders)
  566. })
  567. t.Run("malformed value disables forwarded trust", func(t *testing.T) {
  568. got := svc.parseSettings(map[string]string{
  569. SettingKeyAPIKeyACLTrustForwardedIP: "true",
  570. SettingKeyForwardedClientIPHeaders: `{"not":"an array"}`,
  571. })
  572. require.False(t, got.APIKeyACLTrustForwardedIP)
  573. require.Empty(t, got.ForwardedClientIPHeaders)
  574. })
  575. }
  576. func TestSettingService_LoadForwardedClientIPSettingsMigration(t *testing.T) {
  577. tests := []struct {
  578. name string
  579. values map[string]string
  580. trustedProxiesSet bool
  581. configDefault bool
  582. wantEnabled bool
  583. wantForwardedIPUpdate string
  584. wantMigrationMarkerSet bool
  585. }{
  586. {
  587. name: "missing setting follows configured default",
  588. values: map[string]string{},
  589. configDefault: true,
  590. wantEnabled: true,
  591. wantMigrationMarkerSet: true,
  592. },
  593. {
  594. name: "legacy false without proxy config migrates to compatibility",
  595. values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
  596. wantEnabled: true,
  597. wantForwardedIPUpdate: "true",
  598. wantMigrationMarkerSet: true,
  599. },
  600. {
  601. name: "legacy false with explicit proxy config stays secure",
  602. values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
  603. trustedProxiesSet: true,
  604. wantEnabled: false,
  605. wantMigrationMarkerSet: true,
  606. },
  607. {
  608. name: "completed migration preserves later false choice",
  609. values: map[string]string{
  610. SettingKeyAPIKeyACLTrustForwardedIP: "false",
  611. settingKeyForwardedClientIPModeV2: "true",
  612. },
  613. wantEnabled: false,
  614. },
  615. }
  616. for _, test := range tests {
  617. t.Run(test.name, func(t *testing.T) {
  618. repo := &forwardedIPMigrationRepoStub{values: test.values}
  619. cfg := &config.Config{Server: config.ServerConfig{TrustedProxiesConfigured: test.trustedProxiesSet}}
  620. cfg.Security.TrustForwardedIPForAPIKeyACL = test.configDefault
  621. svc := NewSettingService(repo, cfg)
  622. require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
  623. require.Equal(t, test.wantEnabled, cfg.TrustForwardedIPForAPIKeyACL())
  624. require.Equal(t, test.wantForwardedIPUpdate, repo.updates[SettingKeyAPIKeyACLTrustForwardedIP])
  625. require.JSONEq(t, `[]`, repo.updates[SettingKeyForwardedClientIPHeaders])
  626. if test.wantMigrationMarkerSet {
  627. require.Equal(t, "true", repo.updates[settingKeyForwardedClientIPModeV2])
  628. } else {
  629. require.NotContains(t, repo.updates, settingKeyForwardedClientIPModeV2)
  630. }
  631. })
  632. }
  633. }
  634. func TestSettingService_LoadForwardedClientIPSettingsLoadsHeaders(t *testing.T) {
  635. repo := &forwardedIPMigrationRepoStub{values: map[string]string{
  636. SettingKeyAPIKeyACLTrustForwardedIP: "true",
  637. SettingKeyForwardedClientIPHeaders: `[" x-cdn-ip ","true-client-ip"]`,
  638. settingKeyForwardedClientIPModeV2: "true",
  639. }}
  640. cfg := &config.Config{}
  641. svc := NewSettingService(repo, cfg)
  642. require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
  643. runtimeSettings := cfg.ForwardedClientIPSettings()
  644. require.True(t, runtimeSettings.TrustForwardedIP)
  645. require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, runtimeSettings.Headers)
  646. require.Nil(t, repo.updates)
  647. }
  648. func TestSettingService_LoadForwardedClientIPSettingsMalformedHeadersDisablesCustomTrust(t *testing.T) {
  649. repo := &forwardedIPMigrationRepoStub{values: map[string]string{
  650. SettingKeyAPIKeyACLTrustForwardedIP: "true",
  651. SettingKeyForwardedClientIPHeaders: `["X Invalid"]`,
  652. }}
  653. cfg := &config.Config{}
  654. svc := NewSettingService(repo, cfg)
  655. err := svc.LoadForwardedClientIPSettings(context.Background())
  656. require.ErrorContains(t, err, "load forwarded client ip headers")
  657. runtimeSettings := cfg.ForwardedClientIPSettings()
  658. require.False(t, runtimeSettings.TrustForwardedIP)
  659. require.Empty(t, runtimeSettings.Headers)
  660. require.Equal(t, "true", repo.updates[settingKeyForwardedClientIPModeV2])
  661. require.NotContains(t, repo.updates, SettingKeyAPIKeyACLTrustForwardedIP)
  662. }
  663. func TestSettingService_LoadForwardedClientIPSettingsBackfillsConfigHeaders(t *testing.T) {
  664. repo := &forwardedIPMigrationRepoStub{values: map[string]string{
  665. settingKeyForwardedClientIPModeV2: "true",
  666. }}
  667. cfg := &config.Config{}
  668. cfg.SetForwardedClientIPSettings(false, []string{"X-Config-IP"})
  669. svc := NewSettingService(repo, cfg)
  670. require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
  671. require.JSONEq(t, `["X-Config-IP"]`, repo.updates[SettingKeyForwardedClientIPHeaders])
  672. require.Equal(t, []string{"X-Config-IP"}, cfg.ForwardedClientIPSettings().Headers)
  673. }
  674. func TestSettingService_LoadForwardedClientIPSettingsReadFailureFailsClosed(t *testing.T) {
  675. repo := &forwardedIPMigrationRepoStub{
  676. getMultipleErr: errors.New("database unavailable"),
  677. }
  678. cfg := &config.Config{}
  679. cfg.SetTrustForwardedIPForAPIKeyACL(true)
  680. svc := NewSettingService(repo, cfg)
  681. err := svc.LoadForwardedClientIPSettings(context.Background())
  682. require.ErrorContains(t, err, "get forwarded client ip settings")
  683. runtimeSettings := cfg.ForwardedClientIPSettings()
  684. require.False(t, runtimeSettings.TrustForwardedIP)
  685. require.Empty(t, runtimeSettings.Headers)
  686. }
  687. func TestSettingService_LoadForwardedClientIPSettingsWriteFailureUsesComputedMode(t *testing.T) {
  688. tests := []struct {
  689. name string
  690. trustedProxiesSet bool
  691. wantEnabled bool
  692. }{
  693. {name: "compatibility migration remains effective", wantEnabled: true},
  694. {name: "explicit proxy policy remains secure", trustedProxiesSet: true, wantEnabled: false},
  695. }
  696. for _, test := range tests {
  697. t.Run(test.name, func(t *testing.T) {
  698. repo := &forwardedIPMigrationRepoStub{
  699. values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
  700. setMultipleErr: errors.New("database unavailable"),
  701. }
  702. cfg := &config.Config{Server: config.ServerConfig{TrustedProxiesConfigured: test.trustedProxiesSet}}
  703. svc := NewSettingService(repo, cfg)
  704. err := svc.LoadForwardedClientIPSettings(context.Background())
  705. require.ErrorContains(t, err, "migrate forwarded client ip setting")
  706. require.Equal(t, test.wantEnabled, cfg.TrustForwardedIPForAPIKeyACL())
  707. })
  708. }
  709. }
  710. func TestSettingService_GetAntigravityUserAgentVersion_Precedence(t *testing.T) {
  711. t.Run("后台设置优先", func(t *testing.T) {
  712. svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
  713. SettingKeyAntigravityUserAgentVersion: "1.24.0",
  714. }}, &config.Config{})
  715. require.Equal(t, "1.24.0", svc.GetAntigravityUserAgentVersion(context.Background()))
  716. })
  717. t.Run("空值回退配置默认值", func(t *testing.T) {
  718. svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
  719. SettingKeyAntigravityUserAgentVersion: "",
  720. }}, &config.Config{})
  721. require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
  722. })
  723. t.Run("缺失回退配置默认值", func(t *testing.T) {
  724. svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{}}, &config.Config{})
  725. require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
  726. })
  727. }
  728. func TestSettingService_UpdateSettings_RejectsInvalidPaymentVisibleMethodSource(t *testing.T) {
  729. repo := &settingUpdateRepoStub{}
  730. svc := NewSettingService(repo, &config.Config{})
  731. err := svc.UpdateSettings(context.Background(), &SystemSettings{
  732. PaymentVisibleMethodAlipaySource: "not-a-provider",
  733. })
  734. require.Error(t, err)
  735. require.Equal(t, "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE", infraerrors.Reason(err))
  736. require.Nil(t, repo.updates)
  737. }
  738. func TestSettingService_PasskeySwitchPersistsAndDefaultsToConfigured(t *testing.T) {
  739. cfg := &config.Config{WebAuthn: config.WebAuthnConfig{
  740. Enabled: true,
  741. RPID: "sub3.nebula-spaces.com",
  742. RPOrigins: []string{"https://sub3.nebula-spaces.com"},
  743. }}
  744. runtimeRepo := &forwardedIPMigrationRepoStub{values: map[string]string{}}
  745. runtimeService := NewSettingService(runtimeRepo, cfg)
  746. enabled, err := runtimeService.PasskeyEnabled(context.Background())
  747. require.NoError(t, err)
  748. require.True(t, enabled)
  749. updateRepo := &settingUpdateRepoStub{}
  750. updateService := NewSettingService(updateRepo, cfg)
  751. require.NoError(t, updateService.UpdateSettings(context.Background(), &SystemSettings{
  752. PasskeyEnabled: false,
  753. }))
  754. require.Equal(t, "false", updateRepo.updates[SettingKeyPasskeyEnabled])
  755. runtimeRepo.values[SettingKeyPasskeyEnabled] = "false"
  756. enabled, err = runtimeService.PasskeyEnabled(context.Background())
  757. require.NoError(t, err)
  758. require.False(t, enabled)
  759. publicSettings, err := runtimeService.GetPublicSettings(context.Background())
  760. require.NoError(t, err)
  761. require.False(t, publicSettings.PasskeyEnabled)
  762. }
  763. // 移除 WebAuthn 配置后,残留的 passkey_enabled="true" 不得再让 GetAllSettings
  764. // 报告开关开启:admin 更新门控以此为准,一旦误报为 true 会拒绝所有设置保存,
  765. // 而此时前端开关处于禁用态,管理员无法在 UI 里自救。
  766. func TestSettingService_StalePasskeyTrueWithoutConfigReportsDisabled(t *testing.T) {
  767. repo := &settingGetAllRepoStub{values: map[string]string{
  768. SettingKeyPasskeyEnabled: "true",
  769. }}
  770. service := NewSettingService(repo, &config.Config{})
  771. settings, err := service.GetAllSettings(context.Background())
  772. require.NoError(t, err)
  773. require.False(t, settings.PasskeyEnabled)
  774. }