You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 15:47:13