瀏覽代碼

sdk添加登录

qdy 3 週之前
父節點
當前提交
eb3c2c6203

+ 214
- 0
cmd/demo-login/main.go 查看文件

@@ -0,0 +1,214 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"time"
11
+
12
+	"git.x2erp.com/qdy/go-base/sdk/configure"
13
+)
14
+
15
+func main() {
16
+	fmt.Println("=== 演示登录功能测试 ===")
17
+
18
+	// 配置服务地址
19
+	configureURL := "http://localhost:8080"
20
+	svcCodeURL := "http://localhost:8020"
21
+
22
+	fmt.Printf("配置中心地址: %s\n", configureURL)
23
+	fmt.Printf("svc-code地址: %s\n", svcCodeURL)
24
+
25
+	// 测试凭证
26
+	userID := "test-user-001"
27
+	password := "password123"
28
+
29
+	fmt.Printf("\n使用测试用户: %s\n", userID)
30
+
31
+	// 1. 直接使用SDK登录配置中心
32
+	fmt.Println("\n--- 方法1: 直接使用SDK登录配置中心 ---")
33
+	token1, err := loginWithSDK(configureURL, userID, password)
34
+	if err != nil {
35
+		fmt.Printf("SDK登录失败: %v\n", err)
36
+	} else {
37
+		fmt.Printf("✅ SDK登录成功!\n")
38
+		fmt.Printf("Token (前100字符): %s...\n", token1[:min(100, len(token1))])
39
+		fmt.Printf("Token长度: %d 字符\n", len(token1))
40
+
41
+		// 验证token
42
+		if err := validateTokenWithSDK(configureURL, token1); err != nil {
43
+			fmt.Printf("Token验证失败: %v\n", err)
44
+		} else {
45
+			fmt.Printf("✅ Token验证通过\n")
46
+		}
47
+	}
48
+
49
+	// 2. 通过svc-code API登录
50
+	fmt.Println("\n--- 方法2: 通过svc-code API登录 ---")
51
+	token2, err := loginWithAPI(svcCodeURL, userID, password)
52
+	if err != nil {
53
+		fmt.Printf("API登录失败: %v\n", err)
54
+	} else {
55
+		fmt.Printf("✅ API登录成功!\n")
56
+		fmt.Printf("Token (前100字符): %s...\n", token2[:min(100, len(token2))])
57
+		fmt.Printf("Token长度: %d 字符\n", len(token2))
58
+
59
+		// 比较两个token是否相同
60
+		if token1 != "" && token2 != "" {
61
+			if token1 == token2 {
62
+				fmt.Printf("✅ 两个方法返回的Token相同\n")
63
+			} else {
64
+				fmt.Printf("⚠️  两个方法返回的Token不同(可能是不同的JWT有效期)\n")
65
+			}
66
+		}
67
+	}
68
+
69
+	// 3. 测试无效凭证
70
+	fmt.Println("\n--- 测试无效凭证 ---")
71
+	testInvalidCredentials(svcCodeURL)
72
+
73
+	fmt.Println("\n=== 测试完成 ===")
74
+}
75
+
76
+// loginWithSDK 使用SDK直接登录配置中心
77
+func loginWithSDK(configureURL, userID, password string) (string, error) {
78
+	// 创建SDK客户端
79
+	client, err := configure.NewBasicAuthClient(configureURL, "test", "test")
80
+	if err != nil {
81
+		return "", fmt.Errorf("创建SDK客户端失败: %w", err)
82
+	}
83
+
84
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
85
+	defer cancel()
86
+
87
+	req := &configure.UserLoginRequest{
88
+		UserID:   userID,
89
+		Password: password,
90
+	}
91
+
92
+	startTime := time.Now()
93
+	token, err := client.LoginUser(ctx, req)
94
+	elapsed := time.Since(startTime)
95
+
96
+	if err != nil {
97
+		return "", fmt.Errorf("登录失败: %w (耗时: %v)", err, elapsed)
98
+	}
99
+
100
+	fmt.Printf("登录耗时: %v\n", elapsed)
101
+	return token, nil
102
+}
103
+
104
+// validateTokenWithSDK 使用SDK验证token
105
+func validateTokenWithSDK(configureURL, token string) error {
106
+	// 使用token创建配置中心客户端
107
+	client, err := configure.NewTokenAuthClient(configureURL, token)
108
+	if err != nil {
109
+		return fmt.Errorf("创建Token客户端失败: %w", err)
110
+	}
111
+
112
+	fmt.Printf("Token客户端创建成功,BaseURL: %s\n", client.GetConfig().BaseURL)
113
+	return nil
114
+}
115
+
116
+// loginWithAPI 通过svc-code API登录
117
+func loginWithAPI(svcCodeURL, userID, password string) (string, error) {
118
+	client := &http.Client{
119
+		Timeout: 30 * time.Second,
120
+	}
121
+
122
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
123
+	loginData := map[string]string{
124
+		"user_id":  userID,
125
+		"password": password,
126
+	}
127
+
128
+	loginJSON, err := json.Marshal(loginData)
129
+	if err != nil {
130
+		return "", fmt.Errorf("JSON序列化失败: %w", err)
131
+	}
132
+
133
+	startTime := time.Now()
134
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(loginJSON))
135
+	if err != nil {
136
+		return "", fmt.Errorf("创建HTTP请求失败: %w", err)
137
+	}
138
+	req.Header.Set("Content-Type", "application/json")
139
+
140
+	resp, err := client.Do(req)
141
+	if err != nil {
142
+		return "", fmt.Errorf("发送登录请求失败: %w", err)
143
+	}
144
+	defer resp.Body.Close()
145
+
146
+	elapsed := time.Since(startTime)
147
+	fmt.Printf("API调用耗时: %v\n", elapsed)
148
+
149
+	if resp.StatusCode != 200 {
150
+		body, _ := io.ReadAll(resp.Body)
151
+		return "", fmt.Errorf("API返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
152
+	}
153
+
154
+	body, err := io.ReadAll(resp.Body)
155
+	if err != nil {
156
+		return "", fmt.Errorf("读取响应体失败: %w", err)
157
+	}
158
+
159
+	var result map[string]interface{}
160
+	if err := json.Unmarshal(body, &result); err != nil {
161
+		return "", fmt.Errorf("解析响应JSON失败: %w, 响应体: %s", err, string(body))
162
+	}
163
+
164
+	if !result["success"].(bool) {
165
+		return "", fmt.Errorf("登录失败: %v", result)
166
+	}
167
+
168
+	token, ok := result["data"].(string)
169
+	if !ok || token == "" {
170
+		return "", fmt.Errorf("响应中未找到有效token: %v", result)
171
+	}
172
+
173
+	return token, nil
174
+}
175
+
176
+// testInvalidCredentials 测试无效凭证
177
+func testInvalidCredentials(svcCodeURL string) {
178
+	client := &http.Client{
179
+		Timeout: 10 * time.Second,
180
+	}
181
+
182
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
183
+	invalidData := map[string]string{
184
+		"user_id":  "invalid-user",
185
+		"password": "wrong-password",
186
+	}
187
+
188
+	invalidJSON, _ := json.Marshal(invalidData)
189
+
190
+	resp, err := client.Post(loginURL, "application/json", bytes.NewReader(invalidJSON))
191
+	if err != nil {
192
+		fmt.Printf("无效凭证测试失败: %v\n", err)
193
+		return
194
+	}
195
+	defer resp.Body.Close()
196
+
197
+	body, _ := io.ReadAll(resp.Body)
198
+	var result map[string]interface{}
199
+	json.Unmarshal(body, &result)
200
+
201
+	if !result["success"].(bool) {
202
+		fmt.Printf("✅ 无效凭证登录失败(预期): %s\n", result["message"])
203
+	} else {
204
+		fmt.Printf("❌ 无效凭证登录不应该成功\n")
205
+	}
206
+}
207
+
208
+// min 返回两个整数的最小值
209
+func min(a, b int) int {
210
+	if a < b {
211
+		return a
212
+	}
213
+	return b
214
+}

