feat: 构建用户模块,实现用户对话历史持久化,完善接口文档 #96
324
backend/internal/api/auth_test.go
Normal file
324
backend/internal/api/auth_test.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hhs/camtalk/internal/api"
|
||||
"github.com/hhs/camtalk/internal/auth"
|
||||
)
|
||||
|
||||
// mockAuthService 实现 auth.Service 接口,用于 API 测试。
|
||||
type mockAuthService struct {
|
||||
RegisterFunc func(ctx context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error)
|
||||
LoginFunc func(ctx context.Context, req auth.LoginRequest) (*auth.AuthResponse, error)
|
||||
RefreshFunc func(ctx context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error)
|
||||
LogoutFunc func(ctx context.Context, userID, refreshToken string) error
|
||||
}
|
||||
|
||||
func (m *mockAuthService) Register(ctx context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error) {
|
||||
return m.RegisterFunc(ctx, req)
|
||||
}
|
||||
|
||||
func (m *mockAuthService) Login(ctx context.Context, req auth.LoginRequest) (*auth.AuthResponse, error) {
|
||||
return m.LoginFunc(ctx, req)
|
||||
}
|
||||
|
||||
func (m *mockAuthService) Refresh(ctx context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error) {
|
||||
return m.RefreshFunc(ctx, req)
|
||||
}
|
||||
|
||||
func (m *mockAuthService) Logout(ctx context.Context, userID, refreshToken string) error {
|
||||
return m.LogoutFunc(ctx, userID, refreshToken)
|
||||
}
|
||||
|
||||
// newTestRouter 创建带 AuthHandler 路由的测试 Gin 引擎。
|
||||
func newTestRouter(svc auth.Service) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
tm := auth.NewTokenManager("test-secret", 15*time.Minute, 7*24*time.Hour)
|
||||
h := api.NewAuthHandler(svc, tm)
|
||||
h.RegisterRoutes(r.Group("/api"))
|
||||
return r
|
||||
}
|
||||
|
||||
// newTestRouterWithToken 创建带 AuthHandler 路由的测试引擎,同时返回 TokenManager 以便生成测试 token。
|
||||
func newTestRouterWithToken(svc auth.Service) (*gin.Engine, *auth.TokenManager) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
tm := auth.NewTokenManager("test-secret", 15*time.Minute, 7*24*time.Hour)
|
||||
h := api.NewAuthHandler(svc, tm)
|
||||
h.RegisterRoutes(r.Group("/api"))
|
||||
return r, tm
|
||||
}
|
||||
|
||||
func sampleAuthResponse() *auth.AuthResponse {
|
||||
return &auth.AuthResponse{
|
||||
User: auth.UserResponse{
|
||||
ID: "user-123",
|
||||
Username: "alice",
|
||||
},
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
}
|
||||
}
|
||||
|
||||
// --- Register ---
|
||||
|
||||
func TestRegister_Success(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
RegisterFunc: func(_ context.Context, req auth.RegisterRequest) (*auth.AuthResponse, error) {
|
||||
assert.Equal(t, "alice", req.Username)
|
||||
assert.Equal(t, "password123", req.Password)
|
||||
return sampleAuthResponse(), nil
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "password123"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
var resp auth.AuthResponse
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "alice", resp.User.Username)
|
||||
assert.NotEmpty(t, resp.AccessToken)
|
||||
}
|
||||
|
||||
func TestRegister_InvalidInput_EmptyBody(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "INVALID_INPUT")
|
||||
}
|
||||
|
||||
func TestRegister_InvalidInput_UsernameTooShort(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RegisterRequest{Username: "ab", Password: "password123"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "username must be 3-64 characters")
|
||||
}
|
||||
|
||||
func TestRegister_InvalidInput_PasswordTooShort(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "short"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "password must be 8-72 characters")
|
||||
}
|
||||
|
||||
func TestRegister_UsernameTaken(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
RegisterFunc: func(_ context.Context, _ auth.RegisterRequest) (*auth.AuthResponse, error) {
|
||||
return nil, auth.ErrUsernameTaken
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RegisterRequest{Username: "alice", Password: "password123"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusConflict, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "USERNAME_TAKEN")
|
||||
}
|
||||
|
||||
// --- Login ---
|
||||
|
||||
func TestLogin_Success(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
LoginFunc: func(_ context.Context, req auth.LoginRequest) (*auth.AuthResponse, error) {
|
||||
assert.Equal(t, "alice", req.Username)
|
||||
assert.Equal(t, "password123", req.Password)
|
||||
return sampleAuthResponse(), nil
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.LoginRequest{Username: "alice", Password: "password123"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp auth.AuthResponse
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "alice", resp.User.Username)
|
||||
}
|
||||
|
||||
func TestLogin_InvalidCredentials(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
LoginFunc: func(_ context.Context, _ auth.LoginRequest) (*auth.AuthResponse, error) {
|
||||
return nil, auth.ErrInvalidCredentials
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.LoginRequest{Username: "alice", Password: "wrong-password"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "INVALID_CREDENTIALS")
|
||||
}
|
||||
|
||||
// --- Refresh ---
|
||||
|
||||
func TestRefresh_Success(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
RefreshFunc: func(_ context.Context, req auth.RefreshRequest) (*auth.AuthResponse, error) {
|
||||
assert.Equal(t, "some-refresh-token", req.RefreshToken)
|
||||
return sampleAuthResponse(), nil
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: "some-refresh-token"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestRefresh_MissingToken(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: ""})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "refresh_token is required")
|
||||
}
|
||||
|
||||
func TestRefresh_UsedToken(t *testing.T) {
|
||||
svc := &mockAuthService{
|
||||
RefreshFunc: func(_ context.Context, _ auth.RefreshRequest) (*auth.AuthResponse, error) {
|
||||
return nil, auth.ErrRefreshTokenUsed
|
||||
},
|
||||
}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(auth.RefreshRequest{RefreshToken: "used-token"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "INVALID_TOKEN")
|
||||
}
|
||||
|
||||
// --- Logout ---
|
||||
|
||||
func TestLogout_Success(t *testing.T) {
|
||||
logoutCalled := false
|
||||
svc := &mockAuthService{
|
||||
LogoutFunc: func(_ context.Context, userID, refreshToken string) error {
|
||||
assert.Equal(t, "user-123", userID)
|
||||
assert.Equal(t, "refresh-token-to-revoke", refreshToken)
|
||||
logoutCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
r, tm := newTestRouterWithToken(svc)
|
||||
|
||||
// 生成有效 token
|
||||
access, _, err := tm.GeneratePair("user-123", "alice")
|
||||
require.NoError(t, err)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"refresh_token": "refresh-token-to-revoke"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+access)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.True(t, logoutCalled)
|
||||
assert.Contains(t, w.Body.String(), "logged out successfully")
|
||||
}
|
||||
|
||||
func TestLogout_MissingAuth(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r := newTestRouter(svc)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"refresh_token": "some-token"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
|
||||
func TestLogout_MissingRefreshToken(t *testing.T) {
|
||||
svc := &mockAuthService{}
|
||||
r, tm := newTestRouterWithToken(svc)
|
||||
|
||||
access, _, err := tm.GeneratePair("user-123", "alice")
|
||||
require.NoError(t, err)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"refresh_token": ""})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/logout", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+access)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "refresh_token is required")
|
||||
}
|
||||
Reference in New Issue
Block a user