fix(settings): restore wechat and payment config persistence
This commit is contained in:
@@ -122,6 +122,13 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
|
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
|
||||||
LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
|
LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
|
||||||
|
WeChatConnectEnabled: settings.WeChatConnectEnabled,
|
||||||
|
WeChatConnectAppID: settings.WeChatConnectAppID,
|
||||||
|
WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured,
|
||||||
|
WeChatConnectMode: settings.WeChatConnectMode,
|
||||||
|
WeChatConnectScopes: settings.WeChatConnectScopes,
|
||||||
|
WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL,
|
||||||
|
WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL,
|
||||||
OIDCConnectEnabled: settings.OIDCConnectEnabled,
|
OIDCConnectEnabled: settings.OIDCConnectEnabled,
|
||||||
OIDCConnectProviderName: settings.OIDCConnectProviderName,
|
OIDCConnectProviderName: settings.OIDCConnectProviderName,
|
||||||
OIDCConnectClientID: settings.OIDCConnectClientID,
|
OIDCConnectClientID: settings.OIDCConnectClientID,
|
||||||
@@ -246,6 +253,15 @@ type UpdateSettingsRequest struct {
|
|||||||
LinuxDoConnectClientSecret string `json:"linuxdo_connect_client_secret"`
|
LinuxDoConnectClientSecret string `json:"linuxdo_connect_client_secret"`
|
||||||
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
||||||
|
|
||||||
|
// WeChat Connect OAuth 登录
|
||||||
|
WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
|
||||||
|
WeChatConnectAppID string `json:"wechat_connect_app_id"`
|
||||||
|
WeChatConnectAppSecret string `json:"wechat_connect_app_secret"`
|
||||||
|
WeChatConnectMode string `json:"wechat_connect_mode"`
|
||||||
|
WeChatConnectScopes string `json:"wechat_connect_scopes"`
|
||||||
|
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
|
||||||
|
WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_redirect_url"`
|
||||||
|
|
||||||
// Generic OIDC OAuth 登录
|
// Generic OIDC OAuth 登录
|
||||||
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
|
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
|
||||||
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
|
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
|
||||||
@@ -509,6 +525,54 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.WeChatConnectEnabled {
|
||||||
|
req.WeChatConnectAppID = strings.TrimSpace(req.WeChatConnectAppID)
|
||||||
|
req.WeChatConnectAppSecret = strings.TrimSpace(req.WeChatConnectAppSecret)
|
||||||
|
req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(req.WeChatConnectMode))
|
||||||
|
req.WeChatConnectScopes = strings.TrimSpace(req.WeChatConnectScopes)
|
||||||
|
req.WeChatConnectRedirectURL = strings.TrimSpace(req.WeChatConnectRedirectURL)
|
||||||
|
req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(req.WeChatConnectFrontendRedirectURL)
|
||||||
|
|
||||||
|
if req.WeChatConnectAppID == "" {
|
||||||
|
response.BadRequest(c, "WeChat App ID is required when enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.WeChatConnectAppSecret == "" {
|
||||||
|
if previousSettings.WeChatConnectAppSecret == "" {
|
||||||
|
response.BadRequest(c, "WeChat App Secret is required when enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.WeChatConnectAppSecret = previousSettings.WeChatConnectAppSecret
|
||||||
|
}
|
||||||
|
if req.WeChatConnectMode == "" {
|
||||||
|
req.WeChatConnectMode = "open"
|
||||||
|
}
|
||||||
|
switch req.WeChatConnectMode {
|
||||||
|
case "open", "mp":
|
||||||
|
default:
|
||||||
|
response.BadRequest(c, "WeChat mode must be open or mp")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.WeChatConnectScopes == "" {
|
||||||
|
req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode)
|
||||||
|
}
|
||||||
|
if req.WeChatConnectRedirectURL == "" {
|
||||||
|
response.BadRequest(c, "WeChat Redirect URL is required when enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := config.ValidateAbsoluteHTTPURL(req.WeChatConnectRedirectURL); err != nil {
|
||||||
|
response.BadRequest(c, "WeChat Redirect URL must be an absolute http(s) URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.WeChatConnectFrontendRedirectURL == "" {
|
||||||
|
req.WeChatConnectFrontendRedirectURL = "/auth/wechat/callback"
|
||||||
|
}
|
||||||
|
if err := config.ValidateFrontendRedirectURL(req.WeChatConnectFrontendRedirectURL); err != nil {
|
||||||
|
response.BadRequest(c, "WeChat Frontend Redirect URL is invalid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generic OIDC 参数验证
|
// Generic OIDC 参数验证
|
||||||
if req.OIDCConnectEnabled {
|
if req.OIDCConnectEnabled {
|
||||||
req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
|
req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
|
||||||
@@ -857,6 +921,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
||||||
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
||||||
|
WeChatConnectEnabled: req.WeChatConnectEnabled,
|
||||||
|
WeChatConnectAppID: req.WeChatConnectAppID,
|
||||||
|
WeChatConnectAppSecret: req.WeChatConnectAppSecret,
|
||||||
|
WeChatConnectMode: req.WeChatConnectMode,
|
||||||
|
WeChatConnectScopes: req.WeChatConnectScopes,
|
||||||
|
WeChatConnectRedirectURL: req.WeChatConnectRedirectURL,
|
||||||
|
WeChatConnectFrontendRedirectURL: req.WeChatConnectFrontendRedirectURL,
|
||||||
OIDCConnectEnabled: req.OIDCConnectEnabled,
|
OIDCConnectEnabled: req.OIDCConnectEnabled,
|
||||||
OIDCConnectProviderName: req.OIDCConnectProviderName,
|
OIDCConnectProviderName: req.OIDCConnectProviderName,
|
||||||
OIDCConnectClientID: req.OIDCConnectClientID,
|
OIDCConnectClientID: req.OIDCConnectClientID,
|
||||||
@@ -1136,6 +1207,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
|
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
|
||||||
LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
|
LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
|
||||||
|
WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
|
||||||
|
WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
|
||||||
|
WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
|
||||||
|
WeChatConnectMode: updatedSettings.WeChatConnectMode,
|
||||||
|
WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
|
||||||
|
WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
|
||||||
|
WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
|
||||||
OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
|
OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
|
||||||
OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
|
OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
|
||||||
OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
|
OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
|
||||||
@@ -1329,6 +1407,27 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if before.LinuxDoConnectRedirectURL != after.LinuxDoConnectRedirectURL {
|
if before.LinuxDoConnectRedirectURL != after.LinuxDoConnectRedirectURL {
|
||||||
changed = append(changed, "linuxdo_connect_redirect_url")
|
changed = append(changed, "linuxdo_connect_redirect_url")
|
||||||
}
|
}
|
||||||
|
if before.WeChatConnectEnabled != after.WeChatConnectEnabled {
|
||||||
|
changed = append(changed, "wechat_connect_enabled")
|
||||||
|
}
|
||||||
|
if before.WeChatConnectAppID != after.WeChatConnectAppID {
|
||||||
|
changed = append(changed, "wechat_connect_app_id")
|
||||||
|
}
|
||||||
|
if req.WeChatConnectAppSecret != "" {
|
||||||
|
changed = append(changed, "wechat_connect_app_secret")
|
||||||
|
}
|
||||||
|
if before.WeChatConnectMode != after.WeChatConnectMode {
|
||||||
|
changed = append(changed, "wechat_connect_mode")
|
||||||
|
}
|
||||||
|
if before.WeChatConnectScopes != after.WeChatConnectScopes {
|
||||||
|
changed = append(changed, "wechat_connect_scopes")
|
||||||
|
}
|
||||||
|
if before.WeChatConnectRedirectURL != after.WeChatConnectRedirectURL {
|
||||||
|
changed = append(changed, "wechat_connect_redirect_url")
|
||||||
|
}
|
||||||
|
if before.WeChatConnectFrontendRedirectURL != after.WeChatConnectFrontendRedirectURL {
|
||||||
|
changed = append(changed, "wechat_connect_frontend_redirect_url")
|
||||||
|
}
|
||||||
if before.OIDCConnectEnabled != after.OIDCConnectEnabled {
|
if before.OIDCConnectEnabled != after.OIDCConnectEnabled {
|
||||||
changed = append(changed, "oidc_connect_enabled")
|
changed = append(changed, "oidc_connect_enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -149,7 +148,7 @@ func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
|
|||||||
// WeChatOAuthCallback exchanges the code with WeChat, resolves openid/unionid,
|
// WeChatOAuthCallback exchanges the code with WeChat, resolves openid/unionid,
|
||||||
// and stores the result in the unified pending-auth flow.
|
// and stores the result in the unified pending-auth flow.
|
||||||
func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
||||||
frontendCallback := wechatOAuthFrontendCallback()
|
frontendCallback := h.wechatOAuthFrontendCallback(c.Request.Context())
|
||||||
|
|
||||||
if providerErr := strings.TrimSpace(c.Query("error")); providerErr != "" {
|
if providerErr := strings.TrimSpace(c.Query("error")); providerErr != "" {
|
||||||
redirectOAuthError(c, frontendCallback, "provider_error", providerErr, c.Query("error_description"))
|
redirectOAuthError(c, frontendCallback, "provider_error", providerErr, c.Query("error_description"))
|
||||||
@@ -859,6 +858,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
|
|||||||
return wechatOAuthConfig{}, err
|
return wechatOAuthConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h == nil || h.settingSvc == nil {
|
||||||
|
return wechatOAuthConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "wechat oauth settings service not ready")
|
||||||
|
}
|
||||||
|
|
||||||
apiBaseURL := ""
|
apiBaseURL := ""
|
||||||
if h != nil && h.settingSvc != nil {
|
if h != nil && h.settingSvc != nil {
|
||||||
settings, err := h.settingSvc.GetAllSettings(ctx)
|
settings, err := h.settingSvc.GetAllSettings(ctx)
|
||||||
@@ -867,27 +870,28 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
effective, err := h.settingSvc.GetWeChatConnectOAuthConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return wechatOAuthConfig{}, err
|
||||||
|
}
|
||||||
|
if effective.Mode != mode {
|
||||||
|
return wechatOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
cfg := wechatOAuthConfig{
|
cfg := wechatOAuthConfig{
|
||||||
mode: mode,
|
mode: mode,
|
||||||
redirectURI: resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback"),
|
appID: strings.TrimSpace(effective.AppID),
|
||||||
frontendCallback: wechatOAuthFrontendCallback(),
|
appSecret: strings.TrimSpace(effective.AppSecret),
|
||||||
|
redirectURI: firstNonEmpty(strings.TrimSpace(effective.RedirectURL), resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback")),
|
||||||
|
frontendCallback: firstNonEmpty(strings.TrimSpace(effective.FrontendRedirectURL), wechatOAuthDefaultFrontendCB),
|
||||||
|
scope: firstNonEmpty(strings.TrimSpace(effective.Scopes), service.DefaultWeChatConnectScopesForMode(mode)),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case "mp":
|
case "mp":
|
||||||
cfg.appID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
|
|
||||||
cfg.appSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
|
|
||||||
cfg.authorizeURL = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
cfg.authorizeURL = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
||||||
cfg.scope = "snsapi_userinfo"
|
|
||||||
default:
|
default:
|
||||||
cfg.appID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
|
|
||||||
cfg.appSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
|
|
||||||
cfg.authorizeURL = "https://open.weixin.qq.com/connect/qrconnect"
|
cfg.authorizeURL = "https://open.weixin.qq.com/connect/qrconnect"
|
||||||
cfg.scope = "snsapi_login"
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.appID == "" || cfg.appSecret == "" {
|
|
||||||
return wechatOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.redirectURI) == "" {
|
if strings.TrimSpace(cfg.redirectURI) == "" {
|
||||||
return wechatOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured")
|
return wechatOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured")
|
||||||
@@ -896,8 +900,14 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func wechatOAuthFrontendCallback() string {
|
func (h *AuthHandler) wechatOAuthFrontendCallback(ctx context.Context) string {
|
||||||
return firstNonEmpty(strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")), wechatOAuthDefaultFrontendCB)
|
if h != nil && h.settingSvc != nil {
|
||||||
|
cfg, err := h.settingSvc.GetWeChatConnectOAuthConfig(ctx)
|
||||||
|
if err == nil && strings.TrimSpace(cfg.FrontendRedirectURL) != "" {
|
||||||
|
return strings.TrimSpace(cfg.FrontendRedirectURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wechatOAuthDefaultFrontendCB
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveWeChatOAuthMode(rawMode string, c *gin.Context) (string, error) {
|
func resolveWeChatOAuthMode(rawMode string, c *gin.Context) (string, error) {
|
||||||
|
|||||||
@@ -33,16 +33,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
|
func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, map[string]string{
|
||||||
|
service.SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
service.SettingKeyWeChatConnectAppID: "wx-open-app",
|
||||||
|
service.SettingKeyWeChatConnectAppSecret: "wx-open-secret",
|
||||||
|
service.SettingKeyWeChatConnectMode: "open",
|
||||||
|
service.SettingKeyWeChatConnectScopes: "snsapi_login",
|
||||||
|
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
})
|
||||||
|
defer client.Close()
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(recorder)
|
c, _ := gin.CreateTestContext(recorder)
|
||||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
|
||||||
c.Request.Host = "api.example.com"
|
c.Request.Host = "api.example.com"
|
||||||
|
|
||||||
handler := &AuthHandler{}
|
|
||||||
handler.WeChatOAuthStart(c)
|
handler.WeChatOAuthStart(c)
|
||||||
|
|
||||||
require.Equal(t, http.StatusFound, recorder.Code)
|
require.Equal(t, http.StatusFound, recorder.Code)
|
||||||
@@ -60,10 +66,6 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
|
func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -124,10 +126,6 @@ func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
|
func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "https://app.example.com/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -151,7 +149,7 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
|
|||||||
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
||||||
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
|
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
|
||||||
|
|
||||||
handler, client := newWeChatOAuthTestHandler(t, false)
|
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "https://app.example.com/auth/wechat/callback"))
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
@@ -177,9 +175,6 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T) {
|
func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wx-mp-secret")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
wechatOAuthAccessTokenURL = originalAccessTokenURL
|
wechatOAuthAccessTokenURL = originalAccessTokenURL
|
||||||
@@ -196,7 +191,7 @@ func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T)
|
|||||||
defer upstream.Close()
|
defer upstream.Close()
|
||||||
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
||||||
|
|
||||||
handler, client := newWeChatOAuthTestHandler(t, false)
|
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("mp", "wx-mp-app", "wx-mp-secret", "/auth/wechat/callback"))
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
handler.cfg.Totp.EncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
handler.cfg.Totp.EncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
@@ -240,7 +235,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
mode string
|
mode string
|
||||||
appIDEnv string
|
|
||||||
appID string
|
appID string
|
||||||
appSecret string
|
appSecret string
|
||||||
openID string
|
openID string
|
||||||
@@ -248,7 +242,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
{
|
{
|
||||||
name: "open",
|
name: "open",
|
||||||
mode: "open",
|
mode: "open",
|
||||||
appIDEnv: "WECHAT_OAUTH_OPEN_APP_ID",
|
|
||||||
appID: "wx-open-app",
|
appID: "wx-open-app",
|
||||||
appSecret: "wx-open-secret",
|
appSecret: "wx-open-secret",
|
||||||
openID: "openid-open-123",
|
openID: "openid-open-123",
|
||||||
@@ -256,7 +249,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
{
|
{
|
||||||
name: "mp",
|
name: "mp",
|
||||||
mode: "mp",
|
mode: "mp",
|
||||||
appIDEnv: "WECHAT_OAUTH_MP_APP_ID",
|
|
||||||
appID: "wx-mp-app",
|
appID: "wx-mp-app",
|
||||||
appSecret: "wx-mp-secret",
|
appSecret: "wx-mp-secret",
|
||||||
openID: "openid-mp-123",
|
openID: "openid-mp-123",
|
||||||
@@ -265,15 +257,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Setenv(tc.appIDEnv, tc.appID)
|
|
||||||
switch tc.mode {
|
|
||||||
case "open":
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", tc.appSecret)
|
|
||||||
case "mp":
|
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", tc.appSecret)
|
|
||||||
}
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -297,7 +280,7 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
|
||||||
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
|
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
|
||||||
|
|
||||||
handler, client := newWeChatOAuthTestHandler(t, false)
|
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings(tc.mode, tc.appID, tc.appSecret, "/auth/wechat/callback"))
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
currentUser, err := client.User.Create().
|
currentUser, err := client.User.Create().
|
||||||
@@ -354,10 +337,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T) {
|
func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -436,10 +415,6 @@ func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
|
func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -529,10 +504,6 @@ func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *testing.T) {
|
func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -611,10 +582,6 @@ func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *tes
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing.T) {
|
func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -737,10 +704,6 @@ func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
|
func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -900,10 +863,6 @@ func TestCompleteWeChatOAuthRegistrationRejectsAdoptExistingUserSession(t *testi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing.T) {
|
func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
|
||||||
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
|
|
||||||
|
|
||||||
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
originalAccessTokenURL := wechatOAuthAccessTokenURL
|
||||||
originalUserInfoURL := wechatOAuthUserInfoURL
|
originalUserInfoURL := wechatOAuthUserInfoURL
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -1010,6 +969,22 @@ func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandler, *dbent.Client) {
|
func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandler, *dbent.Client) {
|
||||||
|
return newWeChatOAuthTestHandlerWithSettings(t, invitationEnabled, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wechatOAuthTestSettings(mode, appID, secret, frontendRedirect string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
service.SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
service.SettingKeyWeChatConnectAppID: appID,
|
||||||
|
service.SettingKeyWeChatConnectAppSecret: secret,
|
||||||
|
service.SettingKeyWeChatConnectMode: mode,
|
||||||
|
service.SettingKeyWeChatConnectScopes: service.DefaultWeChatConnectScopesForMode(mode),
|
||||||
|
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
service.SettingKeyWeChatConnectFrontendRedirectURL: frontendRedirect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool, extraSettings map[string]string) (*AuthHandler, *dbent.Client) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", "file:auth_wechat_oauth?mode=memory&cache=shared")
|
db, err := sql.Open("sqlite", "file:auth_wechat_oauth?mode=memory&cache=shared")
|
||||||
@@ -1036,12 +1011,17 @@ func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandl
|
|||||||
UserConcurrency: 1,
|
UserConcurrency: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
settingSvc := service.NewSettingService(&wechatOAuthSettingRepoStub{
|
values := map[string]string{
|
||||||
values: map[string]string{
|
|
||||||
service.SettingKeyRegistrationEnabled: "true",
|
service.SettingKeyRegistrationEnabled: "true",
|
||||||
service.SettingKeyInvitationCodeEnabled: boolSettingValue(invitationEnabled),
|
service.SettingKeyInvitationCodeEnabled: boolSettingValue(invitationEnabled),
|
||||||
},
|
}
|
||||||
}, cfg)
|
for key, value := range wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "/auth/wechat/callback") {
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range extraSettings {
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
settingSvc := service.NewSettingService(&wechatOAuthSettingRepoStub{values: values}, cfg)
|
||||||
|
|
||||||
authSvc := service.NewAuthService(
|
authSvc := service.NewAuthService(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ type SystemSettings struct {
|
|||||||
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
|
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
|
||||||
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
|
||||||
|
|
||||||
|
WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
|
||||||
|
WeChatConnectAppID string `json:"wechat_connect_app_id"`
|
||||||
|
WeChatConnectAppSecretConfigured bool `json:"wechat_connect_app_secret_configured"`
|
||||||
|
WeChatConnectMode string `json:"wechat_connect_mode"`
|
||||||
|
WeChatConnectScopes string `json:"wechat_connect_scopes"`
|
||||||
|
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
|
||||||
|
WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_redirect_url"`
|
||||||
|
|
||||||
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
|
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
|
||||||
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
|
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
|
||||||
OIDCConnectClientID string `json:"oidc_connect_client_id"`
|
OIDCConnectClientID string `json:"oidc_connect_client_id"`
|
||||||
|
|||||||
@@ -84,12 +84,17 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
|
|||||||
|
|
||||||
func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
|
func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
h := NewSettingHandler(service.NewSettingService(&settingHandlerPublicRepoStub{
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
values: map[string]string{
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "")
|
service.SettingKeyWeChatConnectEnabled: "true",
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "")
|
service.SettingKeyWeChatConnectAppID: "wx-mp-app",
|
||||||
|
service.SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
|
||||||
h := NewSettingHandler(service.NewSettingService(&settingHandlerPublicRepoStub{}, &config.Config{}), "test-version")
|
service.SettingKeyWeChatConnectMode: "mp",
|
||||||
|
service.SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
},
|
||||||
|
}, &config.Config{}), "test-version")
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(recorder)
|
c, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -110,6 +115,6 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
|
|||||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
||||||
require.Equal(t, 0, resp.Code)
|
require.Equal(t, 0, resp.Code)
|
||||||
require.True(t, resp.Data.WeChatOAuthEnabled)
|
require.True(t, resp.Data.WeChatOAuthEnabled)
|
||||||
require.True(t, resp.Data.WeChatOAuthOpenEnabled)
|
require.False(t, resp.Data.WeChatOAuthOpenEnabled)
|
||||||
require.False(t, resp.Data.WeChatOAuthMPEnabled)
|
require.True(t, resp.Data.WeChatOAuthMPEnabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,15 @@ const (
|
|||||||
SettingKeyLinuxDoConnectClientSecret = "linuxdo_connect_client_secret"
|
SettingKeyLinuxDoConnectClientSecret = "linuxdo_connect_client_secret"
|
||||||
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
||||||
|
|
||||||
|
// WeChat Connect OAuth 登录设置
|
||||||
|
SettingKeyWeChatConnectEnabled = "wechat_connect_enabled"
|
||||||
|
SettingKeyWeChatConnectAppID = "wechat_connect_app_id"
|
||||||
|
SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret"
|
||||||
|
SettingKeyWeChatConnectMode = "wechat_connect_mode"
|
||||||
|
SettingKeyWeChatConnectScopes = "wechat_connect_scopes"
|
||||||
|
SettingKeyWeChatConnectRedirectURL = "wechat_connect_redirect_url"
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL = "wechat_connect_frontend_redirect_url"
|
||||||
|
|
||||||
// Generic OIDC OAuth 登录设置
|
// Generic OIDC OAuth 登录设置
|
||||||
SettingKeyOIDCConnectEnabled = "oidc_connect_enabled"
|
SettingKeyOIDCConnectEnabled = "oidc_connect_enabled"
|
||||||
SettingKeyOIDCConnectProviderName = "oidc_connect_provider_name"
|
SettingKeyOIDCConnectProviderName = "oidc_connect_provider_name"
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ type UpdatePaymentConfigRequest struct {
|
|||||||
CancelRateLimitWindow *int `json:"cancel_rate_limit_window"`
|
CancelRateLimitWindow *int `json:"cancel_rate_limit_window"`
|
||||||
CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
|
CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
|
||||||
CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
|
CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
|
||||||
|
|
||||||
|
VisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
|
||||||
|
VisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
|
||||||
|
VisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
|
||||||
|
VisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MethodLimits holds per-payment-type limits.
|
// MethodLimits holds per-payment-type limits.
|
||||||
@@ -319,6 +324,10 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
|
|||||||
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow),
|
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow),
|
||||||
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit),
|
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit),
|
||||||
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode),
|
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode),
|
||||||
|
SettingPaymentVisibleMethodAlipaySource: derefStr(req.VisibleMethodAlipaySource),
|
||||||
|
SettingPaymentVisibleMethodWxpaySource: derefStr(req.VisibleMethodWxpaySource),
|
||||||
|
SettingPaymentVisibleMethodAlipayEnabled: formatBoolOrEmpty(req.VisibleMethodAlipayEnabled),
|
||||||
|
SettingPaymentVisibleMethodWxpayEnabled: formatBoolOrEmpty(req.VisibleMethodWxpayEnabled),
|
||||||
}
|
}
|
||||||
if req.EnabledTypes != nil {
|
if req.EnabledTypes != nil {
|
||||||
m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")
|
m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ func newPaymentConfigServiceTestClient(t *testing.T) *dbent.Client {
|
|||||||
|
|
||||||
type paymentConfigSettingRepoStub struct {
|
type paymentConfigSettingRepoStub struct {
|
||||||
values map[string]string
|
values map[string]string
|
||||||
|
updates map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *paymentConfigSettingRepoStub) Get(context.Context, string) (*Setting, error) {
|
func (s *paymentConfigSettingRepoStub) Get(context.Context, string) (*Setting, error) {
|
||||||
@@ -383,10 +384,52 @@ func (s *paymentConfigSettingRepoStub) GetMultiple(_ context.Context, keys []str
|
|||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
func (s *paymentConfigSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
|
func (s *paymentConfigSettingRepoStub) SetMultiple(_ context.Context, values map[string]string) error {
|
||||||
|
s.updates = make(map[string]string, len(values))
|
||||||
|
for key, value := range values {
|
||||||
|
s.updates[key] = value
|
||||||
|
if s.values == nil {
|
||||||
|
s.values = map[string]string{}
|
||||||
|
}
|
||||||
|
s.values[key] = value
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *paymentConfigSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
|
func (s *paymentConfigSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
|
||||||
return s.values, nil
|
return s.values, nil
|
||||||
}
|
}
|
||||||
func (s *paymentConfigSettingRepoStub) Delete(context.Context, string) error { return nil }
|
func (s *paymentConfigSettingRepoStub) Delete(context.Context, string) error { return nil }
|
||||||
|
|
||||||
|
func TestUpdatePaymentConfig_PersistsVisibleMethodRouting(t *testing.T) {
|
||||||
|
repo := &paymentConfigSettingRepoStub{values: map[string]string{}}
|
||||||
|
svc := &PaymentConfigService{settingRepo: repo}
|
||||||
|
|
||||||
|
alipayEnabled := true
|
||||||
|
wxpayEnabled := false
|
||||||
|
err := svc.UpdatePaymentConfig(context.Background(), UpdatePaymentConfigRequest{
|
||||||
|
VisibleMethodAlipayEnabled: &alipayEnabled,
|
||||||
|
VisibleMethodAlipaySource: paymentConfigStrPtr(VisibleMethodSourceEasyPayAlipay),
|
||||||
|
VisibleMethodWxpayEnabled: &wxpayEnabled,
|
||||||
|
VisibleMethodWxpaySource: paymentConfigStrPtr(VisibleMethodSourceOfficialWechat),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdatePaymentConfig returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.values[SettingPaymentVisibleMethodAlipayEnabled] != "true" {
|
||||||
|
t.Fatalf("alipay enabled = %q, want true", repo.values[SettingPaymentVisibleMethodAlipayEnabled])
|
||||||
|
}
|
||||||
|
if repo.values[SettingPaymentVisibleMethodAlipaySource] != VisibleMethodSourceEasyPayAlipay {
|
||||||
|
t.Fatalf("alipay source = %q, want %q", repo.values[SettingPaymentVisibleMethodAlipaySource], VisibleMethodSourceEasyPayAlipay)
|
||||||
|
}
|
||||||
|
if repo.values[SettingPaymentVisibleMethodWxpayEnabled] != "false" {
|
||||||
|
t.Fatalf("wxpay enabled = %q, want false", repo.values[SettingPaymentVisibleMethodWxpayEnabled])
|
||||||
|
}
|
||||||
|
if repo.values[SettingPaymentVisibleMethodWxpaySource] != VisibleMethodSourceOfficialWechat {
|
||||||
|
t.Fatalf("wxpay source = %q, want %q", repo.values[SettingPaymentVisibleMethodWxpaySource], VisibleMethodSourceOfficialWechat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentConfigStrPtr(value string) *string {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -512,16 +511,21 @@ func requiresWeChatJSAPICompatibleSelection(req CreateOrderRequest, sel *payment
|
|||||||
return req.IsWeChatBrowser || strings.TrimSpace(req.OpenID) != ""
|
return req.IsWeChatBrowser || strings.TrimSpace(req.OpenID) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PaymentService) getWeChatPaymentOAuthCredential(context.Context) (string, string, error) {
|
func (s *PaymentService) getWeChatPaymentOAuthCredential(ctx context.Context) (string, string, error) {
|
||||||
appID := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
|
if s == nil || s.configService == nil || s.configService.settingRepo == nil {
|
||||||
appSecret := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
|
|
||||||
if appID == "" || appSecret == "" {
|
|
||||||
return "", "", infraerrors.ServiceUnavailable(
|
return "", "", infraerrors.ServiceUnavailable(
|
||||||
"WECHAT_PAYMENT_MP_NOT_CONFIGURED",
|
"WECHAT_PAYMENT_MP_NOT_CONFIGURED",
|
||||||
"wechat in-app payment requires a complete WeChat MP OAuth credential",
|
"wechat in-app payment requires a complete WeChat MP OAuth credential",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return appID, appSecret, nil
|
cfg, err := (&SettingService{settingRepo: s.configService.settingRepo}).GetWeChatConnectOAuthConfig(ctx)
|
||||||
|
if err != nil || cfg.Mode != "mp" || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" {
|
||||||
|
return "", "", infraerrors.ServiceUnavailable(
|
||||||
|
"WECHAT_PAYMENT_MP_NOT_CONFIGURED",
|
||||||
|
"wechat in-app payment requires a complete WeChat MP OAuth credential",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(cfg.AppID), strings.TrimSpace(cfg.AppSecret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func classifyCreatePaymentError(req CreateOrderRequest, providerKey string, err error) error {
|
func classifyCreatePaymentError(req CreateOrderRequest, providerKey string, err error) error {
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
|
|||||||
settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{
|
settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{
|
||||||
SettingPaymentVisibleMethodWxpayEnabled: "true",
|
SettingPaymentVisibleMethodWxpayEnabled: "true",
|
||||||
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
|
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
|
||||||
|
SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
SettingKeyWeChatConnectAppID: "wx-mp-app",
|
||||||
|
SettingKeyWeChatConnectAppSecret: "wechat-secret",
|
||||||
|
SettingKeyWeChatConnectMode: "mp",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
}},
|
}},
|
||||||
encryptionKey: []byte(jsapiTestEncryptionKey),
|
encryptionKey: []byte(jsapiTestEncryptionKey),
|
||||||
}
|
}
|
||||||
@@ -77,9 +84,6 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
|
|||||||
configService: configService,
|
configService: configService,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
|
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
|
|
||||||
|
|
||||||
sel, err := svc.selectCreateOrderInstance(ctx, CreateOrderRequest{
|
sel, err := svc.selectCreateOrderInstance(ctx, CreateOrderRequest{
|
||||||
PaymentType: payment.TypeWxpay,
|
PaymentType: payment.TypeWxpay,
|
||||||
OpenID: "openid-123",
|
OpenID: "openid-123",
|
||||||
|
|||||||
@@ -91,10 +91,15 @@ func TestBuildCreateOrderResponseCopiesJSAPIPayload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
|
func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx123456")
|
svc := newWeChatPaymentOAuthTestService(map[string]string{
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
|
SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
SettingKeyWeChatConnectAppID: "wx123456",
|
||||||
svc := &PaymentService{}
|
SettingKeyWeChatConnectAppSecret: "wechat-secret",
|
||||||
|
SettingKeyWeChatConnectMode: "mp",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
})
|
||||||
|
|
||||||
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
|
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
|
||||||
Amount: 12.5,
|
Amount: 12.5,
|
||||||
@@ -132,7 +137,7 @@ func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
|
|||||||
func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testing.T) {
|
func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc := &PaymentService{}
|
svc := newWeChatPaymentOAuthTestService(nil)
|
||||||
|
|
||||||
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
|
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
|
||||||
Amount: 12.5,
|
Amount: 12.5,
|
||||||
@@ -155,10 +160,15 @@ func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t *testing.T) {
|
func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx123456")
|
svc := newWeChatPaymentOAuthTestService(map[string]string{
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
|
SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
SettingKeyWeChatConnectAppID: "wx123456",
|
||||||
svc := &PaymentService{}
|
SettingKeyWeChatConnectAppSecret: "wechat-secret",
|
||||||
|
SettingKeyWeChatConnectMode: "mp",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
})
|
||||||
|
|
||||||
resp, err := svc.maybeBuildWeChatOAuthRequiredResponseForSelection(context.Background(), CreateOrderRequest{
|
resp, err := svc.maybeBuildWeChatOAuthRequiredResponseForSelection(context.Background(), CreateOrderRequest{
|
||||||
Amount: 12.5,
|
Amount: 12.5,
|
||||||
@@ -175,3 +185,11 @@ func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t
|
|||||||
t.Fatalf("expected nil response, got %+v", resp)
|
t.Fatalf("expected nil response, got %+v", resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newWeChatPaymentOAuthTestService(values map[string]string) *PaymentService {
|
||||||
|
return &PaymentService{
|
||||||
|
configService: &PaymentConfigService{
|
||||||
|
settingRepo: &paymentConfigSettingRepoStub{values: values},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -173,8 +172,43 @@ var (
|
|||||||
const (
|
const (
|
||||||
defaultAuthSourceBalance = 0
|
defaultAuthSourceBalance = 0
|
||||||
defaultAuthSourceConcurrency = 5
|
defaultAuthSourceConcurrency = 5
|
||||||
|
defaultWeChatConnectMode = "open"
|
||||||
|
defaultWeChatConnectScopes = "snsapi_login"
|
||||||
|
defaultWeChatConnectFrontend = "/auth/wechat/callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func normalizeWeChatConnectModeSetting(raw string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||||
|
case "mp":
|
||||||
|
return "mp"
|
||||||
|
default:
|
||||||
|
return "open"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultWeChatConnectScopeForMode(mode string) string {
|
||||||
|
if normalizeWeChatConnectModeSetting(mode) == "mp" {
|
||||||
|
return "snsapi_userinfo"
|
||||||
|
}
|
||||||
|
return defaultWeChatConnectScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWeChatConnectScopeSetting(raw, mode string) string {
|
||||||
|
switch normalizeWeChatConnectModeSetting(mode) {
|
||||||
|
case "mp":
|
||||||
|
switch strings.TrimSpace(raw) {
|
||||||
|
case "snsapi_base":
|
||||||
|
return "snsapi_base"
|
||||||
|
case "snsapi_userinfo":
|
||||||
|
return "snsapi_userinfo"
|
||||||
|
default:
|
||||||
|
return defaultWeChatConnectScopeForMode(mode)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return defaultWeChatConnectScopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewSettingService 创建系统设置服务实例
|
// NewSettingService 创建系统设置服务实例
|
||||||
func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
|
func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
|
||||||
return &SettingService{
|
return &SettingService{
|
||||||
@@ -240,6 +274,13 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
SettingKeyCustomMenuItems,
|
SettingKeyCustomMenuItems,
|
||||||
SettingKeyCustomEndpoints,
|
SettingKeyCustomEndpoints,
|
||||||
SettingKeyLinuxDoConnectEnabled,
|
SettingKeyLinuxDoConnectEnabled,
|
||||||
|
SettingKeyWeChatConnectEnabled,
|
||||||
|
SettingKeyWeChatConnectAppID,
|
||||||
|
SettingKeyWeChatConnectAppSecret,
|
||||||
|
SettingKeyWeChatConnectMode,
|
||||||
|
SettingKeyWeChatConnectScopes,
|
||||||
|
SettingKeyWeChatConnectRedirectURL,
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL,
|
||||||
SettingKeyBackendModeEnabled,
|
SettingKeyBackendModeEnabled,
|
||||||
SettingPaymentEnabled,
|
SettingPaymentEnabled,
|
||||||
SettingKeyOIDCConnectEnabled,
|
SettingKeyOIDCConnectEnabled,
|
||||||
@@ -274,9 +315,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
if oidcProviderName == "" {
|
if oidcProviderName == "" {
|
||||||
oidcProviderName = "OIDC"
|
oidcProviderName = "OIDC"
|
||||||
}
|
}
|
||||||
weChatOpenEnabled := isWeChatOAuthOpenConfigured()
|
weChatEnabled, weChatOpenEnabled, weChatMPEnabled := s.weChatOAuthCapabilitiesFromSettings(settings)
|
||||||
weChatMPEnabled := isWeChatOAuthMPConfigured()
|
|
||||||
weChatEnabled := weChatOpenEnabled || weChatMPEnabled
|
|
||||||
|
|
||||||
// Password reset requires email verification to be enabled
|
// Password reset requires email verification to be enabled
|
||||||
emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
|
emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
|
||||||
@@ -431,6 +470,56 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultWeChatConnectScopesForMode(mode string) string {
|
||||||
|
return defaultWeChatConnectScopeForMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
|
||||||
|
cfg := WeChatConnectOAuthConfig{
|
||||||
|
Enabled: settings[SettingKeyWeChatConnectEnabled] == "true",
|
||||||
|
AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]),
|
||||||
|
AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]),
|
||||||
|
Mode: normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]),
|
||||||
|
Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode]),
|
||||||
|
RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]),
|
||||||
|
FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]),
|
||||||
|
}
|
||||||
|
if cfg.FrontendRedirectURL == "" {
|
||||||
|
cfg.FrontendRedirectURL = defaultWeChatConnectFrontend
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Enabled {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
|
||||||
|
}
|
||||||
|
if cfg.AppID == "" {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth app id not configured")
|
||||||
|
}
|
||||||
|
if cfg.AppSecret == "" {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth app secret not configured")
|
||||||
|
}
|
||||||
|
if cfg.RedirectURL == "" {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured")
|
||||||
|
}
|
||||||
|
if cfg.FrontendRedirectURL == "" {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url not configured")
|
||||||
|
}
|
||||||
|
if err := config.ValidateAbsoluteHTTPURL(cfg.RedirectURL); err != nil {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
|
||||||
|
}
|
||||||
|
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
|
||||||
|
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool) {
|
||||||
|
cfg, err := s.parseWeChatConnectOAuthConfig(settings)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, false
|
||||||
|
}
|
||||||
|
return true, cfg.Mode == "open", cfg.Mode == "mp"
|
||||||
|
}
|
||||||
|
|
||||||
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
|
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
|
||||||
// array string, returning only items with visibility != "admin".
|
// array string, returning only items with visibility != "admin".
|
||||||
func filterUserVisibleMenuItems(raw string) json.RawMessage {
|
func filterUserVisibleMenuItems(raw string) json.RawMessage {
|
||||||
@@ -467,20 +556,6 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWeChatOAuthConfigured() bool {
|
|
||||||
return isWeChatOAuthOpenConfigured() || isWeChatOAuthMPConfigured()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWeChatOAuthOpenConfigured() bool {
|
|
||||||
return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID")) != "" &&
|
|
||||||
strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET")) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWeChatOAuthMPConfigured() bool {
|
|
||||||
return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID")) != "" &&
|
|
||||||
strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET")) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
|
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
|
||||||
func safeRawJSONArray(raw string) json.RawMessage {
|
func safeRawJSONArray(raw string) json.RawMessage {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
@@ -625,6 +700,15 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
}
|
}
|
||||||
settings.PaymentVisibleMethodAlipaySource = alipaySource
|
settings.PaymentVisibleMethodAlipaySource = alipaySource
|
||||||
settings.PaymentVisibleMethodWxpaySource = wxpaySource
|
settings.PaymentVisibleMethodWxpaySource = wxpaySource
|
||||||
|
settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
|
||||||
|
settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret)
|
||||||
|
settings.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings.WeChatConnectMode)
|
||||||
|
settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode)
|
||||||
|
settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL)
|
||||||
|
settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL)
|
||||||
|
if settings.WeChatConnectFrontendRedirectURL == "" {
|
||||||
|
settings.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
|
||||||
|
}
|
||||||
|
|
||||||
updates := make(map[string]string)
|
updates := make(map[string]string)
|
||||||
|
|
||||||
@@ -694,6 +778,17 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
|
updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeChat Connect OAuth 登录
|
||||||
|
updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
|
||||||
|
updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
|
||||||
|
updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode
|
||||||
|
updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes
|
||||||
|
updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL
|
||||||
|
updates[SettingKeyWeChatConnectFrontendRedirectURL] = settings.WeChatConnectFrontendRedirectURL
|
||||||
|
if settings.WeChatConnectAppSecret != "" {
|
||||||
|
updates[SettingKeyWeChatConnectAppSecret] = settings.WeChatConnectAppSecret
|
||||||
|
}
|
||||||
|
|
||||||
// OEM设置
|
// OEM设置
|
||||||
updates[SettingKeySiteName] = settings.SiteName
|
updates[SettingKeySiteName] = settings.SiteName
|
||||||
updates[SettingKeySiteLogo] = settings.SiteLogo
|
updates[SettingKeySiteLogo] = settings.SiteLogo
|
||||||
@@ -1200,6 +1295,10 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
SettingKeyTablePageSizeOptions: "[10,20,50,100]",
|
SettingKeyTablePageSizeOptions: "[10,20,50,100]",
|
||||||
SettingKeyCustomMenuItems: "[]",
|
SettingKeyCustomMenuItems: "[]",
|
||||||
SettingKeyCustomEndpoints: "[]",
|
SettingKeyCustomEndpoints: "[]",
|
||||||
|
SettingKeyWeChatConnectEnabled: "false",
|
||||||
|
SettingKeyWeChatConnectMode: "open",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_login",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
|
||||||
SettingKeyOIDCConnectEnabled: "false",
|
SettingKeyOIDCConnectEnabled: "false",
|
||||||
SettingKeyOIDCConnectProviderName: "OIDC",
|
SettingKeyOIDCConnectProviderName: "OIDC",
|
||||||
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
||||||
@@ -1491,6 +1590,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
}
|
}
|
||||||
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
|
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
|
||||||
|
|
||||||
|
// WeChat Connect 设置:完全以 DB 系统设置为准。
|
||||||
|
result.WeChatConnectEnabled = settings[SettingKeyWeChatConnectEnabled] == "true"
|
||||||
|
result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID])
|
||||||
|
result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret])
|
||||||
|
result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != ""
|
||||||
|
result.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode])
|
||||||
|
result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode])
|
||||||
|
result.WeChatConnectRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL])
|
||||||
|
result.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL])
|
||||||
|
if result.WeChatConnectFrontendRedirectURL == "" {
|
||||||
|
result.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
|
||||||
|
}
|
||||||
|
|
||||||
// Model fallback settings
|
// Model fallback settings
|
||||||
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
|
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
|
||||||
result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
|
result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
|
||||||
@@ -1972,6 +2084,26 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf
|
|||||||
return effective, nil
|
return effective, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWeChatConnectOAuthConfig 返回用于登录的最终生效 WeChat Connect 配置。
|
||||||
|
//
|
||||||
|
// WeChat Connect 已回归 DB 系统设置模型,不再回退到 config/env。
|
||||||
|
func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeChatConnectOAuthConfig, error) {
|
||||||
|
keys := []string{
|
||||||
|
SettingKeyWeChatConnectEnabled,
|
||||||
|
SettingKeyWeChatConnectAppID,
|
||||||
|
SettingKeyWeChatConnectAppSecret,
|
||||||
|
SettingKeyWeChatConnectMode,
|
||||||
|
SettingKeyWeChatConnectScopes,
|
||||||
|
SettingKeyWeChatConnectRedirectURL,
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL,
|
||||||
|
}
|
||||||
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||||
|
if err != nil {
|
||||||
|
return WeChatConnectOAuthConfig{}, fmt.Errorf("get wechat connect settings: %w", err)
|
||||||
|
}
|
||||||
|
return s.parseWeChatConnectOAuthConfig(settings)
|
||||||
|
}
|
||||||
|
|
||||||
// GetOverloadCooldownSettings 获取529过载冷却配置
|
// GetOverloadCooldownSettings 获取529过载冷却配置
|
||||||
func (s *SettingService) GetOverloadCooldownSettings(ctx context.Context) (*OverloadCooldownSettings, error) {
|
func (s *SettingService) GetOverloadCooldownSettings(ctx context.Context) (*OverloadCooldownSettings, error) {
|
||||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyOverloadCooldownSettings)
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyOverloadCooldownSettings)
|
||||||
|
|||||||
@@ -92,16 +92,21 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
|
func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
|
svc := NewSettingService(&settingPublicRepoStub{
|
||||||
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
|
values: map[string]string{
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "")
|
SettingKeyWeChatConnectEnabled: "true",
|
||||||
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "")
|
SettingKeyWeChatConnectAppID: "wx-mp-app",
|
||||||
|
SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
|
||||||
svc := NewSettingService(&settingPublicRepoStub{}, &config.Config{})
|
SettingKeyWeChatConnectMode: "mp",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
},
|
||||||
|
}, &config.Config{})
|
||||||
|
|
||||||
settings, err := svc.GetPublicSettings(context.Background())
|
settings, err := svc.GetPublicSettings(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, settings.WeChatOAuthEnabled)
|
require.True(t, settings.WeChatOAuthEnabled)
|
||||||
require.True(t, settings.WeChatOAuthOpenEnabled)
|
require.False(t, settings.WeChatOAuthOpenEnabled)
|
||||||
require.False(t, settings.WeChatOAuthMPEnabled)
|
require.True(t, settings.WeChatOAuthMPEnabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type settingWeChatRepoStub struct {
|
||||||
|
values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) Get(context.Context, string) (*Setting, error) {
|
||||||
|
panic("unexpected Get call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) GetValue(_ context.Context, key string) (string, error) {
|
||||||
|
if value, ok := s.values[key]; ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
return "", ErrSettingNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) Set(context.Context, string, string) error {
|
||||||
|
panic("unexpected Set call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
|
||||||
|
out := make(map[string]string, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
if value, ok := s.values[key]; ok {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) SetMultiple(context.Context, map[string]string) error {
|
||||||
|
panic("unexpected SetMultiple call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) GetAll(context.Context) (map[string]string, error) {
|
||||||
|
panic("unexpected GetAll call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settingWeChatRepoStub) Delete(context.Context, string) error {
|
||||||
|
panic("unexpected Delete call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *testing.T) {
|
||||||
|
repo := &settingWeChatRepoStub{
|
||||||
|
values: map[string]string{
|
||||||
|
SettingKeyWeChatConnectEnabled: "true",
|
||||||
|
SettingKeyWeChatConnectAppID: "wx-db-app",
|
||||||
|
SettingKeyWeChatConnectAppSecret: "wx-db-secret",
|
||||||
|
SettingKeyWeChatConnectMode: "mp",
|
||||||
|
SettingKeyWeChatConnectScopes: "snsapi_base",
|
||||||
|
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := NewSettingService(repo, &config.Config{})
|
||||||
|
|
||||||
|
got, err := svc.GetWeChatConnectOAuthConfig(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, got.Enabled)
|
||||||
|
require.Equal(t, "wx-db-app", got.AppID)
|
||||||
|
require.Equal(t, "wx-db-secret", got.AppSecret)
|
||||||
|
require.Equal(t, "mp", got.Mode)
|
||||||
|
require.Equal(t, "snsapi_base", got.Scopes)
|
||||||
|
require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL)
|
||||||
|
require.Equal(t, "/auth/wechat/callback", got.FrontendRedirectURL)
|
||||||
|
}
|
||||||
@@ -31,6 +31,16 @@ type SystemSettings struct {
|
|||||||
LinuxDoConnectClientSecretConfigured bool
|
LinuxDoConnectClientSecretConfigured bool
|
||||||
LinuxDoConnectRedirectURL string
|
LinuxDoConnectRedirectURL string
|
||||||
|
|
||||||
|
// WeChat Connect OAuth 登录
|
||||||
|
WeChatConnectEnabled bool
|
||||||
|
WeChatConnectAppID string
|
||||||
|
WeChatConnectAppSecret string
|
||||||
|
WeChatConnectAppSecretConfigured bool
|
||||||
|
WeChatConnectMode string
|
||||||
|
WeChatConnectScopes string
|
||||||
|
WeChatConnectRedirectURL string
|
||||||
|
WeChatConnectFrontendRedirectURL string
|
||||||
|
|
||||||
// Generic OIDC OAuth 登录
|
// Generic OIDC OAuth 登录
|
||||||
OIDCConnectEnabled bool
|
OIDCConnectEnabled bool
|
||||||
OIDCConnectProviderName string
|
OIDCConnectProviderName string
|
||||||
@@ -177,6 +187,16 @@ type PublicSettings struct {
|
|||||||
BalanceLowNotifyRechargeURL string
|
BalanceLowNotifyRechargeURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WeChatConnectOAuthConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
AppID string
|
||||||
|
AppSecret string
|
||||||
|
Mode string
|
||||||
|
Scopes string
|
||||||
|
RedirectURL string
|
||||||
|
FrontendRedirectURL string
|
||||||
|
}
|
||||||
|
|
||||||
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
||||||
type StreamTimeoutSettings struct {
|
type StreamTimeoutSettings struct {
|
||||||
// Enabled 是否启用流超时处理
|
// Enabled 是否启用流超时处理
|
||||||
|
|||||||
21
frontend/src/api/__tests__/settings.wechatConnect.spec.ts
Normal file
21
frontend/src/api/__tests__/settings.wechatConnect.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultWeChatConnectScopesForMode,
|
||||||
|
normalizeWeChatConnectMode,
|
||||||
|
} from "@/api/admin/settings";
|
||||||
|
|
||||||
|
describe("admin settings wechat connect helpers", () => {
|
||||||
|
it("normalizes legacy or noisy mode values to the backend contract", () => {
|
||||||
|
expect(normalizeWeChatConnectMode("OPEN")).toBe("open");
|
||||||
|
expect(normalizeWeChatConnectMode(" open_platform ")).toBe("open");
|
||||||
|
expect(normalizeWeChatConnectMode("mp")).toBe("mp");
|
||||||
|
expect(normalizeWeChatConnectMode("official_account")).toBe("mp");
|
||||||
|
expect(normalizeWeChatConnectMode("unknown")).toBe("open");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps each mode to the backend default scopes", () => {
|
||||||
|
expect(defaultWeChatConnectScopesForMode("open")).toBe("snsapi_login");
|
||||||
|
expect(defaultWeChatConnectScopesForMode("mp")).toBe("snsapi_userinfo");
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { defineComponent, h, ref } from 'vue'
|
import { defineComponent, h, ref } from "vue";
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from "@vue/test-utils";
|
||||||
|
|
||||||
import SettingsView from '../SettingsView.vue'
|
import SettingsView from "../SettingsView.vue";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getSettings,
|
getSettings,
|
||||||
@@ -38,9 +38,9 @@ const {
|
|||||||
adminSettingsFetch: vi.fn(),
|
adminSettingsFetch: vi.fn(),
|
||||||
showError: vi.fn(),
|
showError: vi.fn(),
|
||||||
showSuccess: vi.fn(),
|
showSuccess: vi.fn(),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('@/api', () => ({
|
vi.mock("@/api", () => ({
|
||||||
adminAPI: {
|
adminAPI: {
|
||||||
settings: {
|
settings: {
|
||||||
getSettings,
|
getSettings,
|
||||||
@@ -63,9 +63,9 @@ vi.mock('@/api', () => ({
|
|||||||
getProviders,
|
getProviders,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('@/stores', () => ({
|
vi.mock("@/stores", () => ({
|
||||||
useAppStore: () => ({
|
useAppStore: () => ({
|
||||||
showError,
|
showError,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
@@ -73,36 +73,36 @@ vi.mock('@/stores', () => ({
|
|||||||
showInfo: vi.fn(),
|
showInfo: vi.fn(),
|
||||||
fetchPublicSettings,
|
fetchPublicSettings,
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('@/stores/adminSettings', () => ({
|
vi.mock("@/stores/adminSettings", () => ({
|
||||||
useAdminSettingsStore: () => ({
|
useAdminSettingsStore: () => ({
|
||||||
fetch: adminSettingsFetch,
|
fetch: adminSettingsFetch,
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('@/composables/useClipboard', () => ({
|
vi.mock("@/composables/useClipboard", () => ({
|
||||||
useClipboard: () => ({
|
useClipboard: () => ({
|
||||||
copyToClipboard: vi.fn(),
|
copyToClipboard: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('@/utils/apiError', () => ({
|
vi.mock("@/utils/apiError", () => ({
|
||||||
extractApiErrorMessage: () => 'error',
|
extractApiErrorMessage: () => "error",
|
||||||
}))
|
}));
|
||||||
|
|
||||||
vi.mock('vue-i18n', async () => {
|
vi.mock("vue-i18n", async () => {
|
||||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
const actual = await vi.importActual<typeof import("vue-i18n")>("vue-i18n");
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useI18n: () => ({
|
useI18n: () => ({
|
||||||
t: (key: string) => key,
|
t: (key: string) => key,
|
||||||
locale: ref('zh-CN'),
|
locale: ref("zh-CN"),
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
const AppLayoutStub = { template: '<div><slot /></div>' }
|
const AppLayoutStub = { template: "<div><slot /></div>" };
|
||||||
const ToggleStub = defineComponent({
|
const ToggleStub = defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -110,25 +110,25 @@ const ToggleStub = defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ["update:modelValue"],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
return () =>
|
return () =>
|
||||||
h('input', {
|
h("input", {
|
||||||
class: 'toggle-stub',
|
class: "toggle-stub",
|
||||||
type: 'checkbox',
|
type: "checkbox",
|
||||||
checked: props.modelValue,
|
checked: props.modelValue,
|
||||||
onChange: (event: Event) => {
|
onChange: (event: Event) => {
|
||||||
emit('update:modelValue', (event.target as HTMLInputElement).checked)
|
emit("update:modelValue", (event.target as HTMLInputElement).checked);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const SelectStub = defineComponent({
|
const SelectStub = defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Boolean, null],
|
type: [String, Number, Boolean, null],
|
||||||
default: '',
|
default: "",
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -136,42 +136,43 @@ const SelectStub = defineComponent({
|
|||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'change'],
|
emits: ["update:modelValue", "change"],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const onChange = (event: Event) => {
|
const onChange = (event: Event) => {
|
||||||
const target = event.target as HTMLSelectElement
|
const target = event.target as HTMLSelectElement;
|
||||||
emit('update:modelValue', target.value)
|
emit("update:modelValue", target.value);
|
||||||
const option = (props.options as Array<Record<string, unknown>>).find(
|
const option =
|
||||||
(item) => String(item.value ?? '') === target.value
|
(props.options as Array<Record<string, unknown>>).find(
|
||||||
) ?? null
|
(item) => String(item.value ?? "") === target.value,
|
||||||
emit('change', target.value, option)
|
) ?? null;
|
||||||
}
|
emit("change", target.value, option);
|
||||||
|
};
|
||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
'select',
|
"select",
|
||||||
{
|
{
|
||||||
class: 'select-stub',
|
class: "select-stub",
|
||||||
value: props.modelValue ?? '',
|
value: props.modelValue ?? "",
|
||||||
'data-placeholder': props.placeholder,
|
"data-placeholder": props.placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
},
|
},
|
||||||
(props.options as Array<Record<string, unknown>>).map((option) =>
|
(props.options as Array<Record<string, unknown>>).map((option) =>
|
||||||
h(
|
h(
|
||||||
'option',
|
"option",
|
||||||
{
|
{
|
||||||
key: `${String(option.value ?? '')}:${String(option.label ?? '')}`,
|
key: `${String(option.value ?? "")}:${String(option.label ?? "")}`,
|
||||||
value: option.value as string,
|
value: option.value as string,
|
||||||
},
|
},
|
||||||
String(option.label ?? '')
|
String(option.label ?? ""),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const baseSettingsResponse = {
|
const baseSettingsResponse = {
|
||||||
registration_enabled: true,
|
registration_enabled: true,
|
||||||
@@ -185,69 +186,77 @@ const baseSettingsResponse = {
|
|||||||
default_balance: 0,
|
default_balance: 0,
|
||||||
default_concurrency: 1,
|
default_concurrency: 1,
|
||||||
default_subscriptions: [],
|
default_subscriptions: [],
|
||||||
site_name: 'Sub2API',
|
site_name: "Sub2API",
|
||||||
site_logo: '',
|
site_logo: "",
|
||||||
site_subtitle: '',
|
site_subtitle: "",
|
||||||
api_base_url: '',
|
api_base_url: "",
|
||||||
contact_info: '',
|
contact_info: "",
|
||||||
doc_url: '',
|
doc_url: "",
|
||||||
home_content: '',
|
home_content: "",
|
||||||
hide_ccs_import_button: false,
|
hide_ccs_import_button: false,
|
||||||
table_default_page_size: 20,
|
table_default_page_size: 20,
|
||||||
table_page_size_options: [10, 20, 50, 100],
|
table_page_size_options: [10, 20, 50, 100],
|
||||||
backend_mode_enabled: false,
|
backend_mode_enabled: false,
|
||||||
custom_menu_items: [],
|
custom_menu_items: [],
|
||||||
custom_endpoints: [],
|
custom_endpoints: [],
|
||||||
frontend_url: '',
|
frontend_url: "",
|
||||||
smtp_host: '',
|
smtp_host: "",
|
||||||
smtp_port: 587,
|
smtp_port: 587,
|
||||||
smtp_username: '',
|
smtp_username: "",
|
||||||
smtp_password_configured: false,
|
smtp_password_configured: false,
|
||||||
smtp_from_email: '',
|
smtp_from_email: "",
|
||||||
smtp_from_name: '',
|
smtp_from_name: "",
|
||||||
smtp_use_tls: true,
|
smtp_use_tls: true,
|
||||||
turnstile_enabled: false,
|
turnstile_enabled: false,
|
||||||
turnstile_site_key: '',
|
turnstile_site_key: "",
|
||||||
turnstile_secret_key_configured: false,
|
turnstile_secret_key_configured: false,
|
||||||
linuxdo_connect_enabled: false,
|
linuxdo_connect_enabled: false,
|
||||||
linuxdo_connect_client_id: '',
|
linuxdo_connect_client_id: "",
|
||||||
linuxdo_connect_client_secret_configured: false,
|
linuxdo_connect_client_secret_configured: false,
|
||||||
linuxdo_connect_redirect_url: '',
|
linuxdo_connect_redirect_url: "",
|
||||||
|
wechat_connect_enabled: true,
|
||||||
|
wechat_connect_app_id: "wx-app-id-123",
|
||||||
|
wechat_connect_app_secret_configured: true,
|
||||||
|
wechat_connect_mode: "mp",
|
||||||
|
wechat_connect_scopes: "",
|
||||||
|
wechat_connect_redirect_url:
|
||||||
|
"https://admin.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
wechat_connect_frontend_redirect_url: "/auth/wechat/callback",
|
||||||
oidc_connect_enabled: false,
|
oidc_connect_enabled: false,
|
||||||
oidc_connect_provider_name: 'OIDC',
|
oidc_connect_provider_name: "OIDC",
|
||||||
oidc_connect_client_id: '',
|
oidc_connect_client_id: "",
|
||||||
oidc_connect_client_secret_configured: false,
|
oidc_connect_client_secret_configured: false,
|
||||||
oidc_connect_issuer_url: '',
|
oidc_connect_issuer_url: "",
|
||||||
oidc_connect_discovery_url: '',
|
oidc_connect_discovery_url: "",
|
||||||
oidc_connect_authorize_url: '',
|
oidc_connect_authorize_url: "",
|
||||||
oidc_connect_token_url: '',
|
oidc_connect_token_url: "",
|
||||||
oidc_connect_userinfo_url: '',
|
oidc_connect_userinfo_url: "",
|
||||||
oidc_connect_jwks_url: '',
|
oidc_connect_jwks_url: "",
|
||||||
oidc_connect_scopes: 'openid email profile',
|
oidc_connect_scopes: "openid email profile",
|
||||||
oidc_connect_redirect_url: '',
|
oidc_connect_redirect_url: "",
|
||||||
oidc_connect_frontend_redirect_url: '/auth/oidc/callback',
|
oidc_connect_frontend_redirect_url: "/auth/oidc/callback",
|
||||||
oidc_connect_token_auth_method: 'client_secret_post',
|
oidc_connect_token_auth_method: "client_secret_post",
|
||||||
oidc_connect_use_pkce: true,
|
oidc_connect_use_pkce: true,
|
||||||
oidc_connect_validate_id_token: true,
|
oidc_connect_validate_id_token: true,
|
||||||
oidc_connect_allowed_signing_algs: 'RS256,ES256,PS256',
|
oidc_connect_allowed_signing_algs: "RS256,ES256,PS256",
|
||||||
oidc_connect_clock_skew_seconds: 120,
|
oidc_connect_clock_skew_seconds: 120,
|
||||||
oidc_connect_require_email_verified: false,
|
oidc_connect_require_email_verified: false,
|
||||||
oidc_connect_userinfo_email_path: '',
|
oidc_connect_userinfo_email_path: "",
|
||||||
oidc_connect_userinfo_id_path: '',
|
oidc_connect_userinfo_id_path: "",
|
||||||
oidc_connect_userinfo_username_path: '',
|
oidc_connect_userinfo_username_path: "",
|
||||||
enable_model_fallback: false,
|
enable_model_fallback: false,
|
||||||
fallback_model_anthropic: '',
|
fallback_model_anthropic: "",
|
||||||
fallback_model_openai: '',
|
fallback_model_openai: "",
|
||||||
fallback_model_gemini: '',
|
fallback_model_gemini: "",
|
||||||
fallback_model_antigravity: '',
|
fallback_model_antigravity: "",
|
||||||
enable_identity_patch: false,
|
enable_identity_patch: false,
|
||||||
identity_patch_prompt: '',
|
identity_patch_prompt: "",
|
||||||
ops_monitoring_enabled: false,
|
ops_monitoring_enabled: false,
|
||||||
ops_realtime_monitoring_enabled: false,
|
ops_realtime_monitoring_enabled: false,
|
||||||
ops_query_mode_default: 'auto',
|
ops_query_mode_default: "auto",
|
||||||
ops_metrics_interval_seconds: 60,
|
ops_metrics_interval_seconds: 60,
|
||||||
min_claude_code_version: '',
|
min_claude_code_version: "",
|
||||||
max_claude_code_version: '',
|
max_claude_code_version: "",
|
||||||
allow_ungrouped_key_scheduling: false,
|
allow_ungrouped_key_scheduling: false,
|
||||||
enable_fingerprint_unification: true,
|
enable_fingerprint_unification: true,
|
||||||
enable_metadata_passthrough: false,
|
enable_metadata_passthrough: false,
|
||||||
@@ -262,27 +271,27 @@ const baseSettingsResponse = {
|
|||||||
payment_balance_disabled: false,
|
payment_balance_disabled: false,
|
||||||
payment_balance_recharge_multiplier: 1,
|
payment_balance_recharge_multiplier: 1,
|
||||||
payment_recharge_fee_rate: 0,
|
payment_recharge_fee_rate: 0,
|
||||||
payment_load_balance_strategy: 'round-robin',
|
payment_load_balance_strategy: "round-robin",
|
||||||
payment_product_name_prefix: '',
|
payment_product_name_prefix: "",
|
||||||
payment_product_name_suffix: '',
|
payment_product_name_suffix: "",
|
||||||
payment_help_image_url: '',
|
payment_help_image_url: "",
|
||||||
payment_help_text: '',
|
payment_help_text: "",
|
||||||
payment_cancel_rate_limit_enabled: false,
|
payment_cancel_rate_limit_enabled: false,
|
||||||
payment_cancel_rate_limit_max: 10,
|
payment_cancel_rate_limit_max: 10,
|
||||||
payment_cancel_rate_limit_window: 1,
|
payment_cancel_rate_limit_window: 1,
|
||||||
payment_cancel_rate_limit_unit: 'day',
|
payment_cancel_rate_limit_unit: "day",
|
||||||
payment_cancel_rate_limit_window_mode: 'rolling',
|
payment_cancel_rate_limit_window_mode: "rolling",
|
||||||
payment_visible_method_alipay_source: 'alipay_direct',
|
payment_visible_method_alipay_source: "alipay_direct",
|
||||||
payment_visible_method_wxpay_source: 'invalid-source',
|
payment_visible_method_wxpay_source: "invalid-source",
|
||||||
payment_visible_method_alipay_enabled: true,
|
payment_visible_method_alipay_enabled: true,
|
||||||
payment_visible_method_wxpay_enabled: true,
|
payment_visible_method_wxpay_enabled: true,
|
||||||
openai_advanced_scheduler_enabled: false,
|
openai_advanced_scheduler_enabled: false,
|
||||||
balance_low_notify_enabled: false,
|
balance_low_notify_enabled: false,
|
||||||
balance_low_notify_threshold: 0,
|
balance_low_notify_threshold: 0,
|
||||||
balance_low_notify_recharge_url: '',
|
balance_low_notify_recharge_url: "",
|
||||||
account_quota_notify_enabled: false,
|
account_quota_notify_enabled: false,
|
||||||
account_quota_notify_emails: [],
|
account_quota_notify_emails: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
function mountView() {
|
function mountView() {
|
||||||
return mount(SettingsView, {
|
return mount(SettingsView, {
|
||||||
@@ -302,184 +311,361 @@ function mountView() {
|
|||||||
BackupSettings: true,
|
BackupSettings: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openPaymentTab(wrapper: ReturnType<typeof mountView>) {
|
async function openPaymentTab(wrapper: ReturnType<typeof mountView>) {
|
||||||
const paymentTabButton = wrapper
|
const paymentTabButton = wrapper
|
||||||
.findAll('button')
|
.findAll("button")
|
||||||
.find((node) => node.text().includes('admin.settings.tabs.payment'))
|
.find((node) => node.text().includes("admin.settings.tabs.payment"));
|
||||||
|
|
||||||
expect(paymentTabButton).toBeDefined()
|
expect(paymentTabButton).toBeDefined();
|
||||||
await paymentTabButton?.trigger('click')
|
await paymentTabButton?.trigger("click");
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('admin SettingsView payment visible method controls', () => {
|
async function openSecurityTab(wrapper: ReturnType<typeof mountView>) {
|
||||||
beforeEach(() => {
|
const securityTabButton = wrapper
|
||||||
getSettings.mockReset()
|
.findAll("button")
|
||||||
updateSettings.mockReset()
|
.find((node) => node.text().includes("admin.settings.tabs.security"));
|
||||||
getWebSearchEmulationConfig.mockReset()
|
|
||||||
updateWebSearchEmulationConfig.mockReset()
|
|
||||||
getAdminApiKey.mockReset()
|
|
||||||
getOverloadCooldownSettings.mockReset()
|
|
||||||
getStreamTimeoutSettings.mockReset()
|
|
||||||
getRectifierSettings.mockReset()
|
|
||||||
getBetaPolicySettings.mockReset()
|
|
||||||
getGroups.mockReset()
|
|
||||||
listProxies.mockReset()
|
|
||||||
getProviders.mockReset()
|
|
||||||
fetchPublicSettings.mockReset()
|
|
||||||
adminSettingsFetch.mockReset()
|
|
||||||
showError.mockReset()
|
|
||||||
showSuccess.mockReset()
|
|
||||||
|
|
||||||
getSettings.mockResolvedValue({ ...baseSettingsResponse })
|
expect(securityTabButton).toBeDefined();
|
||||||
|
await securityTabButton?.trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("admin SettingsView payment visible method controls", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getSettings.mockReset();
|
||||||
|
updateSettings.mockReset();
|
||||||
|
getWebSearchEmulationConfig.mockReset();
|
||||||
|
updateWebSearchEmulationConfig.mockReset();
|
||||||
|
getAdminApiKey.mockReset();
|
||||||
|
getOverloadCooldownSettings.mockReset();
|
||||||
|
getStreamTimeoutSettings.mockReset();
|
||||||
|
getRectifierSettings.mockReset();
|
||||||
|
getBetaPolicySettings.mockReset();
|
||||||
|
getGroups.mockReset();
|
||||||
|
listProxies.mockReset();
|
||||||
|
getProviders.mockReset();
|
||||||
|
fetchPublicSettings.mockReset();
|
||||||
|
adminSettingsFetch.mockReset();
|
||||||
|
showError.mockReset();
|
||||||
|
showSuccess.mockReset();
|
||||||
|
|
||||||
|
getSettings.mockResolvedValue({ ...baseSettingsResponse });
|
||||||
updateSettings.mockImplementation(async (payload) => ({
|
updateSettings.mockImplementation(async (payload) => ({
|
||||||
...baseSettingsResponse,
|
...baseSettingsResponse,
|
||||||
...payload,
|
...payload,
|
||||||
}))
|
}));
|
||||||
getWebSearchEmulationConfig.mockResolvedValue({
|
getWebSearchEmulationConfig.mockResolvedValue({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
});
|
||||||
updateWebSearchEmulationConfig.mockResolvedValue({
|
updateWebSearchEmulationConfig.mockResolvedValue({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
});
|
||||||
getAdminApiKey.mockResolvedValue({
|
getAdminApiKey.mockResolvedValue({
|
||||||
exists: false,
|
exists: false,
|
||||||
masked_key: '',
|
masked_key: "",
|
||||||
})
|
});
|
||||||
getOverloadCooldownSettings.mockResolvedValue({
|
getOverloadCooldownSettings.mockResolvedValue({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cooldown_minutes: 10,
|
cooldown_minutes: 10,
|
||||||
})
|
});
|
||||||
getStreamTimeoutSettings.mockResolvedValue({
|
getStreamTimeoutSettings.mockResolvedValue({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
action: 'temp_unsched',
|
action: "temp_unsched",
|
||||||
temp_unsched_minutes: 5,
|
temp_unsched_minutes: 5,
|
||||||
threshold_count: 3,
|
threshold_count: 3,
|
||||||
threshold_window_minutes: 10,
|
threshold_window_minutes: 10,
|
||||||
})
|
});
|
||||||
getRectifierSettings.mockResolvedValue({
|
getRectifierSettings.mockResolvedValue({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
thinking_signature_enabled: true,
|
thinking_signature_enabled: true,
|
||||||
thinking_budget_enabled: true,
|
thinking_budget_enabled: true,
|
||||||
apikey_signature_enabled: false,
|
apikey_signature_enabled: false,
|
||||||
apikey_signature_patterns: [],
|
apikey_signature_patterns: [],
|
||||||
})
|
});
|
||||||
getBetaPolicySettings.mockResolvedValue({
|
getBetaPolicySettings.mockResolvedValue({
|
||||||
rules: [],
|
rules: [],
|
||||||
})
|
});
|
||||||
getGroups.mockResolvedValue([])
|
getGroups.mockResolvedValue([]);
|
||||||
listProxies.mockResolvedValue({
|
listProxies.mockResolvedValue({
|
||||||
items: [],
|
items: [],
|
||||||
})
|
});
|
||||||
getProviders.mockResolvedValue({
|
getProviders.mockResolvedValue({
|
||||||
data: [],
|
data: [],
|
||||||
})
|
});
|
||||||
fetchPublicSettings.mockResolvedValue(undefined)
|
fetchPublicSettings.mockResolvedValue(undefined);
|
||||||
adminSettingsFetch.mockResolvedValue(undefined)
|
adminSettingsFetch.mockResolvedValue(undefined);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('loads canonical source options and normalizes existing values', async () => {
|
it("loads canonical source options and normalizes existing values", async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView();
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
await openPaymentTab(wrapper)
|
await openPaymentTab(wrapper);
|
||||||
|
|
||||||
const paymentSourceSelects = wrapper
|
const paymentSourceSelects = wrapper
|
||||||
.findAll('select.select-stub')
|
.findAll("select.select-stub")
|
||||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
.filter((node) =>
|
||||||
|
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||||
|
);
|
||||||
|
|
||||||
expect(paymentSourceSelects).toHaveLength(2)
|
expect(paymentSourceSelects).toHaveLength(2);
|
||||||
|
|
||||||
const alipaySelect = paymentSourceSelects.find(
|
const alipaySelect = paymentSourceSelects.find(
|
||||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
(node) => node.attributes("data-placeholder") === "alipay",
|
||||||
)
|
);
|
||||||
const wxpaySelect = paymentSourceSelects.find(
|
const wxpaySelect = paymentSourceSelects.find(
|
||||||
(node) => node.attributes('data-placeholder') === 'wxpay'
|
(node) => node.attributes("data-placeholder") === "wxpay",
|
||||||
)
|
);
|
||||||
|
|
||||||
expect(alipaySelect?.element.value).toBe('official_alipay')
|
expect(alipaySelect?.element.value).toBe("official_alipay");
|
||||||
expect(alipaySelect?.findAll('option').map((option) => option.element.value)).toEqual([
|
expect(
|
||||||
'',
|
alipaySelect?.findAll("option").map((option) => option.element.value),
|
||||||
'official_alipay',
|
).toEqual(["", "official_alipay", "easypay_alipay"]);
|
||||||
'easypay_alipay',
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(wxpaySelect?.element.value).toBe('')
|
expect(wxpaySelect?.element.value).toBe("");
|
||||||
expect(wxpaySelect?.findAll('option').map((option) => option.element.value)).toEqual([
|
expect(
|
||||||
'',
|
wxpaySelect?.findAll("option").map((option) => option.element.value),
|
||||||
'official_wxpay',
|
).toEqual(["", "official_wxpay", "easypay_wxpay"]);
|
||||||
'easypay_wxpay',
|
});
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('saves canonical source keys selected from the dropdowns', async () => {
|
it("saves canonical source keys selected from the dropdowns", async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView();
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
await openPaymentTab(wrapper)
|
await openPaymentTab(wrapper);
|
||||||
|
|
||||||
const paymentSourceSelects = wrapper
|
const paymentSourceSelects = wrapper
|
||||||
.findAll('select.select-stub')
|
.findAll("select.select-stub")
|
||||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
.filter((node) =>
|
||||||
|
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||||
|
);
|
||||||
|
|
||||||
const alipaySelect = paymentSourceSelects.find(
|
const alipaySelect = paymentSourceSelects.find(
|
||||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
(node) => node.attributes("data-placeholder") === "alipay",
|
||||||
)
|
);
|
||||||
const wxpaySelect = paymentSourceSelects.find(
|
const wxpaySelect = paymentSourceSelects.find(
|
||||||
(node) => node.attributes('data-placeholder') === 'wxpay'
|
(node) => node.attributes("data-placeholder") === "wxpay",
|
||||||
)
|
);
|
||||||
|
|
||||||
await alipaySelect?.setValue('easypay_alipay')
|
await alipaySelect?.setValue("easypay_alipay");
|
||||||
await wxpaySelect?.setValue('official_wxpay')
|
await wxpaySelect?.setValue("official_wxpay");
|
||||||
await wrapper.find('form').trigger('submit.prevent')
|
await wrapper.find("form").trigger("submit.prevent");
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
|
|
||||||
expect(updateSettings).toHaveBeenCalledTimes(1)
|
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||||
expect(updateSettings).toHaveBeenCalledWith(
|
expect(updateSettings).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
payment_visible_method_alipay_source: 'easypay_alipay',
|
payment_visible_method_alipay_source: "easypay_alipay",
|
||||||
payment_visible_method_wxpay_source: 'official_wxpay',
|
payment_visible_method_wxpay_source: "official_wxpay",
|
||||||
payment_visible_method_alipay_enabled: true,
|
payment_visible_method_alipay_enabled: true,
|
||||||
payment_visible_method_wxpay_enabled: true,
|
payment_visible_method_wxpay_enabled: true,
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('blocks saving when a visible payment method is enabled without a source', async () => {
|
it("blocks saving when a visible payment method is enabled without a source", async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView();
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
await openPaymentTab(wrapper)
|
await openPaymentTab(wrapper);
|
||||||
|
|
||||||
const paymentSourceSelects = wrapper
|
const paymentSourceSelects = wrapper
|
||||||
.findAll('select.select-stub')
|
.findAll("select.select-stub")
|
||||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
.filter((node) =>
|
||||||
|
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||||
|
);
|
||||||
|
|
||||||
const alipaySelect = paymentSourceSelects.find(
|
const alipaySelect = paymentSourceSelects.find(
|
||||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
(node) => node.attributes("data-placeholder") === "alipay",
|
||||||
)
|
);
|
||||||
|
|
||||||
await alipaySelect?.setValue('')
|
await alipaySelect?.setValue("");
|
||||||
await wrapper.find('form').trigger('submit.prevent')
|
await wrapper.find("form").trigger("submit.prevent");
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
|
|
||||||
expect(updateSettings).not.toHaveBeenCalled()
|
expect(updateSettings).not.toHaveBeenCalled();
|
||||||
expect(showError).toHaveBeenCalled()
|
expect(showError).toHaveBeenCalled();
|
||||||
expect(String(showError.mock.calls.at(-1)?.[0] ?? '')).toContain('支付来源')
|
expect(String(showError.mock.calls.at(-1)?.[0] ?? "")).toContain(
|
||||||
})
|
"支付来源",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders advanced scheduler copy as local experimental gateway policy', async () => {
|
it("renders advanced scheduler copy as local experimental gateway policy", async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView();
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises();
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('OpenAI 实验调度策略')
|
expect(wrapper.text()).toContain("OpenAI 实验调度策略");
|
||||||
expect(wrapper.text()).toContain('默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑')
|
expect(wrapper.text()).toContain(
|
||||||
expect(wrapper.text()).not.toContain('OpenAI 高级调度器')
|
"默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑",
|
||||||
})
|
);
|
||||||
})
|
expect(wrapper.text()).not.toContain("OpenAI 高级调度器");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("admin SettingsView wechat connect controls", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getSettings.mockReset();
|
||||||
|
updateSettings.mockReset();
|
||||||
|
getWebSearchEmulationConfig.mockReset();
|
||||||
|
updateWebSearchEmulationConfig.mockReset();
|
||||||
|
getAdminApiKey.mockReset();
|
||||||
|
getOverloadCooldownSettings.mockReset();
|
||||||
|
getStreamTimeoutSettings.mockReset();
|
||||||
|
getRectifierSettings.mockReset();
|
||||||
|
getBetaPolicySettings.mockReset();
|
||||||
|
getGroups.mockReset();
|
||||||
|
listProxies.mockReset();
|
||||||
|
getProviders.mockReset();
|
||||||
|
fetchPublicSettings.mockReset();
|
||||||
|
adminSettingsFetch.mockReset();
|
||||||
|
showError.mockReset();
|
||||||
|
showSuccess.mockReset();
|
||||||
|
|
||||||
|
getSettings.mockResolvedValue({
|
||||||
|
...baseSettingsResponse,
|
||||||
|
payment_visible_method_wxpay_source: "official_wxpay",
|
||||||
|
});
|
||||||
|
updateSettings.mockImplementation(async (payload) => ({
|
||||||
|
...baseSettingsResponse,
|
||||||
|
payment_visible_method_wxpay_source: "official_wxpay",
|
||||||
|
...payload,
|
||||||
|
}));
|
||||||
|
getWebSearchEmulationConfig.mockResolvedValue({
|
||||||
|
enabled: false,
|
||||||
|
providers: [],
|
||||||
|
});
|
||||||
|
updateWebSearchEmulationConfig.mockResolvedValue({
|
||||||
|
enabled: false,
|
||||||
|
providers: [],
|
||||||
|
});
|
||||||
|
getAdminApiKey.mockResolvedValue({
|
||||||
|
exists: false,
|
||||||
|
masked_key: "",
|
||||||
|
});
|
||||||
|
getOverloadCooldownSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
cooldown_minutes: 10,
|
||||||
|
});
|
||||||
|
getStreamTimeoutSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
action: "temp_unsched",
|
||||||
|
temp_unsched_minutes: 5,
|
||||||
|
threshold_count: 3,
|
||||||
|
threshold_window_minutes: 10,
|
||||||
|
});
|
||||||
|
getRectifierSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
thinking_signature_enabled: true,
|
||||||
|
thinking_budget_enabled: true,
|
||||||
|
apikey_signature_enabled: false,
|
||||||
|
apikey_signature_patterns: [],
|
||||||
|
});
|
||||||
|
getBetaPolicySettings.mockResolvedValue({
|
||||||
|
rules: [],
|
||||||
|
});
|
||||||
|
getGroups.mockResolvedValue([]);
|
||||||
|
listProxies.mockResolvedValue({
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
getProviders.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
fetchPublicSettings.mockResolvedValue(undefined);
|
||||||
|
adminSettingsFetch.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads and echoes WeChat Connect fields from the backend payload", async () => {
|
||||||
|
const wrapper = mountView();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await openSecurityTab(wrapper);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
wrapper.get('[data-testid="wechat-connect-app-id"]')
|
||||||
|
.element as HTMLInputElement
|
||||||
|
).value,
|
||||||
|
).toBe("wx-app-id-123");
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
wrapper.get('[data-testid="wechat-connect-mode"]')
|
||||||
|
.element as HTMLSelectElement
|
||||||
|
).value,
|
||||||
|
).toBe("mp");
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
wrapper.get('[data-testid="wechat-connect-scopes"]')
|
||||||
|
.element as HTMLInputElement
|
||||||
|
).value,
|
||||||
|
).toBe("snsapi_userinfo");
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.get('[data-testid="wechat-connect-app-secret"]')
|
||||||
|
.attributes("placeholder"),
|
||||||
|
).toContain("密钥已配置");
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
wrapper.get('[data-testid="wechat-connect-frontend-redirect-url"]')
|
||||||
|
.element as HTMLInputElement
|
||||||
|
).value,
|
||||||
|
).toBe("/auth/wechat/callback");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves WeChat Connect fields using the backend contract and clears the secret after save", async () => {
|
||||||
|
const wrapper = mountView();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await openSecurityTab(wrapper);
|
||||||
|
|
||||||
|
await wrapper
|
||||||
|
.get('[data-testid="wechat-connect-app-id"]')
|
||||||
|
.setValue("wx-app-id-updated");
|
||||||
|
await wrapper
|
||||||
|
.get('[data-testid="wechat-connect-app-secret"]')
|
||||||
|
.setValue("new-secret");
|
||||||
|
await wrapper.get('[data-testid="wechat-connect-mode"]').setValue("open");
|
||||||
|
await wrapper
|
||||||
|
.get('[data-testid="wechat-connect-scopes"]')
|
||||||
|
.setValue(" snsapi_base ");
|
||||||
|
await wrapper
|
||||||
|
.get('[data-testid="wechat-connect-redirect-url"]')
|
||||||
|
.setValue("https://admin.example.com/api/v1/auth/oauth/wechat/callback");
|
||||||
|
await wrapper
|
||||||
|
.get('[data-testid="wechat-connect-frontend-redirect-url"]')
|
||||||
|
.setValue("/auth/wechat/callback");
|
||||||
|
await wrapper.find("form").trigger("submit.prevent");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
expect(updateSettings).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
wechat_connect_enabled: true,
|
||||||
|
wechat_connect_app_id: "wx-app-id-updated",
|
||||||
|
wechat_connect_app_secret: "new-secret",
|
||||||
|
wechat_connect_mode: "open",
|
||||||
|
wechat_connect_scopes: "snsapi_base",
|
||||||
|
wechat_connect_redirect_url:
|
||||||
|
"https://admin.example.com/api/v1/auth/oauth/wechat/callback",
|
||||||
|
wechat_connect_frontend_redirect_url: "/auth/wechat/callback",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
wrapper.get('[data-testid="wechat-connect-app-secret"]')
|
||||||
|
.element as HTMLInputElement
|
||||||
|
).value,
|
||||||
|
).toBe("");
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.get('[data-testid="wechat-connect-app-secret"]')
|
||||||
|
.attributes("placeholder"),
|
||||||
|
).toContain("密钥已配置");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user