package service import ( "context" "encoding/json" "fmt" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ========== GetNotification ========== func TestNotificationService_GetNotification_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) notification := createTestNotification(t, db, user.ID, &account.ID, "conversation_created") result, err := svc.GetNotification(context.Background(), notification.ID) require.NoError(t, err) assert.Equal(t, notification.ID, result.ID) assert.Equal(t, user.ID, result.UserID) assert.Equal(t, "conversation_created", result.NotificationType) } func TestNotificationService_GetNotification_不存在(t *testing.T) { _, _, _, svc := setupNotificationService(t) result, err := svc.GetNotification(context.Background(), 9999) assert.Error(t, err) assert.Nil(t, result) } // ========== ListNotifications ========== func TestNotificationService_ListNotifications_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) // 创建多条通知 createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "conversation_assigned") notifications, total, err := svc.ListNotifications(context.Background(), user.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, notifications, 2) } func TestNotificationService_ListNotifications_分页(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) // 创建3条通知 for i := 0; i < 3; i++ { createTestNotification(t, db, user.ID, &account.ID, "conversation_created") } // 第一页,每页2条 notifications, total, err := svc.ListNotifications(context.Background(), user.ID, 1, 2) require.NoError(t, err) assert.Equal(t, int64(3), total) assert.Len(t, notifications, 2) // 第二页 notifications2, total2, err2 := svc.ListNotifications(context.Background(), user.ID, 2, 2) require.NoError(t, err2) assert.Equal(t, int64(3), total2) assert.Len(t, notifications2, 1) } func TestNotificationService_ListNotifications_无通知(t *testing.T) { _, _, _, svc := setupNotificationService(t) notifications, total, err := svc.ListNotifications(context.Background(), 9999, 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), total) assert.Empty(t, notifications) } // ========== ListNotificationsByAccount ========== func TestNotificationService_ListNotificationsByAccount_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "message_created") notifications, total, err := svc.ListNotificationsByAccount(context.Background(), user.ID, account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, notifications, 2) } func TestNotificationService_ListNotificationsByAccount_不同账户隔离(t *testing.T) { db, _, _, svc := setupNotificationService(t) account1 := createTestAccount(t, db) account2 := createTestAccount(t, db) user := createTestUser(t, db, account1.ID) // 只在 account1 下创建通知 createTestNotification(t, db, user.ID, &account1.ID, "conversation_created") // 查询 account2,应该无通知 notifications, total, err := svc.ListNotificationsByAccount(context.Background(), user.ID, account2.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), total) assert.Empty(t, notifications) } func TestNotificationService_ListNotificationsByAccount_分页(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) for i := 0; i < 5; i++ { createTestNotification(t, db, user.ID, &account.ID, "conversation_created") } notifications, total, err := svc.ListNotificationsByAccount(context.Background(), user.ID, account.ID, 1, 3) require.NoError(t, err) assert.Equal(t, int64(5), total) assert.Len(t, notifications, 3) } // ========== CreateNotification ========== func TestNotificationService_CreateNotification_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) notification := &model.Notification{ UserID: user.ID, AccountID: &account.ID, NotificationType: "conversation_assigned", PrimaryActorType: "User", PrimaryActorID: user.ID, PushEnabled: true, EmailEnabled: false, } err := svc.CreateNotification(context.Background(), notification) require.NoError(t, err) assert.NotZero(t, notification.ID) // 验证数据库中确实存在 var count int64 db.Model(&model.Notification{}).Where("id = ?", notification.ID).Count(&count) assert.Equal(t, int64(1), count) } func TestNotificationService_CreateNotification_缺少必填字段(t *testing.T) { _, _, _, svc := setupNotificationService(t) notification := &model.Notification{ NotificationType: "conversation_created", UserID: 0, // SQLite doesn't enforce NOT NULL on uint(0), so this succeeds } err := svc.CreateNotification(context.Background(), notification) // SQLite does not enforce NOT NULL constraints on zero values for integer fields, // so the create succeeds even with UserID=0. In production (Postgres), this would fail. assert.NoError(t, err) } // ========== MarkRead ========== func TestNotificationService_MarkRead_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) notification := createTestNotification(t, db, user.ID, &account.ID, "conversation_created") // 初始状态:未读 assert.Nil(t, notification.ReadAt) err := svc.MarkRead(context.Background(), notification.ID) require.NoError(t, err) // 验证已标记为已读 result, err := svc.GetNotification(context.Background(), notification.ID) require.NoError(t, err) assert.NotNil(t, result.ReadAt) } func TestNotificationService_MarkRead_不存在(t *testing.T) { _, _, _, svc := setupNotificationService(t) err := svc.MarkRead(context.Background(), 9999) // 对不存在的记录标记已读,repo 返回 gorm.ErrRecordNotFound assert.Error(t, err) } // ========== MarkAllRead ========== func TestNotificationService_MarkAllRead_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "message_created") err := svc.MarkAllRead(context.Background(), user.ID) require.NoError(t, err) // 验证所有通知都已标记为已读 notifications, _, err := svc.ListNotifications(context.Background(), user.ID, 1, 10) require.NoError(t, err) for _, n := range notifications { assert.NotNil(t, n.ReadAt) } } func TestNotificationService_MarkAllRead_无通知用户(t *testing.T) { _, _, _, svc := setupNotificationService(t) // 对没有任何通知的用户标记全部已读,不应报错 err := svc.MarkAllRead(context.Background(), 9999) require.NoError(t, err) } // ========== MarkAllReadByAccount ========== func TestNotificationService_MarkAllReadByAccount_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "message_created") err := svc.MarkAllReadByAccount(context.Background(), user.ID, account.ID) require.NoError(t, err) // 验证该账户下的所有通知都已标记为已读 notifications, _, err := svc.ListNotificationsByAccount(context.Background(), user.ID, account.ID, 1, 10) require.NoError(t, err) for _, n := range notifications { assert.NotNil(t, n.ReadAt) } } func TestNotificationService_MarkAllReadByAccount_不同账户隔离(t *testing.T) { db, _, _, svc := setupNotificationService(t) account1 := createTestAccount(t, db) account2 := createTestAccount(t, db) user := createTestUser(t, db, account1.ID) // 在 account1 下创建通知 notif1 := createTestNotification(t, db, user.ID, &account1.ID, "conversation_created") // 在 account2 下创建通知 notif2 := createTestNotification(t, db, user.ID, &account2.ID, "message_created") // 只标记 account1 下的通知为已读 err := svc.MarkAllReadByAccount(context.Background(), user.ID, account1.ID) require.NoError(t, err) // account1 的通知应为已读 result1, err := svc.GetNotification(context.Background(), notif1.ID) require.NoError(t, err) assert.NotNil(t, result1.ReadAt) // account2 的通知应仍为未读 result2, err := svc.GetNotification(context.Background(), notif2.ID) require.NoError(t, err) assert.Nil(t, result2.ReadAt) } // ========== DeleteNotification ========== func TestNotificationService_DeleteNotification_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) notification := createTestNotification(t, db, user.ID, &account.ID, "conversation_created") err := svc.DeleteNotification(context.Background(), notification.ID) require.NoError(t, err) // 验证软删除后无法通过 GetNotification 获取 result, err := svc.GetNotification(context.Background(), notification.ID) assert.Error(t, err) assert.Nil(t, result) } func TestNotificationService_DeleteNotification_不存在(t *testing.T) { _, _, _, svc := setupNotificationService(t) // GORM soft-delete on a non-existent record returns nil (affects 0 rows but no error) err := svc.DeleteNotification(context.Background(), 9999) assert.NoError(t, err) } // ========== GetUnreadCount ========== func TestNotificationService_GetUnreadCount_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "message_created") count, err := svc.GetUnreadCount(context.Background(), user.ID) require.NoError(t, err) assert.Equal(t, int64(2), count) } func TestNotificationService_GetUnreadCount_标记已读后减少(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) notif1 := createTestNotification(t, db, user.ID, &account.ID, "conversation_created") createTestNotification(t, db, user.ID, &account.ID, "message_created") // 标记一条为已读 err := svc.MarkRead(context.Background(), notif1.ID) require.NoError(t, err) count, err := svc.GetUnreadCount(context.Background(), user.ID) require.NoError(t, err) assert.Equal(t, int64(1), count) } func TestNotificationService_GetUnreadCount_无通知(t *testing.T) { _, _, _, svc := setupNotificationService(t) count, err := svc.GetUnreadCount(context.Background(), 9999) require.NoError(t, err) assert.Equal(t, int64(0), count) } // ========== GetPreferences ========== func TestNotificationService_GetPreferences_成功(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) // 直接通过 repo 创建偏好 pref1 := &model.NotificationPreference{ UserID: user.ID, AccountID: &account.ID, Channel: "email", EventType: "conversation_created", Enabled: true, } require.NoError(t, db.Create(pref1).Error) pref2 := &model.NotificationPreference{ UserID: user.ID, AccountID: &account.ID, Channel: "push", EventType: "message_created", Enabled: false, } require.NoError(t, db.Create(pref2).Error) prefs, err := svc.GetPreferences(context.Background(), user.ID, account.ID) require.NoError(t, err) assert.Len(t, prefs, 2) } func TestNotificationService_GetPreferences_无偏好(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) prefs, err := svc.GetPreferences(context.Background(), 9999, account.ID) require.NoError(t, err) assert.Empty(t, prefs) } // ========== UpdatePreferences ========== func TestNotificationService_UpdatePreferences_创建新偏好(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) prefs := []model.NotificationPreference{ { UserID: user.ID, AccountID: &account.ID, Channel: "email", EventType: "conversation_created", Enabled: true, }, { UserID: user.ID, AccountID: &account.ID, Channel: "push", EventType: "conversation_assigned", Enabled: false, }, } err := svc.UpdatePreferences(context.Background(), user.ID, account.ID, prefs) require.NoError(t, err) // 验证偏好已创建 result, err := svc.GetPreferences(context.Background(), user.ID, account.ID) require.NoError(t, err) assert.Len(t, result, 2) } func TestNotificationService_UpdatePreferences_更新已有偏好(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) // 先创建偏好 prefs := []model.NotificationPreference{ { UserID: user.ID, AccountID: &account.ID, Channel: "email", EventType: "conversation_created", Enabled: true, }, } err := svc.UpdatePreferences(context.Background(), user.ID, account.ID, prefs) require.NoError(t, err) // 更新偏好(将 Enabled 改为 false) updatedPrefs := []model.NotificationPreference{ { UserID: user.ID, AccountID: &account.ID, Channel: "email", EventType: "conversation_created", Enabled: false, }, } err = svc.UpdatePreferences(context.Background(), user.ID, account.ID, updatedPrefs) require.NoError(t, err) // 验证偏好已更新 result, err := svc.GetPreferences(context.Background(), user.ID, account.ID) require.NoError(t, err) assert.Len(t, result, 1) assert.False(t, result[0].Enabled) } func TestNotificationService_UpdatePreferences_强制设置UserID和AccountID(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) // 即使传入的偏好中 UserID 为 0 或 AccountID 为 nil, // UpdatePreferences 应强制设置正确的 UserID 和 AccountID prefs := []model.NotificationPreference{ { Channel: "email", EventType: "conversation_created", Enabled: true, }, } err := svc.UpdatePreferences(context.Background(), user.ID, account.ID, prefs) require.NoError(t, err) // 验证 UserID 和 AccountID 被正确设置 result, err := svc.GetPreferences(context.Background(), user.ID, account.ID) require.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, user.ID, result[0].UserID) assert.NotNil(t, result[0].AccountID) assert.Equal(t, account.ID, *result[0].AccountID) } func TestNotificationService_UpdatePreferences_批量创建多种偏好(t *testing.T) { db, _, _, svc := setupNotificationService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) prefs := []model.NotificationPreference{ {Channel: "email", EventType: "conversation_created", Enabled: true, Preferences: json.RawMessage(`{"sound": true}`)}, {Channel: "push", EventType: "conversation_created", Enabled: true, Preferences: json.RawMessage(`{"vibration": true}`)}, {Channel: "browser", EventType: "conversation_assigned", Enabled: false, Preferences: json.RawMessage(`{}`)}, {Channel: "email", EventType: "message_created", Enabled: true, Preferences: json.RawMessage(`{}`)}, } err := svc.UpdatePreferences(context.Background(), user.ID, account.ID, prefs) require.NoError(t, err) result, err := svc.GetPreferences(context.Background(), user.ID, account.ID) require.NoError(t, err) assert.Len(t, result, 4) } // --- G8 extension service tests: Snooze, Unread, DeleteAll --- // Reference: Chatwoot notifications_controller.rb#snooze, #unread, #destroy_all func TestNotificationService_SnoozeNotification(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user := createTestUser(t, db, accountID) notif := createTestNotification(t, db, user.ID, &accountID, "conversation_assignment") // Mark notification as read first readTime := time.Now() require.NoError(t, db.Model(notif).Update("read_at", &readTime).Error) // Snooze it — should clear read_at and set snoozed_until snoozeUntil := time.Now().Add(2 * time.Hour) result, err := svc.SnoozeNotification(context.Background(), notif.ID, user.ID, accountID, snoozeUntil) require.NoError(t, err) assert.NotNil(t, result) assert.NotNil(t, result.SnoozedUntil) assert.Nil(t, result.ReadAt) // snooze clears read status } func TestNotificationService_SnoozeNotification_WrongUser(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user := createTestUser(t, db, accountID) otherUser := createTestUser(t, db, accountID) notif := createTestNotification(t, db, user.ID, &accountID, "conversation_assignment") snoozeUntil := time.Now().Add(2 * time.Hour) _, err := svc.SnoozeNotification(context.Background(), notif.ID, otherUser.ID, accountID, snoozeUntil) assert.Error(t, err) // should fail — wrong user } func TestNotificationService_MarkNotificationUnread(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user := createTestUser(t, db, accountID) notif := createTestNotification(t, db, user.ID, &accountID, "conversation_assignment") // Mark notification as read first readTime := time.Now() require.NoError(t, db.Model(notif).Update("read_at", &readTime).Error) // Mark it unread — should clear read_at result, err := svc.MarkNotificationUnread(context.Background(), notif.ID, user.ID, accountID) require.NoError(t, err) assert.NotNil(t, result) assert.Nil(t, result.ReadAt) } func TestNotificationService_MarkNotificationUnread_WrongUser(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user := createTestUser(t, db, accountID) otherUser := createTestUser(t, db, accountID) notif := createTestNotification(t, db, user.ID, &accountID, "conversation_assignment") _, err := svc.MarkNotificationUnread(context.Background(), notif.ID, otherUser.ID, accountID) assert.Error(t, err) } func TestNotificationService_DeleteAllNotifications(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user := createTestUser(t, db, accountID) // Create multiple notifications for this user for i := 0; i < 5; i++ { createTestNotification(t, db, user.ID, &accountID, fmt.Sprintf("type_%d", i)) } // Verify they exist var count int64 require.NoError(t, db.Model(&model.Notification{}).Where("user_id = ? AND account_id = ?", user.ID, accountID).Count(&count).Error) assert.Equal(t, int64(5), count) // Delete all err := svc.DeleteAllNotifications(context.Background(), user.ID, accountID) require.NoError(t, err) // Verify all deleted require.NoError(t, db.Model(&model.Notification{}).Where("user_id = ? AND account_id = ?", user.ID, accountID).Count(&count).Error) assert.Equal(t, int64(0), count) } func TestNotificationService_DeleteAllNotifications_OnlyDeletesForUser(t *testing.T) { db, _, _, svc := setupNotificationService(t) accountID := uint(1) user1 := createTestUser(t, db, accountID) user2 := createTestUser(t, db, accountID) // Create notifications for both users for i := 0; i < 3; i++ { createTestNotification(t, db, user1.ID, &accountID, fmt.Sprintf("user1_type_%d", i)) createTestNotification(t, db, user2.ID, &accountID, fmt.Sprintf("user2_type_%d", i)) } // Delete all for user1 only err := svc.DeleteAllNotifications(context.Background(), user1.ID, accountID) require.NoError(t, err) // user1's notifications should be gone var count1 int64 require.NoError(t, db.Model(&model.Notification{}).Where("user_id = ?", user1.ID).Count(&count1).Error) assert.Equal(t, int64(0), count1) // user2's notifications should remain var count2 int64 require.NoError(t, db.Model(&model.Notification{}).Where("user_id = ?", user2.ID).Count(&count2).Error) assert.Equal(t, int64(3), count2) }