Files
backend/internal/service/token_service_test.go

513 lines
13 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 service
import (
"carrotskin/internal/model"
"context"
"fmt"
"testing"
"go.uber.org/zap"
)
// TestTokenService_Constants 测试Token服务相关常量
func TestTokenService_Constants(t *testing.T) {
// 内部常量已私有化,通过服务行为间接测试
t.Skip("Token constants are now private - test through service behavior instead")
}
// TestTokenService_Validation 测试Token验证逻辑
func TestTokenService_Validation(t *testing.T) {
tests := []struct {
name string
accessToken string
wantValid bool
}{
{
name: "空token无效",
accessToken: "",
wantValid: false,
},
{
name: "非空token可能有效",
accessToken: "valid-token-string",
wantValid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试空token检查逻辑
isValid := tt.accessToken != ""
if isValid != tt.wantValid {
t.Errorf("Token validation failed: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestTokenService_ClientTokenLogic 测试ClientToken逻辑
func TestTokenService_ClientTokenLogic(t *testing.T) {
tests := []struct {
name string
clientToken string
shouldGenerate bool
}{
{
name: "空的clientToken应该生成新的",
clientToken: "",
shouldGenerate: true,
},
{
name: "非空的clientToken应该使用提供的",
clientToken: "existing-client-token",
shouldGenerate: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldGenerate := tt.clientToken == ""
if shouldGenerate != tt.shouldGenerate {
t.Errorf("ClientToken logic failed: got %v, want %v", shouldGenerate, tt.shouldGenerate)
}
})
}
}
// TestTokenService_ProfileSelection 测试Profile选择逻辑
func TestTokenService_ProfileSelection(t *testing.T) {
tests := []struct {
name string
profileCount int
shouldAutoSelect bool
}{
{
name: "只有一个profile时自动选择",
profileCount: 1,
shouldAutoSelect: true,
},
{
name: "多个profile时不自动选择",
profileCount: 2,
shouldAutoSelect: false,
},
{
name: "没有profile时不自动选择",
profileCount: 0,
shouldAutoSelect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldAutoSelect := tt.profileCount == 1
if shouldAutoSelect != tt.shouldAutoSelect {
t.Errorf("Profile selection logic failed: got %v, want %v", shouldAutoSelect, tt.shouldAutoSelect)
}
})
}
}
// TestTokenService_CleanupLogic 测试清理逻辑
func TestTokenService_CleanupLogic(t *testing.T) {
tests := []struct {
name string
tokenCount int
maxCount int
shouldCleanup bool
cleanupCount int
}{
{
name: "token数量未超过上限不需要清理",
tokenCount: 5,
maxCount: 10,
shouldCleanup: false,
cleanupCount: 0,
},
{
name: "token数量超过上限需要清理",
tokenCount: 15,
maxCount: 10,
shouldCleanup: true,
cleanupCount: 5,
},
{
name: "token数量等于上限不需要清理",
tokenCount: 10,
maxCount: 10,
shouldCleanup: false,
cleanupCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldCleanup := tt.tokenCount > tt.maxCount
if shouldCleanup != tt.shouldCleanup {
t.Errorf("Cleanup decision failed: got %v, want %v", shouldCleanup, tt.shouldCleanup)
}
if shouldCleanup {
expectedCleanupCount := tt.tokenCount - tt.maxCount
if expectedCleanupCount != tt.cleanupCount {
t.Errorf("Cleanup count failed: got %d, want %d", expectedCleanupCount, tt.cleanupCount)
}
}
})
}
}
// TestTokenService_UserIDValidation 测试UserID验证
func TestTokenService_UserIDValidation(t *testing.T) {
tests := []struct {
name string
userID int64
isValid bool
}{
{
name: "有效的UserID",
userID: 1,
isValid: true,
},
{
name: "UserID为0时无效",
userID: 0,
isValid: false,
},
{
name: "负数UserID无效",
userID: -1,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.userID > 0
if isValid != tt.isValid {
t.Errorf("UserID validation failed: got %v, want %v", isValid, tt.isValid)
}
})
}
}
// ============================================================================
// 使用 Mock 的集成测试
// ============================================================================
// TestTokenServiceImpl_Create 测试创建Token
func TestTokenServiceImpl_Create(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Profile
testProfile := &model.Profile{
UUID: "test-profile-uuid",
UserID: 1,
Name: "TestProfile",
}
_ = profileRepo.Create(context.Background(), testProfile)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
tests := []struct {
name string
userID int64
uuid string
clientToken string
wantErr bool
}{
{
name: "正常创建Token指定UUID",
userID: 1,
uuid: "test-profile-uuid",
clientToken: "client-token-1",
wantErr: false,
},
{
name: "正常创建Token空clientToken",
userID: 1,
uuid: "test-profile-uuid",
clientToken: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
_, _, accessToken, clientToken, err := tokenService.Create(ctx, tt.userID, tt.uuid, tt.clientToken)
if tt.wantErr {
if err == nil {
t.Error("期望返回错误,但实际没有错误")
}
} else {
if err != nil {
t.Errorf("不期望返回错误: %v", err)
return
}
if accessToken == "" {
t.Error("accessToken不应为空")
}
if clientToken == "" {
t.Error("clientToken不应为空")
}
}
})
}
}
// TestTokenServiceImpl_Validate 测试验证Token
func TestTokenServiceImpl_Validate(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Token
testToken := &model.Token{
AccessToken: "valid-access-token",
ClientToken: "valid-client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), testToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
tests := []struct {
name string
accessToken string
clientToken string
wantValid bool
}{
{
name: "有效Token完全匹配",
accessToken: "valid-access-token",
clientToken: "valid-client-token",
wantValid: true,
},
{
name: "有效Token只检查accessToken",
accessToken: "valid-access-token",
clientToken: "",
wantValid: true,
},
{
name: "无效TokenaccessToken不存在",
accessToken: "invalid-access-token",
clientToken: "",
wantValid: false,
},
{
name: "无效TokenclientToken不匹配",
accessToken: "valid-access-token",
clientToken: "wrong-client-token",
wantValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
isValid := tokenService.Validate(ctx, tt.accessToken, tt.clientToken)
if isValid != tt.wantValid {
t.Errorf("Token验证结果不匹配: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestTokenServiceImpl_Invalidate 测试注销Token
func TestTokenServiceImpl_Invalidate(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Token
testToken := &model.Token{
AccessToken: "token-to-invalidate",
ClientToken: "client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), testToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 验证Token存在
isValid := tokenService.Validate(ctx, "token-to-invalidate", "")
if !isValid {
t.Error("Token应该有效")
}
// 注销Token
tokenService.Invalidate(ctx, "token-to-invalidate")
// 验证Token已失效从repo中删除
_, err := tokenRepo.FindByAccessToken(context.Background(), "token-to-invalidate")
if err == nil {
t.Error("Token应该已被删除")
}
}
// TestTokenServiceImpl_InvalidateUserTokens 测试注销用户所有Token
func TestTokenServiceImpl_InvalidateUserTokens(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置多个Token
for i := 1; i <= 3; i++ {
_ = tokenRepo.Create(context.Background(), &model.Token{
AccessToken: fmt.Sprintf("user1-token-%d", i),
ClientToken: "client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
})
}
_ = tokenRepo.Create(context.Background(), &model.Token{
AccessToken: "user2-token-1",
ClientToken: "client-token",
UserID: 2,
ProfileId: "test-profile-uuid-2",
Usable: true,
})
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 注销用户1的所有Token
tokenService.InvalidateUserTokens(ctx, 1)
// 验证用户1的Token已失效
tokens, _ := tokenRepo.GetByUserID(context.Background(), 1)
if len(tokens) > 0 {
t.Errorf("用户1的Token应该全部被删除但还剩 %d 个", len(tokens))
}
// 验证用户2的Token仍然存在
tokens2, _ := tokenRepo.GetByUserID(context.Background(), 2)
if len(tokens2) != 1 {
t.Errorf("用户2的Token应该仍然存在期望1个实际 %d 个", len(tokens2))
}
}
// TestTokenServiceImpl_Refresh 覆盖 Refresh 的主要分支
func TestTokenServiceImpl_Refresh(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置 Profile 与 Token
profile := &model.Profile{
UUID: "profile-uuid",
UserID: 1,
}
_ = profileRepo.Create(context.Background(), profile)
oldToken := &model.Token{
AccessToken: "old-token",
ClientToken: "client-token",
UserID: 1,
ProfileId: "",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), oldToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 正常刷新,不指定 profile
newAccess, client, err := tokenService.Refresh(ctx, "old-token", "client-token", "")
if err != nil {
t.Fatalf("Refresh 正常情况失败: %v", err)
}
if newAccess == "" || client != "client-token" {
t.Fatalf("Refresh 返回值异常: access=%s, client=%s", newAccess, client)
}
// accessToken 为空
if _, _, err := tokenService.Refresh(ctx, "", "client-token", ""); err == nil {
t.Fatalf("Refresh 在 accessToken 为空时应返回错误")
}
}
// TestTokenServiceImpl_GetByAccessToken 封装 GetUUIDByAccessToken / GetUserIDByAccessToken
func TestTokenServiceImpl_GetByAccessToken(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
token := &model.Token{
AccessToken: "token-1",
UserID: 42,
ProfileId: "profile-42",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), token)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
uuid, err := tokenService.GetUUIDByAccessToken(ctx, "token-1")
if err != nil || uuid != "profile-42" {
t.Fatalf("GetUUIDByAccessToken 返回错误: uuid=%s, err=%v", uuid, err)
}
uid, err := tokenService.GetUserIDByAccessToken(ctx, "token-1")
if err != nil || uid != 42 {
t.Fatalf("GetUserIDByAccessToken 返回错误: uid=%d, err=%v", uid, err)
}
}
// TestTokenServiceImpl_validateProfileByUserID 直接测试内部校验逻辑
func TestTokenServiceImpl_validateProfileByUserID(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
svc := &tokenService{
tokenRepo: tokenRepo,
profileRepo: profileRepo,
logger: logger,
}
// 预置 Profile
profile := &model.Profile{
UUID: "p-1",
UserID: 1,
}
_ = profileRepo.Create(context.Background(), profile)
// 参数非法
if ok, err := svc.validateProfileByUserID(context.Background(), 0, ""); err == nil || ok {
t.Fatalf("validateProfileByUserID 在参数非法时应返回错误")
}
// Profile 不存在
if ok, err := svc.validateProfileByUserID(context.Background(), 1, "not-exists"); err == nil || ok {
t.Fatalf("validateProfileByUserID 在 Profile 不存在时应返回错误")
}
// 用户与 Profile 匹配
if ok, err := svc.validateProfileByUserID(context.Background(), 1, "p-1"); err != nil || !ok {
t.Fatalf("validateProfileByUserID 匹配时应返回 true, err=%v", err)
}
// 用户与 Profile 不匹配
if ok, err := svc.validateProfileByUserID(context.Background(), 2, "p-1"); err != nil || ok {
t.Fatalf("validateProfileByUserID 不匹配时应返回 false, err=%v", err)
}
}