package repositories import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/testutil" ) func TestNotificationRepository_Create(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") notification := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Task Due Soon", Body: "Your task is due tomorrow", } err := repo.Create(notification) require.NoError(t, err) assert.NotZero(t, notification.ID) } func TestNotificationRepository_FindByID(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") notification := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskOverdue, Title: "Task Overdue", Body: "Your task is overdue", } err := db.Create(notification).Error require.NoError(t, err) found, err := repo.FindByID(notification.ID) require.NoError(t, err) assert.Equal(t, "Task Overdue", found.Title) assert.Equal(t, user.ID, found.UserID) } func TestNotificationRepository_FindByID_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) _, err := repo.FindByID(9999) assert.Error(t, err) } func TestNotificationRepository_FindByUser(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") // Create notifications for user for i := 0; i < 5; i++ { n := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Notification", Body: "Body", } err := db.Create(n).Error require.NoError(t, err) } // Create notification for other user otherN := &models.Notification{ UserID: otherUser.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Other", Body: "Body", } err := db.Create(otherN).Error require.NoError(t, err) // Find all for user notifications, err := repo.FindByUser(user.ID, 0, 0) require.NoError(t, err) assert.Len(t, notifications, 5) // Find with limit notifications, err = repo.FindByUser(user.ID, 3, 0) require.NoError(t, err) assert.Len(t, notifications, 3) // Find with offset notifications, err = repo.FindByUser(user.ID, 3, 3) require.NoError(t, err) assert.Len(t, notifications, 2) // Only 2 remaining } func TestNotificationRepository_MarkAsRead(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") notification := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskCompleted, Title: "Task Completed", Body: "Your task was completed", Read: false, } err := db.Create(notification).Error require.NoError(t, err) err = repo.MarkAsRead(notification.ID) require.NoError(t, err) found, err := repo.FindByID(notification.ID) require.NoError(t, err) assert.True(t, found.Read) assert.NotNil(t, found.ReadAt) } func TestNotificationRepository_MarkAllAsRead(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create multiple unread notifications for i := 0; i < 3; i++ { n := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Unread", Body: "Body", Read: false, } err := db.Create(n).Error require.NoError(t, err) } err := repo.MarkAllAsRead(user.ID) require.NoError(t, err) // Verify all are read count, err := repo.CountUnread(user.ID) require.NoError(t, err) assert.Equal(t, int64(0), count) } func TestNotificationRepository_CountUnread(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create 3 unread and 2 read notifications for i := 0; i < 3; i++ { n := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Unread", Body: "Body", Read: false, } err := db.Create(n).Error require.NoError(t, err) } for i := 0; i < 2; i++ { n := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskCompleted, Title: "Read", Body: "Body", Read: true, } err := db.Create(n).Error require.NoError(t, err) } count, err := repo.CountUnread(user.ID) require.NoError(t, err) assert.Equal(t, int64(3), count) } func TestNotificationRepository_MarkAsSent(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") notification := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "To Send", Body: "Body", Sent: false, } err := db.Create(notification).Error require.NoError(t, err) err = repo.MarkAsSent(notification.ID) require.NoError(t, err) found, err := repo.FindByID(notification.ID) require.NoError(t, err) assert.True(t, found.Sent) assert.NotNil(t, found.SentAt) } func TestNotificationRepository_SetError(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") notification := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Error Notification", Body: "Body", } err := db.Create(notification).Error require.NoError(t, err) err = repo.SetError(notification.ID, "failed to send: connection timeout") require.NoError(t, err) found, err := repo.FindByID(notification.ID) require.NoError(t, err) assert.Equal(t, "failed to send: connection timeout", found.ErrorMessage) } func TestNotificationRepository_GetPendingNotifications(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create pending (unsent) notifications for i := 0; i < 5; i++ { n := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskDueSoon, Title: "Pending", Body: "Body", Sent: false, } err := db.Create(n).Error require.NoError(t, err) } // Create sent notification sent := &models.Notification{ UserID: user.ID, NotificationType: models.NotificationTaskCompleted, Title: "Already Sent", Body: "Body", Sent: true, } err := db.Create(sent).Error require.NoError(t, err) pending, err := repo.GetPendingNotifications(10) require.NoError(t, err) assert.Len(t, pending, 5) // Only unsent // Test with limit pending, err = repo.GetPendingNotifications(3) require.NoError(t, err) assert.Len(t, pending, 3) } func TestNotificationRepository_APNSDevice_CRUD(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create APNS device device := &models.APNSDevice{ Name: "iPhone 15", Active: true, UserID: &user.ID, DeviceID: "device-uuid-123", RegistrationID: "apns-token-abc123", } err := repo.CreateAPNSDevice(device) require.NoError(t, err) assert.NotZero(t, device.ID) // Find by ID found, err := repo.FindAPNSDeviceByID(device.ID) require.NoError(t, err) assert.Equal(t, "iPhone 15", found.Name) assert.Equal(t, "apns-token-abc123", found.RegistrationID) // Find by token foundByToken, err := repo.FindAPNSDeviceByToken("apns-token-abc123") require.NoError(t, err) assert.Equal(t, device.ID, foundByToken.ID) // Find by user devices, err := repo.FindAPNSDevicesByUser(user.ID) require.NoError(t, err) assert.Len(t, devices, 1) // Update device.Name = "iPhone 16" err = repo.UpdateAPNSDevice(device) require.NoError(t, err) found, err = repo.FindAPNSDeviceByID(device.ID) require.NoError(t, err) assert.Equal(t, "iPhone 16", found.Name) // Deactivate err = repo.DeactivateAPNSDevice(device.ID) require.NoError(t, err) // Should not appear in active devices devices, err = repo.FindAPNSDevicesByUser(user.ID) require.NoError(t, err) assert.Len(t, devices, 0) // Delete err = repo.DeleteAPNSDevice(device.ID) require.NoError(t, err) _, err = repo.FindAPNSDeviceByID(device.ID) assert.Error(t, err) } func TestNotificationRepository_GCMDevice_CRUD(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create GCM device device := &models.GCMDevice{ Name: "Pixel 8", Active: true, UserID: &user.ID, DeviceID: "android-device-uuid", RegistrationID: "fcm-token-xyz789", CloudMessageType: "FCM", } err := repo.CreateGCMDevice(device) require.NoError(t, err) assert.NotZero(t, device.ID) // Find by ID found, err := repo.FindGCMDeviceByID(device.ID) require.NoError(t, err) assert.Equal(t, "Pixel 8", found.Name) assert.Equal(t, "fcm-token-xyz789", found.RegistrationID) // Find by token foundByToken, err := repo.FindGCMDeviceByToken("fcm-token-xyz789") require.NoError(t, err) assert.Equal(t, device.ID, foundByToken.ID) // Find by user devices, err := repo.FindGCMDevicesByUser(user.ID) require.NoError(t, err) assert.Len(t, devices, 1) // Update device.Name = "Pixel 9" err = repo.UpdateGCMDevice(device) require.NoError(t, err) found, err = repo.FindGCMDeviceByID(device.ID) require.NoError(t, err) assert.Equal(t, "Pixel 9", found.Name) // Deactivate err = repo.DeactivateGCMDevice(device.ID) require.NoError(t, err) devices, err = repo.FindGCMDevicesByUser(user.ID) require.NoError(t, err) assert.Len(t, devices, 0) // Delete err = repo.DeleteGCMDevice(device.ID) require.NoError(t, err) _, err = repo.FindGCMDeviceByID(device.ID) assert.Error(t, err) } func TestNotificationRepository_GetActiveTokensForUser(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create APNS devices apns1 := &models.APNSDevice{ Active: true, UserID: &user.ID, RegistrationID: "ios-token-1", } err := db.Create(apns1).Error require.NoError(t, err) apns2 := &models.APNSDevice{ Active: true, UserID: &user.ID, RegistrationID: "ios-token-2", } err = db.Create(apns2).Error require.NoError(t, err) // Create inactive APNS device (should not be returned) // Insert inactive device via raw SQL to bypass GORM's default:true override err = db.Exec("INSERT INTO push_notifications_apnsdevice (active, user_id, registration_id, date_created) VALUES (false, ?, 'ios-token-inactive', CURRENT_TIMESTAMP)", user.ID).Error require.NoError(t, err) // Create GCM device gcm1 := &models.GCMDevice{ Active: true, UserID: &user.ID, RegistrationID: "android-token-1", CloudMessageType: "FCM", } err = db.Create(gcm1).Error require.NoError(t, err) iosTokens, androidTokens, err := repo.GetActiveTokensForUser(user.ID) require.NoError(t, err) assert.Len(t, iosTokens, 2) assert.Contains(t, iosTokens, "ios-token-1") assert.Contains(t, iosTokens, "ios-token-2") assert.Len(t, androidTokens, 1) assert.Contains(t, androidTokens, "android-token-1") } func TestNotificationRepository_GetActiveTokensForUser_NoDevices(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") iosTokens, androidTokens, err := repo.GetActiveTokensForUser(user.ID) require.NoError(t, err) assert.Empty(t, iosTokens) assert.Empty(t, androidTokens) } func TestNotificationRepository_FindPreferencesByUser(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create preferences with defaults (GORM applies default:true for bools) prefs := &models.NotificationPreference{ UserID: user.ID, } err := db.Create(prefs).Error require.NoError(t, err) // Update one field to false via raw SQL to bypass GORM default err = db.Exec("UPDATE notifications_notificationpreference SET task_due_soon = false WHERE user_id = ?", user.ID).Error require.NoError(t, err) found, err := repo.FindPreferencesByUser(user.ID) require.NoError(t, err) assert.Equal(t, user.ID, found.UserID) assert.False(t, found.TaskDueSoon) assert.True(t, found.TaskOverdue) } func TestNotificationRepository_FindPreferencesByUser_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) _, err := repo.FindPreferencesByUser(9999) assert.Error(t, err) } func TestNotificationRepository_UpdatePreferences(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewNotificationRepository(db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") prefs, err := repo.GetOrCreatePreferences(user.ID) require.NoError(t, err) prefs.TaskDueSoon = false prefs.DailyDigest = false err = repo.UpdatePreferences(prefs) require.NoError(t, err) found, err := repo.FindPreferencesByUser(user.ID) require.NoError(t, err) assert.False(t, found.TaskDueSoon) assert.False(t, found.DailyDigest) }