+ 64
- 0
internal/routes/auth_routes.go 查看文件

@@ -0,0 +1,64 @@
1
+package routes
2
+
3
+import (
4
+	"context"
5
+
6
+	"git.x2erp.com/qdy/go-base/authbase"
7
+	"git.x2erp.com/qdy/go-base/ctx"
8
+	"git.x2erp.com/qdy/go-base/model/response"
9
+	"git.x2erp.com/qdy/go-base/sdk/configure"
10
+	"git.x2erp.com/qdy/go-base/webx/router"
11
+)
12
+
13
+// RegisterAuthRoutes 注册认证路由
14
+func RegisterAuthRoutes(ws *router.RouterService, configClient *configure.Client) {
15
+	// 用户登录(公开端点,无需认证)
16
+	ws.POST("/api/auth/login",
17
+		func(req *configure.UserLoginRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[string], error) {
18
+			token, err := configClient.LoginUser(ctx, req)
19
+			if err != nil {
20
+				return &response.QueryResult[string]{
21
+					Success: false,
22
+					Message: err.Error(),
23
+				}, nil
24
+			}
25
+
26
+			return &response.QueryResult[string]{
27
+				Success: true,
28
+				Data:    token,
29
+			}, nil
30
+		},
31
+	).Desc("用户登录(返回配置中心token)").Register()
32
+
33
+	// Token验证端点(需要Token认证,用于测试token有效性)
34
+	ws.POST("/api/auth/validate",
35
+		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
36
+			// 如果请求能到达这里,说明TokenAuth中间件已经验证了token
37
+			// 返回当前用户信息
38
+			userInfo := map[string]interface{}{
39
+				"user_id":       reqCtx.UserID,
40
+				"username":      reqCtx.Username,
41
+				"tenant_id":     reqCtx.TenantID,
42
+				"project_id":    reqCtx.ProjectID,
43
+				"authenticated": true,
44
+			}
45
+			return &response.QueryResult[map[string]interface{}]{
46
+				Success: true,
47
+				Data:    userInfo,
48
+			}, nil
49
+		},
50
+	).Use(authbase.TokenAuth).Desc("验证token有效性并返回用户信息").Register()
51
+
52
+	// 健康检查端点(公开)
53
+	ws.GET("/api/auth/health",
54
+		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
55
+			return &response.QueryResult[map[string]interface{}]{
56
+				Success: true,
57
+				Data: map[string]interface{}{
58
+					"status":  "healthy",
59
+					"service": "svc-code-auth",
60
+				},
61
+			}, nil
62
+		},
63
+	).Desc("认证服务健康检查").Register()
64
+}

