- Updated main.go to initialize email service and include it in the dependency injection container. - Refactored handlers to utilize context in service method calls, improving consistency and error handling. - Introduced new service options for upload, security, and captcha services, enhancing modularity and testability. - Removed unused repository implementations to streamline the codebase. This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
514 lines
13 KiB
Go
514 lines
13 KiB
Go
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",
|
||
IsActive: true,
|
||
}
|
||
profileRepo.Create(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(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: "无效Token(accessToken不存在)",
|
||
accessToken: "invalid-access-token",
|
||
clientToken: "",
|
||
wantValid: false,
|
||
},
|
||
{
|
||
name: "无效Token(clientToken不匹配)",
|
||
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(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("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(&model.Token{
|
||
AccessToken: fmt.Sprintf("user1-token-%d", i),
|
||
ClientToken: "client-token",
|
||
UserID: 1,
|
||
ProfileId: "test-profile-uuid",
|
||
Usable: true,
|
||
})
|
||
}
|
||
tokenRepo.Create(&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(1)
|
||
if len(tokens) > 0 {
|
||
t.Errorf("用户1的Token应该全部被删除,但还剩 %d 个", len(tokens))
|
||
}
|
||
|
||
// 验证用户2的Token仍然存在
|
||
tokens2, _ := tokenRepo.GetByUserID(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(profile)
|
||
|
||
oldToken := &model.Token{
|
||
AccessToken: "old-token",
|
||
ClientToken: "client-token",
|
||
UserID: 1,
|
||
ProfileId: "",
|
||
Usable: true,
|
||
}
|
||
tokenRepo.Create(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(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(profile)
|
||
|
||
// 参数非法
|
||
if ok, err := svc.validateProfileByUserID(0, ""); err == nil || ok {
|
||
t.Fatalf("validateProfileByUserID 在参数非法时应返回错误")
|
||
}
|
||
|
||
// Profile 不存在
|
||
if ok, err := svc.validateProfileByUserID(1, "not-exists"); err == nil || ok {
|
||
t.Fatalf("validateProfileByUserID 在 Profile 不存在时应返回错误")
|
||
}
|
||
|
||
// 用户与 Profile 匹配
|
||
if ok, err := svc.validateProfileByUserID(1, "p-1"); err != nil || !ok {
|
||
t.Fatalf("validateProfileByUserID 匹配时应返回 true, err=%v", err)
|
||
}
|
||
|
||
// 用户与 Profile 不匹配
|
||
if ok, err := svc.validateProfileByUserID(2, "p-1"); err != nil || ok {
|
||
t.Fatalf("validateProfileByUserID 不匹配时应返回 false, err=%v", err)
|
||
}
|
||
}
|