mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-06 08:49:24 +00:00
新增 Grok Web SSO → xAI Device Flow → Grok Build OAuth 导入链路, 支持管理员批量粘贴 SSO key 创建 OAuth 账号。 - 后端:ConvertSSOToBuild、ConvertFromSSO、POST /admin/grok/sso-to-oauth - 批量:3 worker 并发,失败跳过并汇总 created/failed,worker panic recover - 无 refresh_token 时写入 expires_at 并强制 auto_pause_on_expired - 前端:SSO Cookie 导入入口、动态超时、中英文案、部分成功不关弹窗 - 测试:pkg/service/handler/前端超时单测;本地 Docker 真实 SSO e2e 通过
117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type grokOAuthClientStub struct {
|
|
refreshResponse *xai.TokenResponse
|
|
ssoResponse *xai.TokenResponse
|
|
exchangeCalls int
|
|
}
|
|
|
|
func (s *grokOAuthClientStub) ExchangeCode(context.Context, string, string, string, string, string) (*xai.TokenResponse, error) {
|
|
s.exchangeCalls++
|
|
return &xai.TokenResponse{}, nil
|
|
}
|
|
|
|
func (s *grokOAuthClientStub) RefreshToken(context.Context, string, string, string) (*xai.TokenResponse, error) {
|
|
return s.refreshResponse, nil
|
|
}
|
|
|
|
func (s *grokOAuthClientStub) ConvertSSOToBuild(context.Context, string, string) (*xai.TokenResponse, error) {
|
|
return s.ssoResponse, nil
|
|
}
|
|
|
|
func TestGrokOAuthServiceRefreshTokenPreservesOriginalRefreshTokenWhenNotRotated(t *testing.T) {
|
|
svc := NewGrokOAuthService(nil, &grokOAuthClientStub{
|
|
refreshResponse: &xai.TokenResponse{
|
|
AccessToken: "new-access-token",
|
|
TokenType: "Bearer",
|
|
ExpiresIn: 3600,
|
|
},
|
|
})
|
|
defer svc.Stop()
|
|
|
|
info, err := svc.RefreshToken(context.Background(), "original-refresh-token", "", "client-id")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "new-access-token", info.AccessToken)
|
|
require.Equal(t, "original-refresh-token", info.RefreshToken)
|
|
require.Equal(t, "client-id", info.ClientID)
|
|
}
|
|
|
|
func TestGrokOAuthServiceExchangeCodeRequiresStateForCallbackURLAndConsumesSession(t *testing.T) {
|
|
client := &grokOAuthClientStub{}
|
|
svc := NewGrokOAuthService(nil, client)
|
|
defer svc.Stop()
|
|
|
|
auth, err := svc.GenerateAuthURL(context.Background(), nil, "")
|
|
require.NoError(t, err)
|
|
|
|
_, err = svc.ExchangeCode(context.Background(), &GrokExchangeCodeInput{
|
|
SessionID: auth.SessionID,
|
|
Code: "http://127.0.0.1:56121/callback?code=code-without-state",
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "GROK_OAUTH_STATE_REQUIRED")
|
|
require.Zero(t, client.exchangeCalls)
|
|
|
|
_, err = svc.ExchangeCode(context.Background(), &GrokExchangeCodeInput{
|
|
SessionID: auth.SessionID,
|
|
Code: "code-with-state",
|
|
State: auth.State,
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "GROK_OAUTH_SESSION_NOT_FOUND")
|
|
require.Zero(t, client.exchangeCalls)
|
|
}
|
|
|
|
func TestGrokOAuthServiceBuildAccountCredentialsDefaultsToSubscriptionProxy(t *testing.T) {
|
|
svc := NewGrokOAuthService(nil, &grokOAuthClientStub{})
|
|
defer svc.Stop()
|
|
|
|
credentials := svc.BuildAccountCredentials(&GrokTokenInfo{
|
|
AccessToken: "access-token",
|
|
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
})
|
|
|
|
require.Equal(t, xai.DefaultCLIBaseURL, credentials["base_url"])
|
|
}
|
|
|
|
func TestGrokOAuthServiceConvertFromSSOExtractsBuildClaims(t *testing.T) {
|
|
svc := NewGrokOAuthService(nil, &grokOAuthClientStub{
|
|
ssoResponse: &xai.TokenResponse{
|
|
AccessToken: makeGrokOAuthJWT(map[string]any{"sub": "user-sub", "team_id": "team-1"}),
|
|
RefreshToken: "refresh-token",
|
|
IDToken: makeGrokOAuthJWT(map[string]any{"email": "user@example.com"}),
|
|
ExpiresIn: 3600,
|
|
},
|
|
})
|
|
defer svc.Stop()
|
|
|
|
info, err := svc.ConvertFromSSO(context.Background(), "sso-token", nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "user@example.com", info.Email)
|
|
require.Equal(t, "user-sub", info.Subject)
|
|
require.Equal(t, "team-1", info.TeamID)
|
|
|
|
credentials := svc.BuildAccountCredentials(info)
|
|
require.Equal(t, "user@example.com", credentials["email"])
|
|
require.Equal(t, "user-sub", credentials["sub"])
|
|
require.Equal(t, "team-1", credentials["team_id"])
|
|
}
|
|
|
|
func makeGrokOAuthJWT(claims map[string]any) string {
|
|
payload, _ := json.Marshal(claims)
|
|
return "header." + base64.RawURLEncoding.EncodeToString(payload) + ".signature"
|
|
}
|