+ 7
- 2
internal/routes/log_stream_routes.go 查看文件

@@ -5,6 +5,7 @@ import (
5 5
 	"net/http"
6 6
 	"time"
7 7
 
8
+	"git.x2erp.com/qdy/go-base/authbase"
8 9
 	"git.x2erp.com/qdy/go-base/webx/router"
9 10
 	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
10 11
 )
@@ -17,9 +18,10 @@ func RegisterLogStreamRoutes(ws *router.RouterService, process *opencode.Process
17 18
 	// 这个函数将由 main.go 中的额外注册调用
18 19
 }
19 20
 
20
-// LogStreamHandler 日志流的 HTTP 处理器
21
+// LogStreamHandler 日志流的 HTTP 处理器(已包含TokenAuth认证)
21 22
 func LogStreamHandler(process *opencode.Process) http.HandlerFunc {
22
-	return func(w http.ResponseWriter, r *http.Request) {
23
+	// 创建内部处理器
24
+	handler := func(w http.ResponseWriter, r *http.Request) {
23 25
 		// 设置 SSE 头
24 26
 		w.Header().Set("Content-Type", "text/event-stream")
25 27
 		w.Header().Set("Cache-Control", "no-cache")
@@ -66,4 +68,7 @@ func LogStreamHandler(process *opencode.Process) http.HandlerFunc {
66 68
 			}
67 69
 		}
68 70
 	}
71
+
72
+	// 包装TokenAuth中间件
73
+	return authbase.TokenAuth(http.HandlerFunc(handler)).ServeHTTP
69 74
 }

+ 9
- 4
internal/routes/prompt_stream_routes.go 查看文件

