Go中需认证Web应用端点测试方案咨询(scs会话管理器)
测试认证受限端点的解决方案(基于《Let's Go》与scs会话管理器)
核心问题解决:在测试中指定认证用户ID
你可以直接利用scs的会话API,在测试时跳过登录流程,手动创建包含指定用户ID的会话,并将会话Cookie附加到测试请求中。以下是结合你现有测试框架的具体实现:
步骤1:扩展测试服务器(testServer)
首先修改testServer结构体,让它能够访问应用的会话管理器,并添加一个带认证的GET方法:
type testServer struct { app *application // 添加应用引用,用于访问会话管理器 url string client *http.Client } // 带认证的GET请求方法 func (ts *testServer) authenticatedGet(t *testing.T, urlPath string, userID int) (int, http.Header, string) { req, err := http.NewRequest(http.MethodGet, urlPath, nil) if err != nil { t.Fatal(err) } // 创建新会话并设置认证用户ID session := ts.app.sessionManager.NewSession() session.Put(req.Context(), "authenticatedUserID", userID) // 保存会话获取ID sessionID, err := ts.app.sessionManager.Save(req.Context(), session) if err != nil { t.Fatal(err) } // 添加会话Cookie到请求 req.AddCookie(&http.Cookie{ Name: ts.app.sessionManager.Config.Cookie.Name, Value: sessionID, Path: "/", }) // 发送请求并处理响应(和原get方法逻辑一致) resp, err := ts.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } return resp.StatusCode, resp.Header, string(body) }
同时更新newTestServer函数,传入应用实例:
func newTestServer(t *testing.T, h http.Handler, app *application) *testServer { ts := &testServer{ app: app, client: &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }, } mux := http.NewServeMux() mux.Handle("/", h) srv := &http.Server{ Addr: ":0", // 使用随机端口 Handler: mux, } ln, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } ts.url = fmt.Sprintf("http://%s", ln.Addr().String()) go func() { if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { t.Fatalf("server failed to start: %v", err) } }() t.Cleanup(func() { if err := srv.Close(); err != nil { t.Fatalf("server failed to close: %v", err) } }) return ts }
步骤2:修改测试用例
在测试中根据用例指定的用户ID,选择调用authenticatedGet或普通get方法:
func TestHandlerA(t *testing.T) { app := newTestApplication(t) ts := newTestServer(t, app.routes(), app) defer ts.Close() tests := []struct { name string urlPath string authenticatedUserID int // 0表示未认证 wantStatus int wantBodyContains string }{ { name: "authenticated user access", urlPath: "/handler-a", authenticatedUserID: 123, wantStatus: http.StatusOK, wantBodyContains: "Welcome, user 123", }, { name: "unauthenticated user access", urlPath: "/handler-a", authenticatedUserID: 0, wantStatus: http.StatusUnauthorized, wantBodyContains: "Please log in", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var status int var header http.Header var body string if tt.authenticatedUserID != 0 { status, header, body = ts.authenticatedGet(t, ts.url+tt.urlPath, tt.authenticatedUserID) } else { status, header, body = ts.get(t, ts.url+tt.urlPath) } // 断言状态码 if status != tt.wantStatus { t.Errorf("got status %d, want %d", status, tt.wantStatus) } // 断言响应内容 if !strings.Contains(body, tt.wantBodyContains) { t.Errorf("body missing expected content: %q, got: %q", tt.wantBodyContains, body) } }) } }
测试认证/授权端点的常用模式
除了上面的方法,还有几种常见的测试模式适用于不同场景:
1. 单元测试单个处理器(绕过中间件)
如果只需要测试处理器本身的逻辑,不需要启动完整服务器,可以直接构造包含认证用户ID的上下文,调用处理器:
func TestHandlerAUnit(t *testing.T) { app := newTestApplication(t) // 构造带有认证用户ID的上下文 ctx := context.Background() ctx = app.sessionManager.Put(ctx, "authenticatedUserID", 123) // 创建测试请求和响应记录器 req := httptest.NewRequest(http.MethodGet, "/handler-a", nil) req = req.WithContext(ctx) w := httptest.NewRecorder() // 直接调用处理器 app.handlerA(w, req) // 断言结果 resp := w.Result() if resp.StatusCode != http.StatusOK { t.Errorf("got status %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), "Welcome, user 123") { t.Error("response body missing expected welcome message") } }
2. 模拟真实登录流程(集成测试)
如果需要模拟真实用户的完整操作流程(登录→访问受限端点),可以先创建测试用户,发送登录请求获取会话Cookie,再用该Cookie访问端点:
func TestHandlerAWithRealLogin(t *testing.T) { app := newTestApplication(t) ts := newTestServer(t, app.routes(), app) defer ts.Close() // 1. 创建测试用户(假设你的用户模型有Create方法) userID, err := app.models.Users.Create("test@example.com", "password123") if err != nil { t.Fatal(err) } // 2. 发送登录请求,获取会话Cookie loginReq, err := http.NewRequest(http.MethodPost, ts.url+"/login", strings.NewReader("email=test@example.com&password=password123")) if err != nil { t.Fatal(err) } loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") loginResp, err := ts.client.Do(loginReq) if err != nil { t.Fatal(err) } defer loginResp.Body.Close() // 提取会话Cookie var sessionCookie *http.Cookie for _, c := range loginResp.Cookies() { if c.Name == app.sessionManager.Config.Cookie.Name { sessionCookie = c break } } if sessionCookie == nil { t.Fatal("no session cookie returned after login") } // 3. 用会话Cookie访问受限端点 req, err := http.NewRequest(http.MethodGet, ts.url+"/handler-a", nil) if err != nil { t.Fatal(err) } req.AddCookie(sessionCookie) resp, err := ts.client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("got status %d, want %d", resp.StatusCode, http.StatusOK) } }
3. 测试权限控制场景
针对不同权限的用户(比如普通用户、管理员),可以在测试用例中指定不同的用户ID,验证端点是否正确返回权限对应的响应:
tests := []struct { name string urlPath string authenticatedUserID int userRole string // 新增角色字段 wantStatus int }{ { name: "admin access to admin endpoint", urlPath: "/admin/dashboard", authenticatedUserID: 1, // 管理员ID userRole: "admin", wantStatus: http.StatusOK, }, { name: "regular user denied admin endpoint", urlPath: "/admin/dashboard", authenticatedUserID: 2, // 普通用户ID userRole: "user", wantStatus: http.StatusForbidden, }, }
内容的提问来源于stack exchange,提问作者quasi_deus
相关产品推荐
相关产品推荐

