- 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>
511 lines
14 KiB
Go
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)
|
|
}
|