@@ -8,6 +8,7 @@ import (
8 8
 	"net/http"
9 9
 	"time"
10 10
 
11
+	"git.x2erp.com/qdy/go-base/authbase"
11 12
 	"git.x2erp.com/qdy/go-base/ctx"
12 13
 	"git.x2erp.com/qdy/go-base/model/response"
13 14
 	"git.x2erp.com/qdy/go-base/webx/router"
@@ -24,7 +25,7 @@ type PromptStreamRequest struct {
24 25
 
25 26
 // RegisterPromptStreamRoutes 注册流式对话路由
26 27
 func RegisterPromptStreamRoutes(ws *router.RouterService, client *opencode.Client) {
27
-	// 流式对话
28
+	// 流式对话(需要Token认证)
28 29
 	ws.POST("/api/prompt/stream",
29 30
 		func(req *PromptStreamRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
30 31
 			// 流式响应需要直接写入 HTTP 响应,不能使用标准的路由返回值
@@ -34,7 +35,7 @@ func RegisterPromptStreamRoutes(ws *router.RouterService, client *opencode.Clien
34 35
 				Message: "流式端点需要特殊处理",
35 36
 			}, nil
36 37
 		},
37
-	).Desc("流式对话(Server-Sent Events)").Register()
38
+	).Use(authbase.TokenAuth).Desc("流式对话(Server-Sent Events)").Register()
38 39
 
39 40
 	// 流式对话的原始 HTTP 处理器
40 41
 	// 注意:这个路由需要直接处理 HTTP 流,不能使用标准的 router 包装
@@ -42,9 +43,10 @@ func RegisterPromptStreamRoutes(ws *router.RouterService, client *opencode.Clien
42 43
 	// 这个函数将由 main.go 中的额外注册调用
43 44
 }
44 45
 
45
-// StreamPromptHandler 流式对话的 HTTP 处理器
46
+// StreamPromptHandler 流式对话的 HTTP 处理器(已包含TokenAuth认证)
46 47
 func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
47
-	return func(w http.ResponseWriter, r *http.Request) {
48
+	// 创建内部处理器
49
+	handler := func(w http.ResponseWriter, r *http.Request) {
48 50
 		// 解析请求
49 51
 		var req PromptStreamRequest
50 52
 		if err := BindJSON(r, &req); err != nil {
@@ -102,6 +104,9 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
102 104
 			}
103 105
 		}
104 106
 	}
107
+
108
+	// 包装TokenAuth中间件
109
+	return authbase.TokenAuth(http.HandlerFunc(handler)).ServeHTTP
105 110
 }
106 111
 
107 112
 // BindJSON 简单的 JSON 绑定函数

+ 3
- 2
internal/routes/prompt_sync_routes.go 查看文件

@@ -3,6 +3,7 @@ package routes
3 3
 import (
4 4
 	"context"
5 5
 
6
+	"git.x2erp.com/qdy/go-base/authbase"
6 7
 	"git.x2erp.com/qdy/go-base/ctx"
7 8
 	"git.x2erp.com/qdy/go-base/model/response"
8 9
 	"git.x2erp.com/qdy/go-base/webx/router"
@@ -25,7 +26,7 @@ type PromptSyncResponse struct {
25 26
 
26 27
 // RegisterPromptSyncRoutes 注册同步对话路由
27 28
 func RegisterPromptSyncRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
28
-	// 同步对话
29
+	// 同步对话(需要Token认证)
29 30
 	ws.POST("/api/prompt/sync",
30 31
 		func(req *PromptSyncRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[PromptSyncResponse], error) {
31 32
 			prompt := &opencode.PromptRequest{
@@ -50,5 +51,5 @@ func RegisterPromptSyncRoutes(ws *router.RouterService, client opencode.OpenCode
50 51
 				},
51 52
 			}, nil
52 53
 		},
53
-	).Desc("同步对话(阻塞等待完整响应)").Register()
54
+	).Use(authbase.TokenAuth).Desc("同步对话(阻塞等待完整响应)").Register()
54 55
 }

+ 5
- 4
internal/routes/session_routes.go 查看文件

@@ -3,6 +3,7 @@ package routes
3 3
 import (
4 4
 	"context"
5 5
 
6
+	"git.x2erp.com/qdy/go-base/authbase"
6 7
 	"git.x2erp.com/qdy/go-base/ctx"
7 8
 	"git.x2erp.com/qdy/go-base/model/response"
8 9
 	"git.x2erp.com/qdy/go-base/webx/router"
@@ -24,7 +25,7 @@ type SessionResponse struct {
24 25
 
25 26
 // RegisterSessionRoutes 注册会话管理路由
26 27
 func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
27
-	// 创建会话
28
+	// 创建会话(需要Token认证)
28 29
 	ws.POST("/api/session/create",
29 30
 		func(req *SessionCreateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[SessionResponse], error) {
30 31
 			session, err := client.CreateSession(ctx, req.Title)
@@ -45,9 +46,9 @@ func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeCli
45 46
 				},
46 47
 			}, nil
47 48
 		},
48
-	).Desc("创建新的 opencode 会话").Register()
49
+	).Use(authbase.TokenAuth).Desc("创建新的 opencode 会话").Register()
49 50
 
