Internal error occurred but is skipped: FindTagsByCommitIDs

Files
CamTalk/backend/internal/auth/service_test.go
hhs 898e30b526 feat: Refresh Token 复用检测 — 检测到已 rotation 的 token 被复用时吊销用户全部 refresh token
- Refresh 方法中 FindRefreshToken 返回 not found 时,检查 JWT 是否有效
- JWT 有效但 DB 不存在 → 判定为复用,调用 DeleteUserRefreshTokens 吊销该用户所有 token
- 增加 TestRefresh_ReuseDetectedRevokesAllTokens 测试覆盖复用场景
2026-06-20 14:51:34 +08:00

236 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hhs/camtalk/internal/auth"
"github.com/hhs/camtalk/internal/store"
)
// newTestService 创建测试用的 AuthService + MemUserRepository。
func newTestService(t *testing.T) (auth.Service, *store.MemUserRepository) {
t.Helper()
tm := auth.NewTokenManager("test-secret", 15*time.Minute, 7*24*time.Hour)
repo := store.NewMemUserRepository()
svc := auth.NewAuthService(tm, repo)
return svc, repo
}
// --- Register ---
func TestRegister_Success(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
resp, err := svc.Register(ctx, auth.RegisterRequest{
Username: "alice",
Password: "password123",
})
require.NoError(t, err)
assert.NotEmpty(t, resp.User.ID)
assert.Equal(t, "alice", resp.User.Username)
assert.NotEmpty(t, resp.AccessToken)
assert.NotEmpty(t, resp.RefreshToken)
}
func TestRegister_DuplicateUsername(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Register(ctx, auth.RegisterRequest{
Username: "alice",
Password: "password123",
})
require.NoError(t, err)
// 同名再次注册
_, err = svc.Register(ctx, auth.RegisterRequest{
Username: "alice",
Password: "another-password",
})
assert.ErrorIs(t, err, auth.ErrUsernameTaken)
}
// --- Login ---
func TestLogin_Success(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
// 先注册
_, err := svc.Register(ctx, auth.RegisterRequest{
Username: "bob",
Password: "password123",
})
require.NoError(t, err)
// 登录
resp, err := svc.Login(ctx, auth.LoginRequest{
Username: "bob",
Password: "password123",
})
require.NoError(t, err)
assert.Equal(t, "bob", resp.User.Username)
assert.NotEmpty(t, resp.AccessToken)
assert.NotEmpty(t, resp.RefreshToken)
}
func TestLogin_WrongPassword(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Register(ctx, auth.RegisterRequest{
Username: "bob",
Password: "password123",
})
require.NoError(t, err)
_, err = svc.Login(ctx, auth.LoginRequest{
Username: "bob",
Password: "wrong-password",
})
assert.ErrorIs(t, err, auth.ErrInvalidCredentials)
}
func TestLogin_UserNotFound(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Login(ctx, auth.LoginRequest{
Username: "nonexistent",
Password: "password123",
})
assert.ErrorIs(t, err, auth.ErrInvalidCredentials)
}
// --- Refresh ---
func TestRefresh_Success(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
// 注册
regResp, err := svc.Register(ctx, auth.RegisterRequest{
Username: "charlie",
Password: "password123",
})
require.NoError(t, err)
// 刷新
refreshResp, err := svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: regResp.RefreshToken,
})
require.NoError(t, err)
assert.Equal(t, "charlie", refreshResp.User.Username)
assert.NotEmpty(t, refreshResp.AccessToken)
assert.NotEmpty(t, refreshResp.RefreshToken)
// 新旧 refresh token 应不同rotation
assert.NotEqual(t, regResp.RefreshToken, refreshResp.RefreshToken)
}
func TestRefresh_UsedTokenFails(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
regResp, err := svc.Register(ctx, auth.RegisterRequest{
Username: "charlie",
Password: "password123",
})
require.NoError(t, err)
// 第一次刷新
_, err = svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: regResp.RefreshToken,
})
require.NoError(t, err)
// 用旧 token 再次刷新 → 应失败
_, err = svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: regResp.RefreshToken,
})
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
}
func TestRefresh_InvalidToken(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
_, err := svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: "completely-invalid-token",
})
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
}
// --- Logout ---
func TestLogout_Success(t *testing.T) {
svc, _ := newTestService(t)
ctx := context.Background()
regResp, err := svc.Register(ctx, auth.RegisterRequest{
Username: "dave",
Password: "password123",
})
require.NoError(t, err)
// 登出
err = svc.Logout(ctx, regResp.User.ID, regResp.RefreshToken)
require.NoError(t, err)
// 登出后 refresh token 应失效
_, err = svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: regResp.RefreshToken,
})
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
}
// --- Refresh Token 复用检测 ---
func TestRefresh_ReuseDetectedRevokesAllTokens(t *testing.T) {
svc, repo := newTestService(t)
ctx := context.Background()
// 注册,获得令牌对 A
regResp, err := svc.Register(ctx, auth.RegisterRequest{
Username: "eve",
Password: "password123",
})
require.NoError(t, err)
tokenPairA_refresh := regResp.RefreshToken
// 再次登录,获得令牌对 B
loginResp, err := svc.Login(ctx, auth.LoginRequest{
Username: "eve",
Password: "password123",
})
require.NoError(t, err)
tokenPairB_refresh := loginResp.RefreshToken
// 用令牌对 A 的 refresh token 正常刷新 → 成功
refreshResp, err := svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: tokenPairA_refresh,
})
require.NoError(t, err)
assert.NotEmpty(t, refreshResp.AccessToken)
// 用令牌对 A 的旧 refresh token 再次刷新 → 复用检测,应失败
_, err = svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: tokenPairA_refresh,
})
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
// 令牌对 B 的 refresh token 也应被吊销(全量吊销)
_, err = svc.Refresh(ctx, auth.RefreshRequest{
RefreshToken: tokenPairB_refresh,
})
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
// 确认 DB 中该用户已无 refresh token
_ = repo // repo 用于确认,但 MemUserRepository 无直接查询方法,通过 Refresh 失败已间接验证
}