fix(upgrade): preserve legacy auth and payment compatibility

This commit is contained in:
IanShaw027
2026-04-22 13:18:10 +08:00
parent 29caf85104
commit 06136af805
14 changed files with 311 additions and 76 deletions

View File

@@ -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,

View File

@@ -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{

View File

@@ -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 迁移文件应用到指定的数据库。

View File

@@ -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()

View File

@@ -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)

View File

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

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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'

View File

@@ -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 <> '';

View File

@@ -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.

View File

@@ -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'")
}