Files
honeyDueAPI/internal/repositories/notification_repo_coverage_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

511 lines
14 KiB
Go

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)
}