Files
sub2api/backend/internal/service/setting_service_update_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

906 lines
33 KiB
Go

//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)
}