fix(upgrade): preserve legacy auth and payment compatibility
This commit is contained in:
@@ -304,8 +304,8 @@ type UpdateSettingsRequest struct {
|
||||
OIDCConnectRedirectURL string `json:"oidc_connect_redirect_url"`
|
||||
OIDCConnectFrontendRedirectURL string `json:"oidc_connect_frontend_redirect_url"`
|
||||
OIDCConnectTokenAuthMethod string `json:"oidc_connect_token_auth_method"`
|
||||
OIDCConnectUsePKCE bool `json:"oidc_connect_use_pkce"`
|
||||
OIDCConnectValidateIDToken bool `json:"oidc_connect_validate_id_token"`
|
||||
OIDCConnectUsePKCE *bool `json:"oidc_connect_use_pkce"`
|
||||
OIDCConnectValidateIDToken *bool `json:"oidc_connect_validate_id_token"`
|
||||
OIDCConnectAllowedSigningAlgs string `json:"oidc_connect_allowed_signing_algs"`
|
||||
OIDCConnectClockSkewSeconds int `json:"oidc_connect_clock_skew_seconds"`
|
||||
OIDCConnectRequireEmailVerified bool `json:"oidc_connect_require_email_verified"`
|
||||
@@ -682,6 +682,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generic OIDC 参数验证
|
||||
oidcUsePKCE := previousSettings.OIDCConnectUsePKCE
|
||||
oidcValidateIDToken := previousSettings.OIDCConnectValidateIDToken
|
||||
if req.OIDCConnectEnabled {
|
||||
req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
|
||||
req.OIDCConnectClientID = strings.TrimSpace(req.OIDCConnectClientID)
|
||||
@@ -716,11 +718,11 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoEmailPath, previousSettings.OIDCConnectUserInfoEmailPath))
|
||||
req.OIDCConnectUserInfoIDPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoIDPath, previousSettings.OIDCConnectUserInfoIDPath))
|
||||
req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoUsernamePath, previousSettings.OIDCConnectUserInfoUsernamePath))
|
||||
if !req.OIDCConnectUsePKCE {
|
||||
req.OIDCConnectUsePKCE = previousSettings.OIDCConnectUsePKCE
|
||||
if req.OIDCConnectUsePKCE != nil {
|
||||
oidcUsePKCE = *req.OIDCConnectUsePKCE
|
||||
}
|
||||
if !req.OIDCConnectValidateIDToken {
|
||||
req.OIDCConnectValidateIDToken = previousSettings.OIDCConnectValidateIDToken
|
||||
if req.OIDCConnectValidateIDToken != nil {
|
||||
oidcValidateIDToken = *req.OIDCConnectValidateIDToken
|
||||
}
|
||||
if req.OIDCConnectClockSkewSeconds == 0 {
|
||||
req.OIDCConnectClockSkewSeconds = previousSettings.OIDCConnectClockSkewSeconds
|
||||
@@ -795,7 +797,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
response.BadRequest(c, "OIDC clock skew seconds must be between 0 and 600")
|
||||
return
|
||||
}
|
||||
if req.OIDCConnectValidateIDToken && req.OIDCConnectAllowedSigningAlgs == "" {
|
||||
if oidcValidateIDToken && req.OIDCConnectAllowedSigningAlgs == "" {
|
||||
response.BadRequest(c, "OIDC Allowed Signing Algs is required when validate_id_token=true")
|
||||
return
|
||||
}
|
||||
@@ -1076,8 +1078,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
OIDCConnectRedirectURL: req.OIDCConnectRedirectURL,
|
||||
OIDCConnectFrontendRedirectURL: req.OIDCConnectFrontendRedirectURL,
|
||||
OIDCConnectTokenAuthMethod: req.OIDCConnectTokenAuthMethod,
|
||||
OIDCConnectUsePKCE: req.OIDCConnectUsePKCE,
|
||||
OIDCConnectValidateIDToken: req.OIDCConnectValidateIDToken,
|
||||
OIDCConnectUsePKCE: oidcUsePKCE,
|
||||
OIDCConnectValidateIDToken: oidcValidateIDToken,
|
||||
OIDCConnectAllowedSigningAlgs: req.OIDCConnectAllowedSigningAlgs,
|
||||
OIDCConnectClockSkewSeconds: req.OIDCConnectClockSkewSeconds,
|
||||
OIDCConnectRequireEmailVerified: req.OIDCConnectRequireEmailVerified,
|
||||
|
||||
@@ -247,6 +247,94 @@ func TestSettingHandler_UpdateSettings_PersistsPaymentVisibleMethodsAndAdvancedS
|
||||
require.Equal(t, true, data["openai_advanced_scheduler_enabled"])
|
||||
}
|
||||
|
||||
func TestSettingHandler_UpdateSettings_PreservesLegacyBlankPaymentVisibleMethodSource(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
repo := &settingHandlerRepoStub{
|
||||
values: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "true",
|
||||
service.SettingPaymentVisibleMethodAlipayEnabled: "true",
|
||||
service.SettingPaymentVisibleMethodAlipaySource: "",
|
||||
service.SettingPaymentVisibleMethodWxpayEnabled: "false",
|
||||
service.SettingPaymentVisibleMethodWxpaySource: "",
|
||||
},
|
||||
}
|
||||
svc := service.NewSettingService(repo, &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}})
|
||||
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil)
|
||||
|
||||
body := map[string]any{
|
||||
"promo_code_enabled": false,
|
||||
}
|
||||
rawBody, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "", repo.values[service.SettingPaymentVisibleMethodAlipaySource])
|
||||
require.Equal(t, "true", repo.values[service.SettingPaymentVisibleMethodAlipayEnabled])
|
||||
}
|
||||
|
||||
func TestSettingHandler_UpdateSettings_PersistsExplicitFalseOIDCCompatibilityFlags(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
repo := &settingHandlerRepoStub{
|
||||
values: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "true",
|
||||
service.SettingKeyOIDCConnectEnabled: "true",
|
||||
service.SettingKeyOIDCConnectProviderName: "OIDC",
|
||||
service.SettingKeyOIDCConnectClientID: "oidc-client",
|
||||
service.SettingKeyOIDCConnectClientSecret: "oidc-secret",
|
||||
service.SettingKeyOIDCConnectIssuerURL: "https://issuer.example.com",
|
||||
service.SettingKeyOIDCConnectAuthorizeURL: "https://issuer.example.com/auth",
|
||||
service.SettingKeyOIDCConnectTokenURL: "https://issuer.example.com/token",
|
||||
service.SettingKeyOIDCConnectUserInfoURL: "https://issuer.example.com/userinfo",
|
||||
service.SettingKeyOIDCConnectJWKSURL: "https://issuer.example.com/jwks",
|
||||
service.SettingKeyOIDCConnectScopes: "openid email profile",
|
||||
service.SettingKeyOIDCConnectRedirectURL: "https://example.com/api/v1/auth/oauth/oidc/callback",
|
||||
service.SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
|
||||
service.SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
|
||||
service.SettingKeyOIDCConnectUsePKCE: "true",
|
||||
service.SettingKeyOIDCConnectValidateIDToken: "true",
|
||||
service.SettingKeyOIDCConnectAllowedSigningAlgs: "RS256",
|
||||
service.SettingKeyOIDCConnectClockSkewSeconds: "120",
|
||||
},
|
||||
}
|
||||
svc := service.NewSettingService(repo, &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}})
|
||||
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil)
|
||||
|
||||
body := map[string]any{
|
||||
"promo_code_enabled": true,
|
||||
"oidc_connect_enabled": true,
|
||||
"oidc_connect_use_pkce": false,
|
||||
"oidc_connect_validate_id_token": false,
|
||||
"oidc_connect_allowed_signing_algs": "",
|
||||
}
|
||||
rawBody, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.UpdateSettings(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "false", repo.values[service.SettingKeyOIDCConnectUsePKCE])
|
||||
require.Equal(t, "false", repo.values[service.SettingKeyOIDCConnectValidateIDToken])
|
||||
|
||||
var resp response.Response
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||
data, ok := resp.Data.(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, false, data["oidc_connect_use_pkce"])
|
||||
require.Equal(t, false, data["oidc_connect_validate_id_token"])
|
||||
}
|
||||
|
||||
func TestSettingHandler_UpdateSettings_RejectsInvalidPaymentVisibleMethodSource(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
repo := &settingHandlerRepoStub{
|
||||
|
||||
@@ -62,10 +62,13 @@ type migrationChecksumCompatibilityRule struct {
|
||||
// 规则必须同时匹配「迁移名 + 数据库 checksum + 当前文件 checksum」且两者都落在该迁移的已知版本集合内才会放行,
|
||||
// 避免放宽全局校验,也允许将误改的历史 migration 回滚为已发布版本而不要求人工修 checksum。
|
||||
var migrationChecksumCompatibilityRules = map[string]migrationChecksumCompatibilityRule{
|
||||
"054_drop_legacy_cache_columns.sql": newMigrationChecksumCompatibilityRule("82de761156e03876653e7a6a4eee883cd927847036f779b0b9f34c42a8af7a7d", "182c193f3359946cf094090cd9e57d5c3fd9abaffbc1e8fc378646b8a6fa12b4"),
|
||||
"061_add_usage_log_request_type.sql": newMigrationChecksumCompatibilityRule("66207e7aa5dd0429c2e2c0fabdaf79783ff157fa0af2e81adff2ee03790ec65c", "08a248652cbab7cfde147fc6ef8cda464f2477674e20b718312faa252e0481c0", "222b4a09c797c22e5922b6b172327c824f5463aaa8760e4f621bc5c22e2be0f3"),
|
||||
"109_auth_identity_compat_backfill.sql": newMigrationChecksumCompatibilityRule("2b380305e73ff0c13aa8c811e45897f2b36ca4a438f7b3e8f98e19ecb6bae0b3", "551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee"),
|
||||
"119_enforce_payment_orders_out_trade_no_unique.sql": newMigrationChecksumCompatibilityRule("0bbe809ae48a9d811dabda1ba1c74955bd71c4a9cc610f9128816818dfa6c11e", "ebd2c67cce0116393fb4f1b5d5116a67c6aceb73820dfb5133d1ff6f36d72d34"),
|
||||
"054_drop_legacy_cache_columns.sql": newMigrationChecksumCompatibilityRule("82de761156e03876653e7a6a4eee883cd927847036f779b0b9f34c42a8af7a7d", "182c193f3359946cf094090cd9e57d5c3fd9abaffbc1e8fc378646b8a6fa12b4"),
|
||||
"061_add_usage_log_request_type.sql": newMigrationChecksumCompatibilityRule("66207e7aa5dd0429c2e2c0fabdaf79783ff157fa0af2e81adff2ee03790ec65c", "08a248652cbab7cfde147fc6ef8cda464f2477674e20b718312faa252e0481c0", "222b4a09c797c22e5922b6b172327c824f5463aaa8760e4f621bc5c22e2be0f3"),
|
||||
"109_auth_identity_compat_backfill.sql": newMigrationChecksumCompatibilityRule("2b380305e73ff0c13aa8c811e45897f2b36ca4a438f7b3e8f98e19ecb6bae0b3", "551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee"),
|
||||
"118_wechat_dual_mode_and_auth_source_defaults.sql": newMigrationChecksumCompatibilityRule("b54194d7a3e4fbf710e0a3590d22a2fe7966804c487052a356e0b55f53ef96b0", "e0cdf835d6c688d64100f483d31bc02ac9ebad414bf1837af239a84bf75b8227"),
|
||||
"119_enforce_payment_orders_out_trade_no_unique.sql": newMigrationChecksumCompatibilityRule("0bbe809ae48a9d811dabda1ba1c74955bd71c4a9cc610f9128816818dfa6c11e", "ebd2c67cce0116393fb4f1b5d5116a67c6aceb73820dfb5133d1ff6f36d72d34"),
|
||||
"120_enforce_payment_orders_out_trade_no_unique_notx.sql": newMigrationChecksumCompatibilityRule("707431450603e70a43ce9fbd61e0c12fa67da4875158ccefabacea069587ab22", "04b082b5a239c525154fe9185d324ee2b05ff90da9297e10dba19f9be79aa59a"),
|
||||
"123_fix_legacy_auth_source_grant_on_signup_defaults.sql": newMigrationChecksumCompatibilityRule("2ce43c2cd89e9f9e1febd34a407ed9e84d177386c5544b6f02c1f58a21129f57", "6cd33422f215dcd1f486ab6f35c0ea5805d9ca69bb25906d94bc649156657145"),
|
||||
}
|
||||
|
||||
// ApplyMigrations 将嵌入的 SQL 迁移文件应用到指定的数据库。
|
||||
|
||||
@@ -94,6 +94,19 @@ func TestIsMigrationChecksumCompatible_AdditionalCases(t *testing.T) {
|
||||
require.True(t, isMigrationChecksumCompatible(name, accepted, rule.fileChecksum))
|
||||
}
|
||||
|
||||
func TestMigrationChecksumCompatibilityRules_CoverEditedUpgradeCompatibilityMigrations(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"118_wechat_dual_mode_and_auth_source_defaults.sql",
|
||||
"120_enforce_payment_orders_out_trade_no_unique_notx.sql",
|
||||
"123_fix_legacy_auth_source_grant_on_signup_defaults.sql",
|
||||
} {
|
||||
rule, ok := migrationChecksumCompatibilityRules[name]
|
||||
require.Truef(t, ok, "missing compatibility rule for %s", name)
|
||||
require.NotEmpty(t, rule.fileChecksum)
|
||||
require.NotEmpty(t, rule.acceptedDBChecksum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAtlasBaselineAligned(t *testing.T) {
|
||||
t.Run("skip_when_no_legacy_table", func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
|
||||
@@ -45,10 +45,18 @@ func (s *PaymentConfigService) pcApplyEnabledVisibleMethodInstances(ctx context.
|
||||
for _, method := range []string{payment.TypeAlipay, payment.TypeWxpay} {
|
||||
matching := filterEnabledVisibleMethodInstances(instances, method)
|
||||
providerKey, err := s.resolveVisibleMethodProviderKey(ctx, method, matching)
|
||||
if err != nil || providerKey == "" {
|
||||
if err != nil {
|
||||
delete(filtered, method)
|
||||
continue
|
||||
}
|
||||
if providerKey == "" {
|
||||
if len(matching) == 0 {
|
||||
delete(filtered, method)
|
||||
continue
|
||||
}
|
||||
filtered[method] = matching
|
||||
continue
|
||||
}
|
||||
selectedInstances := filterVisibleMethodInstancesByProviderKey(instances, method, providerKey)
|
||||
if len(selectedInstances) == 0 {
|
||||
delete(filtered, method)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnionFloat(t *testing.T) {
|
||||
@@ -402,3 +403,59 @@ func TestGetAvailableMethodLimitsUsesConfiguredVisibleMethodSource(t *testing.T)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvailableMethodLimitsPreservesLegacyCrossProviderBehaviorWhenVisibleMethodSourceMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newPaymentConfigServiceTestClient(t)
|
||||
|
||||
_, err := client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeAlipay).
|
||||
SetName("Official Alipay").
|
||||
SetConfig("{}").
|
||||
SetSupportedTypes("alipay").
|
||||
SetLimits(`{"alipay":{"singleMin":10,"singleMax":100}}`).
|
||||
SetEnabled(true).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeEasyPay).
|
||||
SetName("EasyPay Mixed").
|
||||
SetConfig("{}").
|
||||
SetSupportedTypes("alipay,wxpay").
|
||||
SetLimits(`{"alipay":{"singleMin":20,"singleMax":200},"wxpay":{"singleMin":40,"singleMax":400}}`).
|
||||
SetEnabled(true).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeWxpay).
|
||||
SetName("Official WeChat").
|
||||
SetConfig("{}").
|
||||
SetSupportedTypes("wxpay").
|
||||
SetLimits(`{"wxpay":{"singleMin":30,"singleMax":300}}`).
|
||||
SetEnabled(true).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
svc := &PaymentConfigService{
|
||||
entClient: client,
|
||||
settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{}},
|
||||
}
|
||||
|
||||
resp, err := svc.GetAvailableMethodLimits(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
alipayLimits, ok := resp.Methods[payment.TypeAlipay]
|
||||
require.True(t, ok, "expected alipay limits to remain visible")
|
||||
require.Equal(t, 10.0, alipayLimits.SingleMin)
|
||||
require.Equal(t, 200.0, alipayLimits.SingleMax)
|
||||
|
||||
wxpayLimits, ok := resp.Methods[payment.TypeWxpay]
|
||||
require.True(t, ok, "expected wxpay limits to remain visible")
|
||||
require.Equal(t, 30.0, wxpayLimits.SingleMin)
|
||||
require.Equal(t, 400.0, wxpayLimits.SingleMax)
|
||||
|
||||
require.Equal(t, 10.0, resp.GlobalMin)
|
||||
require.Equal(t, 400.0, resp.GlobalMax)
|
||||
}
|
||||
|
||||
@@ -586,7 +586,60 @@ func TestVisibleMethodLoadBalancerUsesConfiguredSourceWhenMultipleProvidersEnabl
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibleMethodLoadBalancerRejectsMissingOrInvalidSourceWhenMultipleProvidersEnabled(t *testing.T) {
|
||||
func TestVisibleMethodLoadBalancerPreservesLegacyCrossProviderRoutingWhenSourceMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
client := newPaymentConfigServiceTestClient(t)
|
||||
|
||||
_, err := client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeAlipay).
|
||||
SetName("Official Alipay").
|
||||
SetConfig("{}").
|
||||
SetSupportedTypes("alipay").
|
||||
SetEnabled(true).
|
||||
SetSortOrder(1).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create official provider: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.PaymentProviderInstance.Create().
|
||||
SetProviderKey(payment.TypeEasyPay).
|
||||
SetName("EasyPay Alipay").
|
||||
SetConfig("{}").
|
||||
SetSupportedTypes("alipay").
|
||||
SetEnabled(true).
|
||||
SetSortOrder(2).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("create easypay provider: %v", err)
|
||||
}
|
||||
|
||||
inner := &captureLoadBalancer{}
|
||||
configService := &PaymentConfigService{
|
||||
entClient: client,
|
||||
settingRepo: &paymentConfigSettingRepoStub{
|
||||
values: map[string]string{
|
||||
visibleMethodSourceSettingKey(payment.TypeAlipay): "",
|
||||
},
|
||||
},
|
||||
}
|
||||
lb := newVisibleMethodLoadBalancer(inner, configService)
|
||||
|
||||
_, err = lb.SelectInstance(ctx, "", payment.TypeAlipay, payment.StrategyRoundRobin, 9.9)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectInstance returned error: %v", err)
|
||||
}
|
||||
if inner.lastProviderKey != "" {
|
||||
t.Fatalf("lastProviderKey = %q, want legacy cross-provider empty key", inner.lastProviderKey)
|
||||
}
|
||||
if inner.lastPaymentType != payment.TypeAlipay {
|
||||
t.Fatalf("lastPaymentType = %q, want %q", inner.lastPaymentType, payment.TypeAlipay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibleMethodLoadBalancerRejectsInvalidSourceWhenMultipleProvidersEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
@@ -595,12 +648,6 @@ func TestVisibleMethodLoadBalancerRejectsMissingOrInvalidSourceWhenMultipleProvi
|
||||
sourceValue string
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
name: "missing alipay source",
|
||||
method: payment.TypeAlipay,
|
||||
sourceValue: "",
|
||||
wantMessage: "alipay source is required when the visible method is enabled",
|
||||
},
|
||||
{
|
||||
name: "invalid wxpay source",
|
||||
method: payment.TypeWxpay,
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -166,15 +167,21 @@ func (s *PaymentConfigService) resolveVisibleMethodSourceProviderKey(ctx context
|
||||
if s != nil && s.settingRepo != nil && sourceKey != "" {
|
||||
value, err := s.settingRepo.GetValue(ctx, sourceKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get %s: %w", sourceKey, err)
|
||||
if !errors.Is(err, ErrSettingNotFound) {
|
||||
return "", fmt.Errorf("get %s: %w", sourceKey, err)
|
||||
}
|
||||
} else {
|
||||
rawSource = value
|
||||
}
|
||||
rawSource = value
|
||||
}
|
||||
|
||||
normalizedSource, err := normalizeVisibleMethodSettingSource(method, rawSource, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if normalizedSource == "" {
|
||||
return "", nil
|
||||
}
|
||||
providerKey, ok := VisibleMethodProviderKeyForSource(method, normalizedSource)
|
||||
if !ok {
|
||||
return "", infraerrors.BadRequest(
|
||||
@@ -200,6 +207,9 @@ func (s *PaymentConfigService) resolveVisibleMethodProviderKey(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if providerKey == "" {
|
||||
return "", nil
|
||||
}
|
||||
selected := selectVisibleMethodInstanceByProviderKey(matching, providerKey)
|
||||
if selected == nil {
|
||||
return "", infraerrors.BadRequest(
|
||||
@@ -237,5 +247,11 @@ func (s *PaymentConfigService) resolveEnabledVisibleMethodInstance(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if providerKey == "" {
|
||||
if len(matching) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &dbent.PaymentProviderInstance{ProviderKey: ""}, nil
|
||||
}
|
||||
return selectVisibleMethodInstanceByProviderKey(matching, providerKey), nil
|
||||
}
|
||||
|
||||
@@ -282,7 +282,19 @@ func mergeWeChatConnectCapabilitySettings(settings map[string]string, base confi
|
||||
mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != ""
|
||||
|
||||
if openConfigured || mpConfigured || mobileConfigured {
|
||||
return parseWeChatConnectCapabilitySettings(settings, enabled, mode)
|
||||
openEnabled := strings.TrimSpace(rawOpen) == "true"
|
||||
mpEnabled := strings.TrimSpace(rawMP) == "true"
|
||||
mobileEnabled := strings.TrimSpace(rawMobile) == "true"
|
||||
_, enabledConfigured := settings[SettingKeyWeChatConnectEnabled]
|
||||
if !enabledConfigured &&
|
||||
enabled &&
|
||||
!openEnabled &&
|
||||
!mpEnabled &&
|
||||
!mobileEnabled &&
|
||||
(base.OpenEnabled || base.MPEnabled || base.MobileEnabled) {
|
||||
return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
|
||||
}
|
||||
return openEnabled, mpEnabled, mobileEnabled
|
||||
}
|
||||
if !enabled {
|
||||
return false, false, false
|
||||
@@ -1921,14 +1933,9 @@ func isFalseSettingValue(value string) bool {
|
||||
}
|
||||
|
||||
func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (string, error) {
|
||||
_ = enabled
|
||||
source = strings.TrimSpace(source)
|
||||
if source == "" {
|
||||
if enabled {
|
||||
return "", infraerrors.BadRequest(
|
||||
"INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
|
||||
fmt.Sprintf("%s source is required when the visible method is enabled", method),
|
||||
)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,36 @@ func TestSettingService_GetWeChatConnectOAuthConfig_FallsBackToConfigWhenDatabas
|
||||
require.Empty(t, got.RedirectURL)
|
||||
}
|
||||
|
||||
func TestSettingService_GetWeChatConnectOAuthConfig_IgnoresSyntheticDisabledCapabilitiesFromMigration118(t *testing.T) {
|
||||
repo := &settingWeChatRepoStub{
|
||||
values: map[string]string{
|
||||
SettingKeyWeChatConnectOpenEnabled: "false",
|
||||
SettingKeyWeChatConnectMPEnabled: "false",
|
||||
},
|
||||
}
|
||||
svc := NewSettingService(repo, &config.Config{
|
||||
WeChat: config.WeChatConnectConfig{
|
||||
Enabled: true,
|
||||
OpenEnabled: true,
|
||||
MPEnabled: true,
|
||||
Mode: "open",
|
||||
OpenAppID: "wx-open-config",
|
||||
OpenAppSecret: "wx-open-secret",
|
||||
MPAppID: "wx-mp-config",
|
||||
MPAppSecret: "wx-mp-secret",
|
||||
FrontendRedirectURL: "/auth/wechat/config-callback",
|
||||
},
|
||||
})
|
||||
|
||||
got, err := svc.GetWeChatConnectOAuthConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, got.Enabled)
|
||||
require.True(t, got.OpenEnabled)
|
||||
require.True(t, got.MPEnabled)
|
||||
require.Equal(t, "wx-open-config", got.AppIDForMode("open"))
|
||||
require.Equal(t, "wx-mp-config", got.AppIDForMode("mp"))
|
||||
}
|
||||
|
||||
func TestSettingService_ParseSettings_FallsBackToConfigForWeChatAdminView(t *testing.T) {
|
||||
svc := NewSettingService(&settingWeChatRepoStub{values: map[string]string{}}, &config.Config{
|
||||
WeChat: config.WeChatConnectConfig{
|
||||
|
||||
@@ -3,6 +3,7 @@ VALUES
|
||||
(
|
||||
'wechat_connect_open_enabled',
|
||||
CASE
|
||||
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
|
||||
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
|
||||
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'false'
|
||||
ELSE 'true'
|
||||
@@ -11,6 +12,7 @@ VALUES
|
||||
(
|
||||
'wechat_connect_mp_enabled',
|
||||
CASE
|
||||
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
|
||||
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
|
||||
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'true'
|
||||
ELSE 'false'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
-- Build the payment order uniqueness guarantee online.
|
||||
-- Create the new partial unique index concurrently first so writes keep flowing,
|
||||
-- then remove the legacy index name once the replacement is ready.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no_unique;
|
||||
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS paymentorder_out_trade_no_unique
|
||||
ON payment_orders (out_trade_no)
|
||||
WHERE out_trade_no <> '';
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
WITH migration_110 AS (
|
||||
SELECT applied_at
|
||||
FROM schema_migrations
|
||||
WHERE filename = '110_pending_auth_and_provider_default_grants.sql'
|
||||
),
|
||||
legacy_provider_defaults AS (
|
||||
SELECT provider_type
|
||||
FROM (
|
||||
VALUES ('email'), ('linuxdo'), ('oidc'), ('wechat')
|
||||
) AS providers(provider_type)
|
||||
CROSS JOIN migration_110
|
||||
JOIN settings balance
|
||||
ON balance.key = 'auth_source_default_' || providers.provider_type || '_balance'
|
||||
JOIN settings concurrency
|
||||
ON concurrency.key = 'auth_source_default_' || providers.provider_type || '_concurrency'
|
||||
JOIN settings subscriptions
|
||||
ON subscriptions.key = 'auth_source_default_' || providers.provider_type || '_subscriptions'
|
||||
JOIN settings grant_on_signup
|
||||
ON grant_on_signup.key = 'auth_source_default_' || providers.provider_type || '_grant_on_signup'
|
||||
JOIN settings grant_on_first_bind
|
||||
ON grant_on_first_bind.key = 'auth_source_default_' || providers.provider_type || '_grant_on_first_bind'
|
||||
WHERE balance.value = '0'
|
||||
AND concurrency.value = '5'
|
||||
AND subscriptions.value = '[]'
|
||||
AND grant_on_signup.value = 'true'
|
||||
AND grant_on_first_bind.value = 'false'
|
||||
AND balance.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
|
||||
AND concurrency.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
|
||||
AND subscriptions.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
|
||||
AND grant_on_signup.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
|
||||
AND grant_on_first_bind.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
|
||||
)
|
||||
UPDATE settings
|
||||
SET
|
||||
value = 'false',
|
||||
updated_at = NOW()
|
||||
FROM legacy_provider_defaults
|
||||
WHERE settings.key = 'auth_source_default_' || legacy_provider_defaults.provider_type || '_grant_on_signup'
|
||||
AND settings.value = 'true';
|
||||
-- Intentionally left as a no-op.
|
||||
-- Legacy installs may have intentionally kept the original signup grant defaults,
|
||||
-- and we cannot distinguish those cases safely from untouched migration 110 rows.
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestMigration118DoesNotForceOverwriteAuthSourceGrantDefaults(t *testing.T)
|
||||
require.NotContains(t, sql, "UPDATE settings")
|
||||
require.NotContains(t, sql, "SET value = 'false'")
|
||||
require.True(t, strings.Contains(sql, "ON CONFLICT (key) DO NOTHING"))
|
||||
require.Contains(t, sql, "THEN ''")
|
||||
}
|
||||
|
||||
func TestAuthIdentityReportTypeWideningRunsBeforeLongReportWritersAndStillReconcilesAt121(t *testing.T) {
|
||||
@@ -63,6 +64,7 @@ func TestMigration119DefersPaymentIndexRolloutToOnlineFollowup(t *testing.T) {
|
||||
|
||||
followupSQL := string(followupContent)
|
||||
require.Contains(t, followupSQL, "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS paymentorder_out_trade_no_unique")
|
||||
require.NotContains(t, followupSQL, "DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no_unique")
|
||||
require.Contains(t, followupSQL, "DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no")
|
||||
require.Contains(t, followupSQL, "WHERE out_trade_no <> ''")
|
||||
|
||||
@@ -92,9 +94,7 @@ func TestMigration123BackfillsLegacyAuthSourceGrantDefaultsSafely(t *testing.T)
|
||||
require.NoError(t, err)
|
||||
|
||||
sql := string(content)
|
||||
require.Contains(t, sql, "110_pending_auth_and_provider_default_grants.sql")
|
||||
require.Contains(t, sql, "schema_migrations")
|
||||
require.Contains(t, sql, "updated_at")
|
||||
require.Contains(t, sql, "'_grant_on_signup'")
|
||||
require.Contains(t, sql, "value = 'false'")
|
||||
require.Contains(t, sql, "Intentionally left as a no-op")
|
||||
require.NotContains(t, sql, "UPDATE settings")
|
||||
require.NotContains(t, sql, "value = 'false'")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user