2026-06-14 17:18:22 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-06-20 14:51:34 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 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 失败已间接验证
|
|
|
|
|
|
}
|