50
-	// 获取会话列表
51
+	// 获取会话列表(需要Token认证)
51 52
 	ws.GET("/api/session/list",
52 53
 		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[[]opencode.Session], error) {
53 54
 			sessions, err := client.ListSessions(ctx)
@@ -63,7 +64,7 @@ func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeCli
63 64
 				Data:    sessions,
64 65
 			}, nil
65 66
 		},
66
-	).Desc("获取 opencode 会话列表").Register()
67
+	).Use(authbase.TokenAuth).Desc("获取 opencode 会话列表").Register()
67 68
 
68 69
 	// 获取单个会话(暂不实现,因为 opencode API 可能不支持)
69 70
 	// ws.GET("/api/session/get",

+ 18
- 2
main.go 查看文件

@@ -14,6 +14,7 @@ import (
14 14
 	"git.x2erp.com/qdy/go-base/graceful"
15 15
 	"git.x2erp.com/qdy/go-base/logger"
16 16
 	"git.x2erp.com/qdy/go-base/model/response"
17
+	"git.x2erp.com/qdy/go-base/sdk/configure"
17 18
 	"git.x2erp.com/qdy/go-base/webx"
18 19
 	"git.x2erp.com/qdy/go-base/webx/router"
19 20
 
@@ -72,6 +73,16 @@ func main() {
72 73
 		log.Fatalf("创建 opencode 客户端失败: %v", err)
73 74
 	}
74 75
 
76
+	// 4.1 创建配置中心客户端
77
+	var configClient *configure.Client
78
+	configClient, err = configure.NewClient()
79
+	if err != nil {
80
+		log.Printf("警告: 创建配置中心客户端失败,登录功能将不可用: %v", err)
81
+		configClient = nil
82
+	} else {
83
+		log.Printf("配置中心客户端创建成功,BaseURL: %s", configClient.GetConfig().BaseURL)
84
+	}
85
+
75 86
 	// 5. 得到 webservice 服务工厂
76 87
 	webxFactory := webx.GetWebServiceFactory()
77 88
 
@@ -85,7 +96,7 @@ func main() {
85 96
 	routerService := router.NewWebService(webService.GetRouter())
86 97
 
87 98
 	// 8. 注册路由--api
88
-	registerRoutes(routerService, client, webService)
99
+	registerRoutes(routerService, client, configClient, webService)
89 100
 
90 101
 	// 9. 注册前端静态文件服务
91 102
 	registerStaticFiles(webService)
@@ -159,7 +170,7 @@ func waitForOpenCodeReady(port int) error {
159 170
 }
160 171
 
161 172
 // registerRoutes 注册所有 API 路由
162
-func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, webService *webx.WebService) {
173
+func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, configClient *configure.Client, webService *webx.WebService) {
163 174
 	// 会话管理路由
164 175
 	routes.RegisterSessionRoutes(ws, client)
165 176
 
@@ -174,6 +185,11 @@ func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, we
174 185
 		webService.GetRouter().Handle("/api/logs/stream", routes.LogStreamHandler(opencodeProcess))
175 186
 	}
176 187
 
188
+	// 认证路由
189
+	if configClient != nil {
190
+		routes.RegisterAuthRoutes(ws, configClient)
191
+	}
192
+
177 193
 	// 健康检查路由
178 194
 	ws.GET("/api/health", func(reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
179 195
 		result := map[string]interface{}{

+ 341
- 0
test/auth_test.go 查看文件

@@ -0,0 +1,341 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"os/exec"
11
+	"testing"
12
+	"time"
13
+
14
+	"git.x2erp.com/qdy/go-base/model/request/queryreq"
15
+	"git.x2erp.com/qdy/go-base/sdk/configure"
16
+)
17
+
18
+// TestAuthLogin 测试认证登录功能
19
+func TestAuthLogin(t *testing.T) {
20
+	// 清理测试缓存
21
+	cleanTestCache(t)
22
+
23
+	// 获取svc-code服务地址
24
+	svcCodeURL := "http://localhost:8020"
25
+
26
+	// 检查svc-code服务是否运行
27
+	if !isServiceRunning(t, svcCodeURL) {
28
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
29
+	}
30
+
31
+	// 检查配置中心服务是否运行
32
+	configureURL := "http://localhost:8080"
33
+	if !isServiceRunning(t, configureURL) {
34
+		t.Skipf("配置中心服务未运行在 %s,跳过测试", configureURL)
35
+	}
36
+
37
+	// 测试1:使用SDK直接登录(验证SDK功能)
38
+	t.Run("SDKLogin", func(t *testing.T) {
39
+		testSDKLogin(t, configureURL)
40
+	})
41
+
42
+	// 测试2:通过svc-code API登录(验证集成功能)
43
+	t.Run("APILogin", func(t *testing.T) {
44
+		testAPILogin(t, svcCodeURL, configureURL)
45
+	})
46
+
47
+	// 测试3:无效凭证登录测试
48
+	t.Run("InvalidCredentials", func(t *testing.T) {
49
+		testInvalidCredentials(t, svcCodeURL)
50
+	})
51
+}
52
+
53
+// testSDKLogin 测试直接使用SDK登录配置中心
54
+func testSDKLogin(t *testing.T, configureURL string) {
55
+	// 创建SDK客户端(登录无需认证,但需要BaseURL)
56
+	client, err := configure.NewBasicAuthClient(configureURL, "test", "test")
57
+	if err != nil {
58
+		t.Fatalf("创建SDK客户端失败: %v", err)
59
+	}
60
+
61
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
62
+	defer cancel()
63
+
64
+	// 使用测试用户登录
65
+	req := &configure.UserLoginRequest{
66
+		UserID:   "test-user-001",
67
+		Password: "password123",
68
+	}
69
+
70
+	startTime := time.Now()
71
+	token, err := client.LoginUser(ctx, req)
72
+	elapsed := time.Since(startTime)
73
+
74
+	if err != nil {
75
+		if isConnectionError(err) {
76
+			t.Skipf("配置中心连接失败,跳过测试: %v", err)
77
+		}
78
+		t.Fatalf("SDK用户登录失败: %v (耗时: %v)", err, elapsed)
79
+	}
80
+
81
+	if token == "" {
82
+		t.Fatal("SDK用户登录返回空token")
83
+	}
84
+
85
+	t.Logf("SDK用户登录成功!获取Token: %s... (耗时: %v)",
86
+		token[:min(50, len(token))], elapsed)
87
+
88
+	// 验证token可用于创建新的认证客户端
89
+	tokenClient, err := configure.NewTokenAuthClient(configureURL, token)
90
+	if err != nil {
91
+		t.Fatalf("使用获取的token创建客户端失败: %v", err)
92
+	}
93
+
94
+	t.Logf("Token客户端创建成功,BaseURL: %s", tokenClient.GetConfig().BaseURL)
95
+}
96
+
97
+// testAPILogin 测试通过svc-code API登录
98
+func testAPILogin(t *testing.T, svcCodeURL, configureURL string) {
99
+	// 创建HTTP客户端
100
+	httpClient := &http.Client{
101
+		Timeout: 30 * time.Second,
102
+	}
103
+
104
+	// 准备登录请求
105
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
106
+	loginData := map[string]string{
107
+		"user_id":  "test-user-001",
108
+		"password": "password123",
109
+	}
110
+	loginJSON, err := json.Marshal(loginData)
111
+	if err != nil {
112
+		t.Fatalf("JSON序列化失败: %v", err)
113
+	}
114
+
115
+	// 发送登录请求
116
+	startTime := time.Now()
117
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(loginJSON))
118
+	if err != nil {
119
+		t.Fatalf("创建HTTP请求失败: %v", err)
120
+	}
121
+	req.Header.Set("Content-Type", "application/json")
122
+
123
+	resp, err := httpClient.Do(req)
124
+	if err != nil {
125
+		t.Fatalf("发送登录请求失败: %v", err)
126
+	}
127
+	defer resp.Body.Close()
128
+
129
+	elapsed := time.Since(startTime)
130
+
131
+	// 检查响应状态码
132
+	if resp.StatusCode != 200 {
133
+		body, _ := io.ReadAll(resp.Body)
134
+		t.Fatalf("登录API返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
135
+	}
136
+
137
+	// 解析响应
138
+	body, err := io.ReadAll(resp.Body)
139
+	if err != nil {
140
+		t.Fatalf("读取响应体失败: %v", err)
141
+	}
142
+
143
+	var result map[string]interface{}
144
+	if err := json.Unmarshal(body, &result); err != nil {
145
+		t.Fatalf("解析响应JSON失败: %v, 响应体: %s", err, string(body))
146
+	}
147
+
148
+	// 检查响应结构
149
+	if !result["success"].(bool) {
150
+		t.Fatalf("登录失败: %v", result)
151
+	}
152
+
153
+	token, ok := result["data"].(string)
154
+	if !ok || token == "" {
155
+		t.Fatalf("响应中未找到有效token: %v", result)
156
+	}
157
+
158
+	t.Logf("API用户登录成功!获取Token: %s... (耗时: %v)",
159
+		token[:min(50, len(token))], elapsed)
160
+
161
+	// 验证token可用于配置中心
162
+	t.Run("ValidateTokenWithConfigure", func(t *testing.T) {
163
+		validateTokenWithConfigure(t, configureURL, token)
164
+	})
165
+}
166
+
167
+// testInvalidCredentials 测试无效凭证登录
168
+func testInvalidCredentials(t *testing.T, svcCodeURL string) {
169
+	httpClient := &http.Client{
170
+		Timeout: 10 * time.Second,
171
+	}
172
+
173
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
174
+	invalidData := map[string]string{
175
+		"user_id":  "invalid-user",
176
+		"password": "wrong-password",
177
+	}
178
+	invalidJSON, err := json.Marshal(invalidData)
179
+	if err != nil {
180
+		t.Fatalf("JSON序列化失败: %v", err)
181
+	}
182
+
183
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(invalidJSON))
184
+	if err != nil {
185
+		t.Fatalf("创建HTTP请求失败: %v", err)
186
+	}
187
+	req.Header.Set("Content-Type", "application/json")
188
+
189
+	resp, err := httpClient.Do(req)
190
+	if err != nil {
191
+		t.Fatalf("发送登录请求失败: %v", err)
192
+	}
193
+	defer resp.Body.Close()
194
+
195
+	// 即使凭证无效,API也应该返回200(业务错误通过success字段表示)
196
+	if resp.StatusCode != 200 {
197
+		body, _ := io.ReadAll(resp.Body)
198
+		t.Fatalf("无效凭证登录返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
199
+	}
200
+
201
+	body, err := io.ReadAll(resp.Body)
202
+	if err != nil {
203
+		t.Fatalf("读取响应体失败: %v", err)
204
+	}
205
+
206
+	var result map[string]interface{}
207
+	if err := json.Unmarshal(body, &result); err != nil {
208
+		t.Fatalf("解析响应JSON失败: %v", err)
209
+	}
210
+
211
+	// 无效凭证应该返回success=false
212
+	if result["success"].(bool) {
213
+		t.Fatal("无效凭证登录应该失败,但成功了")
214
+	}
215
+
216
+	t.Logf("无效凭证登录失败(预期): %v", result)
217
+}
218
+
219
+// validateTokenWithConfigure 验证token可用于配置中心
220
+func validateTokenWithConfigure(t *testing.T, configureURL, token string) {
221
+	// 使用token创建配置中心客户端
222
+	client, err := configure.NewTokenAuthClient(configureURL, token)
223
+	if err != nil {
224
+		t.Fatalf("使用token创建配置中心客户端失败: %v", err)
225
+	}
226
+
227
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
228
+	defer cancel()
229
+
230
+	// 尝试查询表列表(验证token权限)
231
+	query := &configure.DicTableQueryRequest{
232
+		QueryRequest: queryreq.QueryRequest{
233
+			Page:     0,
234
+			PageSize: 5,
235
+		},
236
+	}
237
+
238
+	result, err := client.ListTables(ctx, query)
239
+	if err != nil {
240
+		// 如果token无效或权限不足,可能失败
241
+		t.Logf("使用token查询表列表失败(可能是token权限问题): %v", err)
242
+	} else {
243
+		t.Logf("使用token查询表列表成功,总记录数: %d", result.TotalCount)
244
+	}
245
+}
246
+
247
+// cleanTestCache 清理测试缓存
248
+func cleanTestCache(t *testing.T) {
249
+	cmd := exec.Command("go", "clean", "-testcache")
250
+	if err := cmd.Run(); err != nil {
251
+		t.Logf("清除测试缓存失败: %v", err)
252
+	}
253
+}
254
+
255
+// isServiceRunning 检查服务是否运行
256
+func isServiceRunning(t *testing.T, url string) bool {
257
+	client := &http.Client{
258
+		Timeout: 3 * time.Second,
259
+	}
260
+
261
+	// 尝试访问健康检查端点或根路径
262
+	healthURL := url + "/health"
263
+	resp, err := client.Get(healthURL)
264
+	if err != nil {
265
+		// 也尝试根路径
266
+		resp, err = client.Get(url)
267
+		if err != nil {
268
+			return false
269
+		}
270
+	}
271
+	defer resp.Body.Close()
272
+
273
+	return resp.StatusCode == 200 || resp.StatusCode == 404
274
+}
275
+
276
+// isConnectionError 检查错误是否是连接错误
277
+func isConnectionError(err error) bool {
278
+	errStr := err.Error()
279
+	return contains(errStr, "connection refused") ||
280
+		contains(errStr, "connect: connection refused") ||
281
+		contains(errStr, "dial tcp") ||
282
+		contains(errStr, "EOF") ||
283
+		contains(errStr, "timeout")
284
+}
285
+
286
+// contains 检查字符串是否包含子串
287
+func contains(s, substr string) bool {
288
+	return len(s) >= len(substr) && (s == substr || (len(s) > 0 && len(substr) > 0 && (s[0:len(substr)] == substr || contains(s[1:], substr))))
289
+}
290
+
291
+// min 返回两个整数的最小值
292
+func min(a, b int) int {
293
+	if a < b {
294
+		return a
295
+	}
296
+	return b
297
+}
298
+
299
+// TestHealthEndpoint 测试健康检查端点
300
+func TestHealthEndpoint(t *testing.T) {
301
+	svcCodeURL := "http://localhost:8020"
302
+
303
+	if !isServiceRunning(t, svcCodeURL) {
304
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
305
+	}
306
+
307
+	client := &http.Client{
308
+		Timeout: 10 * time.Second,
309
+	}
310
+
311
+	resp, err := client.Get(svcCodeURL + "/api/health")
312
+	if err != nil {
313
+		t.Fatalf("访问健康检查端点失败: %v", err)
314
+	}
315
+	defer resp.Body.Close()
316
+
317
+	if resp.StatusCode != 200 {
318
+		t.Fatalf("健康检查端点返回非200状态码: %d", resp.StatusCode)
319
+	}
320
+
321
+	body, err := io.ReadAll(resp.Body)
322
+	if err != nil {
323
+		t.Fatalf("读取响应体失败: %v", err)
324
+	}
325
+
326
+	var result map[string]interface{}
327
+	if err := json.Unmarshal(body, &result); err != nil {
328
+		t.Fatalf("解析响应JSON失败: %v", err)
329
+	}
330
+
331
+	if !result["success"].(bool) {
332
+		t.Fatalf("健康检查返回success=false: %v", result)
333
+	}
334
+
335
+	data := result["data"].(map[string]interface{})
336
+	if data["status"] != "healthy" {
337
+		t.Fatalf("健康检查状态不是healthy: %v", data)
338
+	}
339
+
340
+	t.Logf("健康检查端点测试通过: %v", data)
341
+}

Loading…
取消
儲存