| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905 |
- //go:build unit
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "math"
- "strconv"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/stretchr/testify/require"
- )
- type settingUpdateRepoStub struct {
- updates map[string]string
- setMultipleErr error
- }
- func (s *settingUpdateRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
- panic("unexpected Get call")
- }
- func (s *settingUpdateRepoStub) GetValue(ctx context.Context, key string) (string, error) {
- panic("unexpected GetValue call")
- }
- func (s *settingUpdateRepoStub) Set(ctx context.Context, key, value string) error {
- panic("unexpected Set call")
- }
- func (s *settingUpdateRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
- panic("unexpected GetMultiple call")
- }
- func (s *settingUpdateRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
- s.updates = make(map[string]string, len(settings))
- for k, v := range settings {
- s.updates[k] = v
- }
- return s.setMultipleErr
- }
- func (s *settingUpdateRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
- panic("unexpected GetAll call")
- }
- func (s *settingUpdateRepoStub) Delete(ctx context.Context, key string) error {
- panic("unexpected Delete call")
- }
- type settingGetAllRepoStub struct {
- values map[string]string
- }
- func (s *settingGetAllRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
- panic("unexpected Get call")
- }
- func (s *settingGetAllRepoStub) GetValue(ctx context.Context, key string) (string, error) {
- panic("unexpected GetValue call")
- }
- func (s *settingGetAllRepoStub) Set(ctx context.Context, key, value string) error {
- panic("unexpected Set call")
- }
- func (s *settingGetAllRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
- panic("unexpected GetMultiple call")
- }
- func (s *settingGetAllRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
- panic("unexpected SetMultiple call")
- }
- func (s *settingGetAllRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
- out := make(map[string]string, len(s.values))
- for key, value := range s.values {
- out[key] = value
- }
- return out, nil
- }
- func (s *settingGetAllRepoStub) Delete(ctx context.Context, key string) error {
- panic("unexpected Delete call")
- }
- type forwardedIPMigrationRepoStub struct {
- values map[string]string
- updates map[string]string
- getMultipleErr error
- setMultipleErr error
- }
- func (s *forwardedIPMigrationRepoStub) Get(context.Context, string) (*Setting, error) {
- panic("unexpected Get call")
- }
- func (s *forwardedIPMigrationRepoStub) GetValue(_ context.Context, key string) (string, error) {
- value, ok := s.values[key]
- if !ok {
- return "", ErrSettingNotFound
- }
- return value, nil
- }
- func (s *forwardedIPMigrationRepoStub) Set(context.Context, string, string) error {
- panic("unexpected Set call")
- }
- func (s *forwardedIPMigrationRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
- if s.getMultipleErr != nil {
- return nil, s.getMultipleErr
- }
- result := make(map[string]string, len(keys))
- for _, key := range keys {
- if value, ok := s.values[key]; ok {
- result[key] = value
- }
- }
- return result, nil
- }
- func (s *forwardedIPMigrationRepoStub) SetMultiple(_ context.Context, values map[string]string) error {
- if s.setMultipleErr != nil {
- return s.setMultipleErr
- }
- s.updates = make(map[string]string, len(values))
- for key, value := range values {
- s.values[key] = value
- s.updates[key] = value
- }
- return nil
- }
- func (s *forwardedIPMigrationRepoStub) GetAll(context.Context) (map[string]string, error) {
- panic("unexpected GetAll call")
- }
- func (s *forwardedIPMigrationRepoStub) Delete(context.Context, string) error {
- panic("unexpected Delete call")
- }
- type settingAntigravityUARepoStub struct {
- values map[string]string
- }
- func (s *settingAntigravityUARepoStub) Get(ctx context.Context, key string) (*Setting, error) {
- panic("unexpected Get call")
- }
- func (s *settingAntigravityUARepoStub) GetValue(ctx context.Context, key string) (string, error) {
- if value, ok := s.values[key]; ok {
- return value, nil
- }
- return "", ErrSettingNotFound
- }
- func (s *settingAntigravityUARepoStub) Set(ctx context.Context, key, value string) error {
- panic("unexpected Set call")
- }
- func (s *settingAntigravityUARepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
- panic("unexpected GetMultiple call")
- }
- func (s *settingAntigravityUARepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
- panic("unexpected SetMultiple call")
- }
- func (s *settingAntigravityUARepoStub) GetAll(ctx context.Context) (map[string]string, error) {
- panic("unexpected GetAll call")
- }
- func (s *settingAntigravityUARepoStub) Delete(ctx context.Context, key string) error {
- panic("unexpected Delete call")
- }
- type defaultSubGroupReaderStub struct {
- byID map[int64]*Group
- errBy map[int64]error
- calls []int64
- }
- func TestSettingService_AffiliateAdminRechargeSetting(t *testing.T) {
- t.Run("missing value defaults to disabled", func(t *testing.T) {
- svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{}}, &config.Config{})
- settings, err := svc.GetAllSettings(context.Background())
- require.NoError(t, err)
- require.False(t, settings.AdminRechargeRebateEnabled)
- })
- t.Run("explicit value is parsed", func(t *testing.T) {
- svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
- SettingKeyAffiliateAdminRechargeEnabled: "true",
- }}, &config.Config{})
- settings, err := svc.GetAllSettings(context.Background())
- require.NoError(t, err)
- require.True(t, settings.AdminRechargeRebateEnabled)
- })
- t.Run("value is persisted", func(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- AdminRechargeRebateEnabled: true,
- })
- require.NoError(t, err)
- require.Equal(t, "true", repo.updates[SettingKeyAffiliateAdminRechargeEnabled])
- })
- }
- func (s *defaultSubGroupReaderStub) GetByID(ctx context.Context, id int64) (*Group, error) {
- s.calls = append(s.calls, id)
- if err, ok := s.errBy[id]; ok {
- return nil, err
- }
- if g, ok := s.byID[id]; ok {
- return g, nil
- }
- return nil, ErrGroupNotFound
- }
- func TestSettingService_UpdateSettings_PersistsCompactHomeEnabled(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{CompactHomeEnabled: true})
- require.NoError(t, err)
- require.Equal(t, "true", repo.updates[SettingKeyCompactHomeEnabled])
- }
- func TestSettingService_UpdateSettings_DefaultSubscriptions_ValidGroup(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- groupReader := &defaultSubGroupReaderStub{
- byID: map[int64]*Group{
- 11: {ID: 11, SubscriptionType: SubscriptionTypeSubscription},
- },
- }
- svc := NewSettingService(repo, &config.Config{})
- svc.SetDefaultSubscriptionGroupReader(groupReader)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- DefaultSubscriptions: []DefaultSubscriptionSetting{
- {GroupID: 11, ValidityDays: 30},
- },
- })
- require.NoError(t, err)
- require.Equal(t, []int64{11}, groupReader.calls)
- raw, ok := repo.updates[SettingKeyDefaultSubscriptions]
- require.True(t, ok)
- var got []DefaultSubscriptionSetting
- require.NoError(t, json.Unmarshal([]byte(raw), &got))
- require.Equal(t, []DefaultSubscriptionSetting{
- {GroupID: 11, ValidityDays: 30},
- }, got)
- }
- func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsNonSubscriptionGroup(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- groupReader := &defaultSubGroupReaderStub{
- byID: map[int64]*Group{
- 12: {ID: 12, SubscriptionType: SubscriptionTypeStandard},
- },
- }
- svc := NewSettingService(repo, &config.Config{})
- svc.SetDefaultSubscriptionGroupReader(groupReader)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- DefaultSubscriptions: []DefaultSubscriptionSetting{
- {GroupID: 12, ValidityDays: 7},
- },
- })
- require.Error(t, err)
- require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_INVALID", infraerrors.Reason(err))
- require.Nil(t, repo.updates)
- }
- func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsNotFoundGroup(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- groupReader := &defaultSubGroupReaderStub{
- errBy: map[int64]error{
- 13: ErrGroupNotFound,
- },
- }
- svc := NewSettingService(repo, &config.Config{})
- svc.SetDefaultSubscriptionGroupReader(groupReader)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- DefaultSubscriptions: []DefaultSubscriptionSetting{
- {GroupID: 13, ValidityDays: 7},
- },
- })
- require.Error(t, err)
- require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_INVALID", infraerrors.Reason(err))
- require.Equal(t, "13", infraerrors.FromError(err).Metadata["group_id"])
- require.Nil(t, repo.updates)
- }
- func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsDuplicateGroup(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- groupReader := &defaultSubGroupReaderStub{
- byID: map[int64]*Group{
- 11: {ID: 11, SubscriptionType: SubscriptionTypeSubscription},
- },
- }
- svc := NewSettingService(repo, &config.Config{})
- svc.SetDefaultSubscriptionGroupReader(groupReader)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- DefaultSubscriptions: []DefaultSubscriptionSetting{
- {GroupID: 11, ValidityDays: 30},
- {GroupID: 11, ValidityDays: 60},
- },
- })
- require.Error(t, err)
- require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_DUPLICATE", infraerrors.Reason(err))
- require.Equal(t, "11", infraerrors.FromError(err).Metadata["group_id"])
- require.Nil(t, repo.updates)
- }
- func TestSettingService_UpdateSettings_DefaultSubscriptions_RejectsDuplicateGroupWithoutGroupReader(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- DefaultSubscriptions: []DefaultSubscriptionSetting{
- {GroupID: 11, ValidityDays: 30},
- {GroupID: 11, ValidityDays: 60},
- },
- })
- require.Error(t, err)
- require.Equal(t, "DEFAULT_SUBSCRIPTION_GROUP_DUPLICATE", infraerrors.Reason(err))
- require.Equal(t, "11", infraerrors.FromError(err).Metadata["group_id"])
- require.Nil(t, repo.updates)
- }
- func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Normalized(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- RegistrationEmailSuffixWhitelist: []string{"example.com", "@EXAMPLE.com", " @foo.bar ", "*.EDU.CN"},
- })
- require.NoError(t, err)
- require.Equal(t, `["@example.com","@foo.bar","*.edu.cn"]`, repo.updates[SettingKeyRegistrationEmailSuffixWhitelist])
- }
- func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Invalid(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- RegistrationEmailSuffixWhitelist: []string{"@invalid_domain"},
- })
- require.Error(t, err)
- require.Equal(t, "INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", infraerrors.Reason(err))
- }
- func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) {
- 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}]`)
- require.Equal(t, []DefaultSubscriptionSetting{
- {GroupID: 11, ValidityDays: 30},
- {GroupID: 11, ValidityDays: 60},
- {GroupID: 12, ValidityDays: MaxValidityDays},
- }, got)
- }
- func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- TableDefaultPageSize: 50,
- TablePageSizeOptions: []int{20, 50, 100},
- })
- require.NoError(t, err)
- require.Equal(t, "50", repo.updates[SettingKeyTableDefaultPageSize])
- require.Equal(t, "[20,50,100]", repo.updates[SettingKeyTablePageSizeOptions])
- err = svc.UpdateSettings(context.Background(), &SystemSettings{
- TableDefaultPageSize: 1000,
- TablePageSizeOptions: []int{20, 100},
- })
- require.NoError(t, err)
- require.Equal(t, "1000", repo.updates[SettingKeyTableDefaultPageSize])
- require.Equal(t, "[20,100]", repo.updates[SettingKeyTablePageSizeOptions])
- }
- func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler(t *testing.T) {
- resetOpenAIAdvancedSchedulerSettingCacheForTest()
- defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- PaymentVisibleMethodAlipaySource: "alipay",
- PaymentVisibleMethodWxpaySource: "easypay",
- PaymentVisibleMethodAlipayEnabled: true,
- PaymentVisibleMethodWxpayEnabled: false,
- OpenAILowUpstreamRatePriorityEnabled: true,
- OpenAIOAuthSchedulingRateMultiplier: 0.05,
- OpenAIAdvancedSchedulerEnabled: true,
- OpenAIAdvancedSchedulerStickyWeightedEnabled: true,
- OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: true,
- OpenAIAdvancedSchedulerLBTopK: " 3 ",
- OpenAIAdvancedSchedulerWeightPriority: "2.50",
- OpenAIAdvancedSchedulerWeightLoad: "0",
- OpenAIAdvancedSchedulerWeightQueue: "0.75",
- OpenAIAdvancedSchedulerWeightErrorRate: "1.25",
- OpenAIAdvancedSchedulerWeightTTFT: "0.5",
- OpenAIAdvancedSchedulerWeightReset: "",
- OpenAIAdvancedSchedulerWeightQuotaHeadroom: "0.2",
- OpenAIAdvancedSchedulerWeightUpstreamCost: "1.5",
- OpenAIAdvancedSchedulerWeightPreviousResponse: "8",
- OpenAIAdvancedSchedulerWeightSessionSticky: "4",
- })
- require.NoError(t, err)
- require.Equal(t, VisibleMethodSourceOfficialAlipay, repo.updates[SettingPaymentVisibleMethodAlipaySource])
- require.Equal(t, VisibleMethodSourceEasyPayWechat, repo.updates[SettingPaymentVisibleMethodWxpaySource])
- require.Equal(t, "true", repo.updates[SettingPaymentVisibleMethodAlipayEnabled])
- require.Equal(t, "false", repo.updates[SettingPaymentVisibleMethodWxpayEnabled])
- require.Equal(t, "true", repo.updates[SettingKeyOpenAILowUpstreamRatePriorityEnabled])
- require.Equal(t, "0.05", repo.updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
- require.Equal(t, "true", repo.updates[openAIAdvancedSchedulerSettingKey])
- require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled])
- require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled])
- require.Equal(t, "3", repo.updates[SettingKeyOpenAIAdvancedSchedulerLBTopK])
- require.Equal(t, "2.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority])
- require.Equal(t, "0", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad])
- require.Equal(t, "0.75", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue])
- require.Equal(t, "1.25", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate])
- require.Equal(t, "0.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT])
- require.Equal(t, "", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightReset])
- require.Equal(t, "0.2", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom])
- require.Equal(t, "1.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost])
- require.Equal(t, "8", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse])
- require.Equal(t, "4", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky])
- }
- func TestSettingService_UpdateSettingsRejectsInvalidOpenAIOAuthSchedulingRateMultiplier(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- for _, rate := range []float64{-0.01, math.NaN(), math.Inf(1)} {
- err := svc.UpdateSettings(context.Background(), &SystemSettings{OpenAIOAuthSchedulingRateMultiplier: rate})
- require.Error(t, err)
- }
- }
- func TestSettingService_UpdateSettings_OpenAIAdvancedSchedulerWeightSums(t *testing.T) {
- maxFloat := strconv.FormatFloat(math.MaxFloat64, 'g', -1, 64)
- tests := []struct {
- name string
- weights SystemSettings
- wantErr bool
- }{
- {
- name: "reset only base is valid",
- weights: SystemSettings{
- OpenAIAdvancedSchedulerWeightPriority: "0",
- OpenAIAdvancedSchedulerWeightLoad: "0",
- OpenAIAdvancedSchedulerWeightQueue: "0",
- OpenAIAdvancedSchedulerWeightErrorRate: "0",
- OpenAIAdvancedSchedulerWeightTTFT: "0",
- OpenAIAdvancedSchedulerWeightReset: "1",
- OpenAIAdvancedSchedulerWeightQuotaHeadroom: "0",
- OpenAIAdvancedSchedulerWeightUpstreamCost: "0",
- OpenAIAdvancedSchedulerWeightPreviousResponse: "0",
- OpenAIAdvancedSchedulerWeightSessionSticky: "0",
- },
- },
- {
- name: "base sum overflow is rejected",
- weights: SystemSettings{
- OpenAIAdvancedSchedulerWeightPriority: maxFloat,
- OpenAIAdvancedSchedulerWeightLoad: maxFloat,
- },
- wantErr: true,
- },
- {
- name: "sticky total sum overflow is rejected",
- weights: SystemSettings{
- OpenAIAdvancedSchedulerWeightPriority: maxFloat,
- OpenAIAdvancedSchedulerWeightPreviousResponse: maxFloat,
- },
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &tt.weights)
- if tt.wantErr {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- })
- }
- }
- func TestSettingService_ParseSettingsDefaultsOpenAIOAuthSchedulingRateMultiplier(t *testing.T) {
- svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
- require.Equal(t, 1.0, svc.parseSettings(map[string]string{}).OpenAIOAuthSchedulingRateMultiplier)
- require.Equal(t, 0.05, svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05"}).OpenAIOAuthSchedulingRateMultiplier)
- }
- func TestSettingService_GetAllSettings_OpenAIAdvancedSchedulerEffectiveValuesUseConfig(t *testing.T) {
- cfg := &config.Config{}
- cfg.Gateway.OpenAIWS.LBTopK = 13
- cfg.Gateway.OpenAIWS.SchedulerScoreWeights = config.GatewayOpenAIWSSchedulerScoreWeights{
- Priority: 2,
- Load: 3,
- Queue: 4,
- ErrorRate: 5,
- TTFT: 6,
- Reset: 7,
- QuotaHeadroom: 8,
- UpstreamCost: 9,
- PreviousResponse: 10,
- SessionSticky: 11,
- }
- svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
- SettingKeyOpenAIAdvancedSchedulerLBTopK: "3",
- SettingKeyOpenAIAdvancedSchedulerWeightPriority: "99",
- SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "88",
- }}, cfg)
- settings, err := svc.GetAllSettings(context.Background())
- require.NoError(t, err)
- require.Equal(t, "3", settings.OpenAIAdvancedSchedulerLBTopK)
- require.Equal(t, "99", settings.OpenAIAdvancedSchedulerWeightPriority)
- require.Equal(t, "88", settings.OpenAIAdvancedSchedulerWeightSessionSticky)
- require.Equal(t, "13", settings.OpenAIAdvancedSchedulerEffectiveLBTopK)
- require.Equal(t, "2", settings.OpenAIAdvancedSchedulerEffectiveWeightPriority)
- require.Equal(t, "3", settings.OpenAIAdvancedSchedulerEffectiveWeightLoad)
- require.Equal(t, "9", settings.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost)
- require.Equal(t, "11", settings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky)
- }
- func TestSettingService_UpdateSettings_AntigravityUserAgentVersion(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- AntigravityUserAgentVersion: "1.23.2",
- })
- require.NoError(t, err)
- require.Equal(t, "1.23.2", repo.updates[SettingKeyAntigravityUserAgentVersion])
- }
- func TestSettingService_InitializeDefaultSettingsPersistsConfiguredForwardedClientIPHeaders(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{values: map[string]string{}}
- cfg := &config.Config{}
- cfg.SetForwardedClientIPSettings(true, []string{"X-Cdn-Ip", "True-Client-Ip"})
- svc := NewSettingService(repo, cfg)
- require.NoError(t, svc.InitializeDefaultSettings(context.Background()))
- require.JSONEq(t, `["X-Cdn-Ip","True-Client-Ip"]`, repo.values[SettingKeyForwardedClientIPHeaders])
- }
- func TestSettingService_UpdateSettings_APIKeyACLTrustForwardedIPRefreshesConfig(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- cfg := &config.Config{}
- svc := NewSettingService(repo, cfg)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- APIKeyACLTrustForwardedIP: true,
- ForwardedClientIPHeaders: []string{" x-cdn-ip ", "X-CDN-IP", "true-client-ip"},
- })
- require.NoError(t, err)
- require.Equal(t, "true", repo.updates[SettingKeyAPIKeyACLTrustForwardedIP])
- require.JSONEq(t, `["X-Cdn-Ip","True-Client-Ip"]`, repo.updates[SettingKeyForwardedClientIPHeaders])
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.True(t, runtimeSettings.TrustForwardedIP)
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, runtimeSettings.Headers)
- runtimeSettings.Headers[0] = "X-Mutated"
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, cfg.ForwardedClientIPSettings().Headers)
- }
- func TestSettingService_UpdateSettings_RejectsInvalidForwardedClientIPHeadersWithoutRefreshing(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- cfg := &config.Config{}
- cfg.SetForwardedClientIPSettings(true, []string{"X-Existing-IP"})
- svc := NewSettingService(repo, cfg)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- ForwardedClientIPHeaders: []string{"X Invalid"},
- })
- require.Error(t, err)
- require.Nil(t, repo.updates)
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.True(t, runtimeSettings.TrustForwardedIP)
- require.Equal(t, []string{"X-Existing-IP"}, runtimeSettings.Headers)
- }
- func TestSettingService_UpdateSettings_WriteFailureDoesNotRefreshForwardedIPRuntime(t *testing.T) {
- repo := &settingUpdateRepoStub{setMultipleErr: errors.New("database unavailable")}
- cfg := &config.Config{}
- cfg.SetForwardedClientIPSettings(false, []string{"X-Existing-IP"})
- svc := NewSettingService(repo, cfg)
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- APIKeyACLTrustForwardedIP: true,
- ForwardedClientIPHeaders: []string{"X-New-IP"},
- })
- require.ErrorContains(t, err, "database unavailable")
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.False(t, runtimeSettings.TrustForwardedIP)
- require.Equal(t, []string{"X-Existing-IP"}, runtimeSettings.Headers)
- }
- func TestSettingService_ParseSettings_APIKeyACLTrustForwardedIPFallsBackToConfigWhenMissing(t *testing.T) {
- cfg := &config.Config{}
- cfg.Security.TrustForwardedIPForAPIKeyACL = true
- svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
- got := svc.parseSettings(map[string]string{})
- require.True(t, got.APIKeyACLTrustForwardedIP)
- }
- func TestSettingService_ParseSettings_APIKeyACLTrustForwardedIPUsesStoredValue(t *testing.T) {
- cfg := &config.Config{}
- cfg.SetTrustForwardedIPForAPIKeyACL(true)
- svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
- got := svc.parseSettings(map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"})
- require.False(t, got.APIKeyACLTrustForwardedIP)
- }
- func TestSettingService_ParseSettings_ForwardedClientIPHeaders(t *testing.T) {
- cfg := &config.Config{}
- cfg.SetForwardedClientIPSettings(true, []string{"X-Config-IP"})
- svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
- t.Run("stored value is normalized", func(t *testing.T) {
- got := svc.parseSettings(map[string]string{
- SettingKeyForwardedClientIPHeaders: `[" x-cdn-ip ","X-CDN-IP","true-client-ip"]`,
- })
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, got.ForwardedClientIPHeaders)
- })
- t.Run("missing value falls back to config", func(t *testing.T) {
- got := svc.parseSettings(map[string]string{})
- require.Equal(t, []string{"X-Config-IP"}, got.ForwardedClientIPHeaders)
- })
- t.Run("malformed value disables forwarded trust", func(t *testing.T) {
- got := svc.parseSettings(map[string]string{
- SettingKeyAPIKeyACLTrustForwardedIP: "true",
- SettingKeyForwardedClientIPHeaders: `{"not":"an array"}`,
- })
- require.False(t, got.APIKeyACLTrustForwardedIP)
- require.Empty(t, got.ForwardedClientIPHeaders)
- })
- }
- func TestSettingService_LoadForwardedClientIPSettingsMigration(t *testing.T) {
- tests := []struct {
- name string
- values map[string]string
- trustedProxiesSet bool
- configDefault bool
- wantEnabled bool
- wantForwardedIPUpdate string
- wantMigrationMarkerSet bool
- }{
- {
- name: "missing setting follows configured default",
- values: map[string]string{},
- configDefault: true,
- wantEnabled: true,
- wantMigrationMarkerSet: true,
- },
- {
- name: "legacy false without proxy config migrates to compatibility",
- values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
- wantEnabled: true,
- wantForwardedIPUpdate: "true",
- wantMigrationMarkerSet: true,
- },
- {
- name: "legacy false with explicit proxy config stays secure",
- values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
- trustedProxiesSet: true,
- wantEnabled: false,
- wantMigrationMarkerSet: true,
- },
- {
- name: "completed migration preserves later false choice",
- values: map[string]string{
- SettingKeyAPIKeyACLTrustForwardedIP: "false",
- settingKeyForwardedClientIPModeV2: "true",
- },
- wantEnabled: false,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{values: test.values}
- cfg := &config.Config{Server: config.ServerConfig{TrustedProxiesConfigured: test.trustedProxiesSet}}
- cfg.Security.TrustForwardedIPForAPIKeyACL = test.configDefault
- svc := NewSettingService(repo, cfg)
- require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
- require.Equal(t, test.wantEnabled, cfg.TrustForwardedIPForAPIKeyACL())
- require.Equal(t, test.wantForwardedIPUpdate, repo.updates[SettingKeyAPIKeyACLTrustForwardedIP])
- require.JSONEq(t, `[]`, repo.updates[SettingKeyForwardedClientIPHeaders])
- if test.wantMigrationMarkerSet {
- require.Equal(t, "true", repo.updates[settingKeyForwardedClientIPModeV2])
- } else {
- require.NotContains(t, repo.updates, settingKeyForwardedClientIPModeV2)
- }
- })
- }
- }
- func TestSettingService_LoadForwardedClientIPSettingsLoadsHeaders(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{values: map[string]string{
- SettingKeyAPIKeyACLTrustForwardedIP: "true",
- SettingKeyForwardedClientIPHeaders: `[" x-cdn-ip ","true-client-ip"]`,
- settingKeyForwardedClientIPModeV2: "true",
- }}
- cfg := &config.Config{}
- svc := NewSettingService(repo, cfg)
- require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.True(t, runtimeSettings.TrustForwardedIP)
- require.Equal(t, []string{"X-Cdn-Ip", "True-Client-Ip"}, runtimeSettings.Headers)
- require.Nil(t, repo.updates)
- }
- func TestSettingService_LoadForwardedClientIPSettingsMalformedHeadersDisablesCustomTrust(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{values: map[string]string{
- SettingKeyAPIKeyACLTrustForwardedIP: "true",
- SettingKeyForwardedClientIPHeaders: `["X Invalid"]`,
- }}
- cfg := &config.Config{}
- svc := NewSettingService(repo, cfg)
- err := svc.LoadForwardedClientIPSettings(context.Background())
- require.ErrorContains(t, err, "load forwarded client ip headers")
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.False(t, runtimeSettings.TrustForwardedIP)
- require.Empty(t, runtimeSettings.Headers)
- require.Equal(t, "true", repo.updates[settingKeyForwardedClientIPModeV2])
- require.NotContains(t, repo.updates, SettingKeyAPIKeyACLTrustForwardedIP)
- }
- func TestSettingService_LoadForwardedClientIPSettingsBackfillsConfigHeaders(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{values: map[string]string{
- settingKeyForwardedClientIPModeV2: "true",
- }}
- cfg := &config.Config{}
- cfg.SetForwardedClientIPSettings(false, []string{"X-Config-IP"})
- svc := NewSettingService(repo, cfg)
- require.NoError(t, svc.LoadForwardedClientIPSettings(context.Background()))
- require.JSONEq(t, `["X-Config-IP"]`, repo.updates[SettingKeyForwardedClientIPHeaders])
- require.Equal(t, []string{"X-Config-IP"}, cfg.ForwardedClientIPSettings().Headers)
- }
- func TestSettingService_LoadForwardedClientIPSettingsReadFailureFailsClosed(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{
- getMultipleErr: errors.New("database unavailable"),
- }
- cfg := &config.Config{}
- cfg.SetTrustForwardedIPForAPIKeyACL(true)
- svc := NewSettingService(repo, cfg)
- err := svc.LoadForwardedClientIPSettings(context.Background())
- require.ErrorContains(t, err, "get forwarded client ip settings")
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.False(t, runtimeSettings.TrustForwardedIP)
- require.Empty(t, runtimeSettings.Headers)
- }
- func TestSettingService_LoadForwardedClientIPSettingsWriteFailureUsesComputedMode(t *testing.T) {
- tests := []struct {
- name string
- trustedProxiesSet bool
- wantEnabled bool
- }{
- {name: "compatibility migration remains effective", wantEnabled: true},
- {name: "explicit proxy policy remains secure", trustedProxiesSet: true, wantEnabled: false},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- repo := &forwardedIPMigrationRepoStub{
- values: map[string]string{SettingKeyAPIKeyACLTrustForwardedIP: "false"},
- setMultipleErr: errors.New("database unavailable"),
- }
- cfg := &config.Config{Server: config.ServerConfig{TrustedProxiesConfigured: test.trustedProxiesSet}}
- svc := NewSettingService(repo, cfg)
- err := svc.LoadForwardedClientIPSettings(context.Background())
- require.ErrorContains(t, err, "migrate forwarded client ip setting")
- require.Equal(t, test.wantEnabled, cfg.TrustForwardedIPForAPIKeyACL())
- })
- }
- }
- func TestSettingService_GetAntigravityUserAgentVersion_Precedence(t *testing.T) {
- t.Run("后台设置优先", func(t *testing.T) {
- svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
- SettingKeyAntigravityUserAgentVersion: "1.24.0",
- }}, &config.Config{})
- require.Equal(t, "1.24.0", svc.GetAntigravityUserAgentVersion(context.Background()))
- })
- t.Run("空值回退配置默认值", func(t *testing.T) {
- svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
- SettingKeyAntigravityUserAgentVersion: "",
- }}, &config.Config{})
- require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
- })
- t.Run("缺失回退配置默认值", func(t *testing.T) {
- svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{}}, &config.Config{})
- require.Equal(t, antigravity.GetDefaultUserAgentVersion(), svc.GetAntigravityUserAgentVersion(context.Background()))
- })
- }
- func TestSettingService_UpdateSettings_RejectsInvalidPaymentVisibleMethodSource(t *testing.T) {
- repo := &settingUpdateRepoStub{}
- svc := NewSettingService(repo, &config.Config{})
- err := svc.UpdateSettings(context.Background(), &SystemSettings{
- PaymentVisibleMethodAlipaySource: "not-a-provider",
- })
- require.Error(t, err)
- require.Equal(t, "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE", infraerrors.Reason(err))
- require.Nil(t, repo.updates)
- }
- func TestSettingService_PasskeySwitchPersistsAndDefaultsToConfigured(t *testing.T) {
- cfg := &config.Config{WebAuthn: config.WebAuthnConfig{
- Enabled: true,
- RPID: "sub3.nebula-spaces.com",
- RPOrigins: []string{"https://sub3.nebula-spaces.com"},
- }}
- runtimeRepo := &forwardedIPMigrationRepoStub{values: map[string]string{}}
- runtimeService := NewSettingService(runtimeRepo, cfg)
- enabled, err := runtimeService.PasskeyEnabled(context.Background())
- require.NoError(t, err)
- require.True(t, enabled)
- updateRepo := &settingUpdateRepoStub{}
- updateService := NewSettingService(updateRepo, cfg)
- require.NoError(t, updateService.UpdateSettings(context.Background(), &SystemSettings{
- PasskeyEnabled: false,
- }))
- require.Equal(t, "false", updateRepo.updates[SettingKeyPasskeyEnabled])
- runtimeRepo.values[SettingKeyPasskeyEnabled] = "false"
- enabled, err = runtimeService.PasskeyEnabled(context.Background())
- require.NoError(t, err)
- require.False(t, enabled)
- publicSettings, err := runtimeService.GetPublicSettings(context.Background())
- require.NoError(t, err)
- require.False(t, publicSettings.PasskeyEnabled)
- }
- // 移除 WebAuthn 配置后,残留的 passkey_enabled="true" 不得再让 GetAllSettings
- // 报告开关开启:admin 更新门控以此为准,一旦误报为 true 会拒绝所有设置保存,
- // 而此时前端开关处于禁用态,管理员无法在 UI 里自救。
- func TestSettingService_StalePasskeyTrueWithoutConfigReportsDisabled(t *testing.T) {
- repo := &settingGetAllRepoStub{values: map[string]string{
- SettingKeyPasskeyEnabled: "true",
- }}
- service := NewSettingService(repo, &config.Config{})
- settings, err := service.GetAllSettings(context.Background())
- require.NoError(t, err)
- require.False(t, settings.PasskeyEnabled)
- }
|