This commit is contained in:
Joe
2026-01-16 15:49:34 +08:00
commit 550d3e1f42
380 changed files with 62024 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
package services
import (
"os"
"testing"
"github.com/goravel/framework/facades"
_ "goravel/tests"
)
func TestMain(m *testing.M) {
if err := facades.Artisan().Call("migrate"); err != nil {
panic(err)
}
exit := m.Run()
os.Exit(exit)
}
+377
View File
@@ -0,0 +1,377 @@
package services
import (
"testing"
"time"
"github.com/goravel/framework/facades"
"github.com/stretchr/testify/suite"
apperrors "goravel/app/errors"
"goravel/app/models"
"goravel/app/services"
"goravel/tests"
)
type NotificationServiceTestSuite struct {
suite.Suite
tests.TestCase
service services.NotificationService
}
func TestNotificationServiceTestSuite(t *testing.T) {
suite.Run(t, &NotificationServiceTestSuite{})
}
func (s *NotificationServiceTestSuite) SetupSuite() {
s.service = services.NewNotificationServiceImpl()
}
func (s *NotificationServiceTestSuite) SetupTest() {
s.RefreshDatabase()
s.Seed()
}
func (s *NotificationServiceTestSuite) TearDownTest() {
// 清理测试数据
_, _ = facades.Orm().Query().Where("type", "test").Delete(&models.Notification{})
}
// ==================== Create 方法测试 ====================
// TestCreate_SingleNotification 测试创建单个通知
func (s *NotificationServiceTestSuite) TestCreate_SingleNotification() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
receiverID := admin.ID
notification, err := s.service.Create("测试标题", "测试内容", "test", nil, &receiverID)
s.NoError(err)
s.NotNil(notification)
s.Equal("测试标题", notification.Title)
s.Equal("测试内容", notification.Content)
s.Equal("test", notification.Type)
s.Equal(receiverID, *notification.ReceiverID)
s.NotZero(notification.ID)
}
// TestCreate_BroadcastToAllAdmins 测试批量创建通知给所有管理员
func (s *NotificationServiceTestSuite) TestCreate_BroadcastToAllAdmins() {
// 获取所有管理员
var admins []models.Admin
err := facades.Orm().Query().Find(&admins)
s.Require().NoError(err)
s.Require().NotEmpty(admins, "应该有至少一个管理员")
// 创建通知给所有管理员(receiverID 为 nil
notification, err := s.service.Create("系统通知", "这是一条系统通知", "system", nil, nil)
s.NoError(err)
s.NotNil(notification)
s.Equal("系统通知", notification.Title)
// 验证所有管理员都收到了通知
count, err := facades.Orm().Query().
Model(&models.Notification{}).
Where("title", "系统通知").
Where("type", "system").
Count()
s.NoError(err)
s.Equal(int64(len(admins)), count, "应该为每个管理员创建一条通知")
}
// TestCreate_NoAdminsFound 测试没有管理员时的错误处理
func (s *NotificationServiceTestSuite) TestCreate_NoAdminsFound() {
// 删除所有管理员
_, err := facades.Orm().Query().Delete(&models.Admin{})
s.Require().NoError(err)
// 尝试创建通知给所有管理员
notification, err := s.service.Create("测试", "内容", "test", nil, nil)
s.Error(err)
s.Nil(notification)
// 验证返回的是业务错误
businessErr, ok := apperrors.GetBusinessError(err)
s.True(ok, "应该返回业务错误")
s.Equal("record_not_found", businessErr.Code)
}
// ==================== List 方法测试 ====================
// TestList_BasicQuery 测试基本查询
func (s *NotificationServiceTestSuite) TestList_BasicQuery() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建几条测试通知
for i := 0; i < 5; i++ {
_, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
}
// 查询通知列表
notifications, total, err := s.service.List(admin.ID, 1, 10, "", "")
s.NoError(err)
s.GreaterOrEqual(int(total), 5)
s.NotEmpty(notifications)
}
// TestList_WithTypeFilter 测试按类型筛选
func (s *NotificationServiceTestSuite) TestList_WithTypeFilter() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建不同类型的通知
_, err = s.service.Create("系统通知", "内容", "system", nil, &admin.ID)
s.Require().NoError(err)
_, err = s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
// 只查询 system 类型的通知
notifications, total, err := s.service.List(admin.ID, 1, 10, "system", "")
s.NoError(err)
s.GreaterOrEqual(int(total), 1)
for _, notif := range notifications {
s.Equal("system", notif.Type)
}
}
// TestList_WithReadStatusFilter 测试按已读状态筛选
func (s *NotificationServiceTestSuite) TestList_WithReadStatusFilter() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建通知
notification, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
// 标记为已读
err = s.service.MarkRead(admin.ID, notification.ID)
s.Require().NoError(err)
// 查询未读通知
unread, total, err := s.service.List(admin.ID, 1, 10, "", "false")
s.NoError(err)
// 验证未读通知不包含已读的通知
found := false
for _, notif := range unread {
if notif.ID == notification.ID {
found = true
break
}
}
s.False(found, "已读通知不应出现在未读列表中")
// 查询已读通知
read, total, err := s.service.List(admin.ID, 1, 10, "", "true")
s.NoError(err)
s.GreaterOrEqual(int(total), 1)
// 验证已读通知包含该通知
found = false
for _, notif := range read {
if notif.ID == notification.ID {
found = true
s.True(notif.IsRead)
break
}
}
s.True(found, "已读通知应出现在已读列表中")
}
// TestList_Pagination 测试分页
func (s *NotificationServiceTestSuite) TestList_Pagination() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建 15 条通知
for i := 0; i < 15; i++ {
_, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
}
// 第一页,每页 10 条
page1, total, err := s.service.List(admin.ID, 1, 10, "", "")
s.NoError(err)
s.GreaterOrEqual(int(total), 15)
s.LessOrEqual(len(page1), 10)
// 第二页
page2, _, err := s.service.List(admin.ID, 2, 10, "", "")
s.NoError(err)
s.LessOrEqual(len(page2), 10)
// 验证两页的数据不重复
if len(page1) > 0 && len(page2) > 0 {
s.NotEqual(page1[0].ID, page2[0].ID, "两页的数据应该不同")
}
}
// ==================== MarkRead 方法测试 ====================
// TestMarkRead_Success 测试标记为已读成功
func (s *NotificationServiceTestSuite) TestMarkRead_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建通知
notification, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
s.False(notification.IsRead, "新通知应该是未读状态")
// 标记为已读
err = s.service.MarkRead(admin.ID, notification.ID)
s.NoError(err)
// 验证已读状态
var updated models.Notification
err = facades.Orm().Query().Where("id", notification.ID).First(&updated)
s.NoError(err)
s.True(updated.IsRead)
s.NotNil(updated.ReadAt)
}
// TestMarkRead_NotFound 测试通知不存在
func (s *NotificationServiceTestSuite) TestMarkRead_NotFound() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 尝试标记不存在的通知为已读
err = s.service.MarkRead(admin.ID, 99999)
s.Error(err)
// 验证返回的是业务错误
businessErr, ok := apperrors.GetBusinessError(err)
s.True(ok, "应该返回业务错误")
s.Equal("record_not_found", businessErr.Code)
}
// TestMarkRead_AlreadyRead 测试已读通知再次标记
func (s *NotificationServiceTestSuite) TestMarkRead_AlreadyRead() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建并标记为已读
notification, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
err = s.service.MarkRead(admin.ID, notification.ID)
s.Require().NoError(err)
// 再次标记为已读(应该不报错)
err = s.service.MarkRead(admin.ID, notification.ID)
s.NoError(err, "已读通知再次标记应该不报错")
}
// ==================== MarkAllRead 方法测试 ====================
// TestMarkAllRead_Success 测试标记所有通知为已读
func (s *NotificationServiceTestSuite) TestMarkAllRead_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建多条未读通知
for i := 0; i < 5; i++ {
_, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
}
// 标记所有为已读
err = s.service.MarkAllRead(admin.ID)
s.NoError(err)
// 验证未读数量为 0
count, err := s.service.UnreadCount(admin.ID)
s.NoError(err)
s.Equal(int64(0), count)
}
// ==================== UnreadCount 方法测试 ====================
// TestUnreadCount_Basic 测试未读数量统计
func (s *NotificationServiceTestSuite) TestUnreadCount_Basic() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建 3 条未读通知
for i := 0; i < 3; i++ {
_, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
}
// 验证未读数量
count, err := s.service.UnreadCount(admin.ID)
s.NoError(err)
s.GreaterOrEqual(count, int64(3))
// 标记一条为已读
notifications, _, err := s.service.List(admin.ID, 1, 1, "", "false")
s.Require().NoError(err)
s.Require().NotEmpty(notifications)
err = s.service.MarkRead(admin.ID, notifications[0].ID)
s.Require().NoError(err)
// 验证未读数量减少
newCount, err := s.service.UnreadCount(admin.ID)
s.NoError(err)
s.Equal(count-1, newCount)
}
// ==================== ListRecent 方法测试 ====================
// TestListRecent_Basic 测试获取最近通知
func (s *NotificationServiceTestSuite) TestListRecent_Basic() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建多条通知
for i := 0; i < 10; i++ {
_, err := s.service.Create("测试通知", "内容", "test", nil, &admin.ID)
s.Require().NoError(err)
time.Sleep(10 * time.Millisecond) // 确保时间戳不同
}
// 获取最近 5 条
recent, err := s.service.ListRecent(admin.ID, 5)
s.NoError(err)
s.LessOrEqual(len(recent), 5)
s.GreaterOrEqual(len(recent), 1)
// 验证返回了数据(排序验证在数据库层面完成,这里只验证数据存在)
s.NotEmpty(recent, "应该返回最近的通知")
}
// TestListRecent_LimitValidation 测试限制验证
func (s *NotificationServiceTestSuite) TestListRecent_LimitValidation() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 测试负数限制(应该使用默认值 5)
recent, err := s.service.ListRecent(admin.ID, -1)
s.NoError(err)
s.LessOrEqual(len(recent), 5)
// 测试超过最大值的限制(应该使用默认值 5)
recent, err = s.service.ListRecent(admin.ID, 100)
s.NoError(err)
s.LessOrEqual(len(recent), 5)
}
+65
View File
@@ -0,0 +1,65 @@
package services
import (
"context"
"fmt"
"testing"
"time"
"github.com/goravel/framework/facades"
"github.com/stretchr/testify/assert"
"goravel/app/models"
"goravel/app/services"
"goravel/app/utils/traceid"
)
func TestSystemLogTraceIDPersistence(t *testing.T) {
t.Parallel()
uniqueTraceID := fmt.Sprintf("trace-%d", time.Now().UnixNano())
log := models.SystemLog{
Level: "info",
Module: "trace_test",
TraceID: uniqueTraceID,
Message: "trace id persistence test",
Context: `{"test":true}`,
}
err := facades.Orm().Query().Create(&log)
assert.NoError(t, err)
assert.NotZero(t, log.ID)
t.Cleanup(func() {
_, _ = facades.Orm().Query().Where("id", log.ID).Delete(&models.SystemLog{})
})
var stored models.SystemLog
err = facades.Orm().Query().Where("id", log.ID).First(&stored)
assert.NoError(t, err)
assert.Equal(t, uniqueTraceID, stored.TraceID)
var filtered models.SystemLog
err = facades.Orm().Query().Where("trace_id", uniqueTraceID).First(&filtered)
assert.NoError(t, err)
assert.Equal(t, log.ID, filtered.ID)
}
func TestSystemLogServiceRecord(t *testing.T) {
t.Parallel()
service := services.NewSystemLogService()
ctx, traceID := traceid.EnsureContext(context.Background())
err := service.Record(ctx, "error", "unit-test", "trace helper test", map[string]any{"ok": true})
assert.NoError(t, err)
var stored models.SystemLog
err = facades.Orm().Query().Where("trace_id", traceID).Order("id desc").First(&stored)
assert.NoError(t, err)
assert.Equal(t, "unit-test", stored.Module)
t.Cleanup(func() {
_, _ = facades.Orm().Query().Where("trace_id", traceID).Delete(&models.SystemLog{})
})
}
+363
View File
@@ -0,0 +1,363 @@
package services
import (
"testing"
"time"
"github.com/goravel/framework/facades"
"github.com/stretchr/testify/suite"
apperrors "goravel/app/errors"
"goravel/app/models"
"goravel/app/services"
"goravel/tests"
)
type TokenServiceFeatureTestSuite struct {
suite.Suite
tests.TestCase
service services.TokenService
}
func TestTokenServiceFeatureTestSuite(t *testing.T) {
suite.Run(t, &TokenServiceFeatureTestSuite{})
}
func (s *TokenServiceFeatureTestSuite) SetupSuite() {
s.service = services.NewTokenServiceImpl()
}
func (s *TokenServiceFeatureTestSuite) SetupTest() {
s.RefreshDatabase()
s.Seed()
}
func (s *TokenServiceFeatureTestSuite) TearDownTest() {
// 清理测试数据
_, _ = facades.Orm().Query().Where("name LIKE ?", "test_%").Delete(&models.PersonalAccessToken{})
}
// ==================== CreateToken 方法测试 ====================
// TestCreateToken_Success 测试创建 token 成功
func (s *TokenServiceFeatureTestSuite) TestCreateToken_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
expiresAt := time.Now().Add(24 * time.Hour)
plainToken, accessToken, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
&expiresAt,
"Chrome",
"127.0.0.1",
"Windows",
"session123",
)
s.NoError(err)
s.NotEmpty(plainToken)
s.NotNil(accessToken)
s.Equal("admin", accessToken.TokenableType)
s.Equal(admin.ID, accessToken.TokenableID)
s.Equal("test_token", accessToken.Name)
s.Equal("Chrome", accessToken.Browser)
s.Equal("127.0.0.1", accessToken.IP)
s.Equal("Windows", accessToken.OS)
s.Equal("session123", accessToken.SessionID)
s.NotNil(accessToken.LastUsedAt, "创建时应该设置 LastUsedAt")
}
// TestCreateToken_WithoutExpiresAt 测试创建无过期时间的 token
func (s *TokenServiceFeatureTestSuite) TestCreateToken_WithoutExpiresAt() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
plainToken, accessToken, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil, // 无过期时间
"",
"",
"",
"",
)
s.NoError(err)
s.NotEmpty(plainToken)
s.NotNil(accessToken)
s.Nil(accessToken.ExpiresAt, "无过期时间的 token 应该 ExpiresAt 为 nil")
}
// TestCreateToken_GenerateSessionID 测试自动生成 SessionID
func (s *TokenServiceFeatureTestSuite) TestCreateToken_GenerateSessionID() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
plainToken, accessToken, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil,
"",
"",
"",
"", // 不提供 SessionID,应该自动生成
)
s.NoError(err)
s.NotEmpty(plainToken)
s.NotNil(accessToken)
s.NotEmpty(accessToken.SessionID, "应该自动生成 SessionID")
}
// ==================== FindToken 方法测试 ====================
// TestFindToken_Success 测试查找 token 成功
func (s *TokenServiceFeatureTestSuite) TestFindToken_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建 token
plainToken, _, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil,
"",
"",
"",
"",
)
s.Require().NoError(err)
// 查找 token
foundToken, err := s.service.FindToken(plainToken)
s.NoError(err)
s.NotNil(foundToken)
s.Equal(admin.ID, foundToken.TokenableID)
}
// TestFindToken_EmptyToken 测试空 token
func (s *TokenServiceFeatureTestSuite) TestFindToken_EmptyToken() {
token, err := s.service.FindToken("")
s.Error(err)
s.Nil(token)
// 验证返回的是业务错误
businessErr, ok := apperrors.GetBusinessError(err)
s.True(ok, "应该返回业务错误")
s.Equal("invalid_argument", businessErr.Code)
}
// TestFindToken_NotFound 测试 token 不存在
func (s *TokenServiceFeatureTestSuite) TestFindToken_NotFound() {
// 使用一个不存在的 token
token, err := s.service.FindToken("non-existent-token-12345")
s.Error(err)
s.Nil(token)
}
// TestFindToken_ExpiredToken 测试过期 token
func (s *TokenServiceFeatureTestSuite) TestFindToken_ExpiredToken() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建已过期的 token
pastTime := time.Now().Add(-1 * time.Hour)
plainToken, _, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
&pastTime,
"",
"",
"",
"",
)
s.Require().NoError(err)
// 尝试查找过期 token
token, err := s.service.FindToken(plainToken)
s.Error(err)
s.Nil(token)
// 验证返回的是业务错误
businessErr, ok := apperrors.GetBusinessError(err)
s.True(ok, "应该返回业务错误")
s.Equal("invalid_argument", businessErr.Code)
s.Contains(businessErr.Message, "expired")
// 验证过期 token 已被删除
count, err := facades.Orm().Query().
Model(&models.PersonalAccessToken{}).
Where("tokenable_id", admin.ID).
Where("name", "test_token").
Count()
s.NoError(err)
s.Equal(int64(0), count, "过期 token 应该被删除")
}
// ==================== DeleteToken 方法测试 ====================
// TestDeleteToken_Success 测试删除 token 成功
func (s *TokenServiceFeatureTestSuite) TestDeleteToken_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建 token
plainToken, _, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil,
"",
"",
"",
"",
)
s.Require().NoError(err)
// 删除 token
err = s.service.DeleteToken(plainToken)
s.NoError(err)
// 验证 token 已被删除
foundToken, err := s.service.FindToken(plainToken)
s.Error(err)
s.Nil(foundToken)
}
// ==================== DeleteTokensByUser 方法测试 ====================
// TestDeleteTokensByUser_Success 测试删除用户的所有 token
func (s *TokenServiceFeatureTestSuite) TestDeleteTokensByUser_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建多个 token
for i := 0; i < 3; i++ {
_, _, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil,
"",
"",
"",
"",
)
s.Require().NoError(err)
}
// 验证创建成功
tokens, err := s.service.GetTokensByUser("admin", admin.ID)
s.Require().NoError(err)
s.GreaterOrEqual(len(tokens), 3)
// 删除用户的所有 token
err = s.service.DeleteTokensByUser("admin", admin.ID)
s.NoError(err)
// 验证所有 token 已被删除
tokens, err = s.service.GetTokensByUser("admin", admin.ID)
s.NoError(err)
s.Empty(tokens)
}
// ==================== GetTokensByUser 方法测试 ====================
// TestGetTokensByUser_Success 测试获取用户的所有 token
func (s *TokenServiceFeatureTestSuite) TestGetTokensByUser_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建多个 token
tokenNames := []string{"token1", "token2", "token3"}
for _, name := range tokenNames {
_, _, err := s.service.CreateToken(
"admin",
admin.ID,
name,
nil,
"",
"",
"",
"",
)
s.Require().NoError(err)
time.Sleep(10 * time.Millisecond) // 确保时间戳不同
}
// 获取用户的所有 token
tokens, err := s.service.GetTokensByUser("admin", admin.ID)
s.NoError(err)
s.GreaterOrEqual(len(tokens), len(tokenNames))
// 验证返回了数据(排序验证在数据库层面完成,这里只验证数据存在)
s.NotEmpty(tokens, "应该返回用户的 token")
}
// ==================== UpdateLastUsedAt 方法测试 ====================
// TestUpdateLastUsedAt_Success 测试更新最后使用时间
func (s *TokenServiceFeatureTestSuite) TestUpdateLastUsedAt_Success() {
var admin models.Admin
err := facades.Orm().Query().First(&admin)
s.Require().NoError(err)
// 创建 token
plainToken, accessToken, err := s.service.CreateToken(
"admin",
admin.ID,
"test_token",
nil,
"",
"",
"",
"",
)
s.Require().NoError(err)
initialLastUsedAt := accessToken.LastUsedAt
s.Require().NotNil(initialLastUsedAt)
// 等待一小段时间确保时间不同
time.Sleep(100 * time.Millisecond)
// 更新最后使用时间
err = s.service.UpdateLastUsedAt(plainToken)
s.NoError(err)
// 验证最后使用时间已更新
updatedToken, err := s.service.FindToken(plainToken)
s.NoError(err)
s.NotNil(updatedToken)
s.NotNil(updatedToken.LastUsedAt)
s.True(updatedToken.LastUsedAt.After(*initialLastUsedAt),
"最后使用时间应该已更新")
}
// TestUpdateLastUsedAt_NotFound 测试更新不存在的 token
func (s *TokenServiceFeatureTestSuite) TestUpdateLastUsedAt_NotFound() {
// 尝试更新不存在的 token
err := s.service.UpdateLastUsedAt("non-existent-token")
// 注意:UpdateLastUsedAt 可能不会返回错误,只是不更新任何记录
// 这取决于 ORM 的实现
s.NoError(err) // 或者根据实际实现调整断言
}