- 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>
1185 lines
38 KiB
Go
1185 lines
38 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
"github.com/treytartt/honeydue-api/internal/push"
|
|
"github.com/treytartt/honeydue-api/internal/repositories"
|
|
"github.com/treytartt/honeydue-api/internal/testutil"
|
|
)
|
|
|
|
func setupNotificationService(t *testing.T) (*NotificationService, *repositories.NotificationRepository) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
// pushClient is nil for testing (no actual push sends)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
return service, notifRepo
|
|
}
|
|
|
|
// === GetNotifications ===
|
|
|
|
func TestNotificationService_GetNotifications(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create some notifications
|
|
for i := 0; i < 3; i++ {
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test Notification",
|
|
Body: "Some task is due soon",
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
resp, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp, 3)
|
|
}
|
|
|
|
func TestNotificationService_GetNotifications_Empty(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
resp, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp)
|
|
}
|
|
|
|
func TestNotificationService_GetNotifications_Pagination(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
for i := 0; i < 5; i++ {
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test",
|
|
Body: "Body",
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Get first 2
|
|
resp, err := service.GetNotifications(user.ID, 2, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp, 2)
|
|
}
|
|
|
|
// === GetUnreadCount ===
|
|
|
|
func TestNotificationService_GetUnreadCount(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create 3 unread notifications
|
|
for i := 0; i < 3; i++ {
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Unread",
|
|
Body: "Body",
|
|
Read: false,
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
count, err := service.GetUnreadCount(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3), count)
|
|
}
|
|
|
|
func TestNotificationService_GetUnreadCount_Zero(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
count, err := service.GetUnreadCount(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
// === MarkAsRead ===
|
|
|
|
func TestNotificationService_MarkAsRead(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test",
|
|
Body: "Body",
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.MarkAsRead(notif.ID, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify unread count is 0
|
|
count, err := service.GetUnreadCount(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
func TestNotificationService_MarkAsRead_WrongUser(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
|
|
|
|
notif := &models.Notification{
|
|
UserID: owner.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Private",
|
|
Body: "Body",
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.MarkAsRead(notif.ID, other.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.notification_not_found")
|
|
}
|
|
|
|
func TestNotificationService_MarkAsRead_NotFound(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.MarkAsRead(9999, user.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.notification_not_found")
|
|
}
|
|
|
|
// === MarkAllAsRead ===
|
|
|
|
func TestNotificationService_MarkAllAsRead(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create unread notifications
|
|
for i := 0; i < 3; i++ {
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Unread",
|
|
Body: "Body",
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
err := service.MarkAllAsRead(user.ID)
|
|
require.NoError(t, err)
|
|
|
|
count, err := service.GetUnreadCount(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
}
|
|
|
|
// === CreateAndSendNotification ===
|
|
|
|
func TestNotificationService_CreateAndSendNotification(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil) // nil push client = no actual push
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
data := map[string]interface{}{
|
|
"task_id": 123,
|
|
}
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", data)
|
|
require.NoError(t, err)
|
|
|
|
// Verify notification was created
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
assert.Equal(t, "Due Soon", notifs[0].Title)
|
|
assert.Equal(t, "Fix faucet", notifs[0].Body)
|
|
}
|
|
|
|
func TestNotificationService_CreateAndSendNotification_DisabledPreference(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create preferences with task_due_soon disabled
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.TaskDueSoon = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify no notification was created (silently skipped)
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
// === Preferences ===
|
|
|
|
func TestNotificationService_GetPreferences(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
resp, err := service.GetPreferences(user.ID)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, resp)
|
|
// Defaults should all be true
|
|
assert.True(t, resp.TaskDueSoon)
|
|
assert.True(t, resp.TaskOverdue)
|
|
assert.True(t, resp.TaskCompleted)
|
|
}
|
|
|
|
func TestNotificationService_UpdatePreferences(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
falseVal := false
|
|
req := &UpdatePreferencesRequest{
|
|
TaskDueSoon: &falseVal,
|
|
}
|
|
|
|
resp, err := service.UpdatePreferences(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.False(t, resp.TaskDueSoon)
|
|
assert.True(t, resp.TaskOverdue) // unchanged
|
|
}
|
|
|
|
func TestNotificationService_UpdatePreferences_InvalidHour(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
invalidHour := 25
|
|
req := &UpdatePreferencesRequest{
|
|
TaskDueSoonHour: &invalidHour,
|
|
}
|
|
|
|
_, err := service.UpdatePreferences(user.ID, req)
|
|
testutil.AssertAppErrorCode(t, err, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestNotificationService_UpdatePreferences_ValidHour(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
hour := 9
|
|
req := &UpdatePreferencesRequest{
|
|
TaskDueSoonHour: &hour,
|
|
}
|
|
|
|
resp, err := service.UpdatePreferences(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 9, *resp.TaskDueSoonHour)
|
|
}
|
|
|
|
// === RegisterDevice ===
|
|
|
|
func TestNotificationService_RegisterDevice_iOS(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
req := &RegisterDeviceRequest{
|
|
Name: "iPhone 15",
|
|
DeviceID: "device-abc",
|
|
RegistrationID: "token-xyz",
|
|
Platform: push.PlatformIOS,
|
|
}
|
|
|
|
resp, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "iPhone 15", resp.Name)
|
|
assert.Equal(t, push.PlatformIOS, resp.Platform)
|
|
assert.True(t, resp.Active)
|
|
}
|
|
|
|
func TestNotificationService_RegisterDevice_Android(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
req := &RegisterDeviceRequest{
|
|
Name: "Pixel 8",
|
|
DeviceID: "device-def",
|
|
RegistrationID: "token-abc",
|
|
Platform: push.PlatformAndroid,
|
|
}
|
|
|
|
resp, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Pixel 8", resp.Name)
|
|
assert.Equal(t, push.PlatformAndroid, resp.Platform)
|
|
assert.True(t, resp.Active)
|
|
}
|
|
|
|
func TestNotificationService_RegisterDevice_InvalidPlatform(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
req := &RegisterDeviceRequest{
|
|
Name: "Unknown",
|
|
DeviceID: "device-bad",
|
|
RegistrationID: "token-bad",
|
|
Platform: "windows",
|
|
}
|
|
|
|
_, err := service.RegisterDevice(user.ID, req)
|
|
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
|
|
}
|
|
|
|
func TestNotificationService_RegisterDevice_UpdateExisting(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Register a device
|
|
req := &RegisterDeviceRequest{
|
|
Name: "iPhone 15",
|
|
DeviceID: "device-abc",
|
|
RegistrationID: "token-xyz",
|
|
Platform: push.PlatformIOS,
|
|
}
|
|
_, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
|
|
// Re-register with same token (should update, not duplicate)
|
|
req.Name = "iPhone 15 Pro"
|
|
resp, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "iPhone 15 Pro", resp.Name)
|
|
}
|
|
|
|
// === ListDevices ===
|
|
|
|
func TestNotificationService_ListDevices(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Register iOS and Android devices
|
|
iosDevice := &models.APNSDevice{
|
|
UserID: &user.ID,
|
|
Name: "iPhone",
|
|
DeviceID: "d1",
|
|
RegistrationID: "t1",
|
|
Active: true,
|
|
}
|
|
err := db.Create(iosDevice).Error
|
|
require.NoError(t, err)
|
|
|
|
androidDevice := &models.GCMDevice{
|
|
UserID: &user.ID,
|
|
Name: "Pixel",
|
|
DeviceID: "d2",
|
|
RegistrationID: "t2",
|
|
CloudMessageType: "FCM",
|
|
Active: true,
|
|
}
|
|
err = db.Create(androidDevice).Error
|
|
require.NoError(t, err)
|
|
|
|
resp, err := service.ListDevices(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp, 2)
|
|
}
|
|
|
|
func TestNotificationService_ListDevices_Empty(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
resp, err := service.ListDevices(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp)
|
|
}
|
|
|
|
// === DeleteDevice - Invalid Platform ===
|
|
|
|
func TestDeleteDevice_InvalidPlatform_Returns400(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.DeleteDevice(1, "windows", user.ID)
|
|
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
|
|
}
|
|
|
|
// === UnregisterDevice ===
|
|
|
|
func TestNotificationService_UnregisterDevice_iOS(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
device := &models.APNSDevice{
|
|
UserID: &user.ID,
|
|
Name: "iPhone",
|
|
DeviceID: "d1",
|
|
RegistrationID: "reg-token-ios",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.UnregisterDevice("reg-token-ios", push.PlatformIOS, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify device is deactivated
|
|
var found models.APNSDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.False(t, found.Active)
|
|
}
|
|
|
|
func TestNotificationService_UnregisterDevice_Android(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
device := &models.GCMDevice{
|
|
UserID: &user.ID,
|
|
Name: "Pixel",
|
|
DeviceID: "d2",
|
|
RegistrationID: "reg-token-android",
|
|
CloudMessageType: "FCM",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.UnregisterDevice("reg-token-android", push.PlatformAndroid, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
var found models.GCMDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.False(t, found.Active)
|
|
}
|
|
|
|
func TestNotificationService_UnregisterDevice_NotFound(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.UnregisterDevice("nonexistent-token", push.PlatformIOS, user.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
|
|
}
|
|
|
|
func TestNotificationService_UnregisterDevice_WrongUser(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "Password123")
|
|
|
|
device := &models.APNSDevice{
|
|
UserID: &owner.ID,
|
|
Name: "iPhone",
|
|
DeviceID: "d1",
|
|
RegistrationID: "owner-token",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.UnregisterDevice("owner-token", push.PlatformIOS, attacker.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
|
|
}
|
|
|
|
func TestNotificationService_UnregisterDevice_InvalidPlatform(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.UnregisterDevice("some-token", "windows", user.ID)
|
|
testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_platform")
|
|
}
|
|
|
|
// === UpdateUserTimezone ===
|
|
|
|
func TestNotificationService_UpdateUserTimezone(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Should not panic, just silently update
|
|
service.UpdateUserTimezone(user.ID, "America/Los_Angeles")
|
|
|
|
// Verify timezone was stored
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prefs.Timezone)
|
|
assert.Equal(t, "America/Los_Angeles", *prefs.Timezone)
|
|
}
|
|
|
|
func TestNotificationService_UpdateUserTimezone_Invalid(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Invalid timezone should be silently ignored
|
|
service.UpdateUserTimezone(user.ID, "Invalid/Timezone")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, prefs.Timezone) // Should not have been set
|
|
}
|
|
|
|
func TestNotificationService_UpdateUserTimezone_NoChangeSkipsWrite(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Set timezone
|
|
service.UpdateUserTimezone(user.ID, "America/New_York")
|
|
|
|
// Set same timezone again — should be a no-op
|
|
service.UpdateUserTimezone(user.ID, "America/New_York")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prefs.Timezone)
|
|
assert.Equal(t, "America/New_York", *prefs.Timezone)
|
|
}
|
|
|
|
func TestDeleteDevice_WrongUser_Returns403(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "password")
|
|
|
|
// Register an iOS device for the owner
|
|
device := &models.APNSDevice{
|
|
UserID: &owner.ID,
|
|
Name: "Owner iPhone",
|
|
DeviceID: "device-123",
|
|
RegistrationID: "token-abc",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
// Attacker tries to deactivate the owner's device
|
|
err = service.DeleteDevice(device.ID, push.PlatformIOS, attacker.ID)
|
|
require.Error(t, err, "should not allow deleting another user's device")
|
|
testutil.AssertAppErrorCode(t, err, http.StatusForbidden)
|
|
|
|
// Verify the device is still active
|
|
var found models.APNSDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.True(t, found.Active, "device should still be active after failed deletion")
|
|
}
|
|
|
|
func TestDeleteDevice_CorrectUser_Succeeds(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
|
|
// Register an iOS device for the owner
|
|
device := &models.APNSDevice{
|
|
UserID: &owner.ID,
|
|
Name: "Owner iPhone",
|
|
DeviceID: "device-123",
|
|
RegistrationID: "token-abc",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
// Owner deactivates their own device
|
|
err = service.DeleteDevice(device.ID, push.PlatformIOS, owner.ID)
|
|
require.NoError(t, err, "owner should be able to deactivate their own device")
|
|
|
|
// Verify the device is now inactive
|
|
var found models.APNSDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.False(t, found.Active, "device should be deactivated")
|
|
}
|
|
|
|
func TestDeleteDevice_WrongUser_Android_Returns403(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "password")
|
|
|
|
// Register an Android device for the owner
|
|
device := &models.GCMDevice{
|
|
UserID: &owner.ID,
|
|
Name: "Owner Pixel",
|
|
DeviceID: "device-456",
|
|
RegistrationID: "token-def",
|
|
CloudMessageType: "FCM",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
// Attacker tries to deactivate the owner's Android device
|
|
err = service.DeleteDevice(device.ID, push.PlatformAndroid, attacker.ID)
|
|
require.Error(t, err, "should not allow deleting another user's Android device")
|
|
testutil.AssertAppErrorCode(t, err, http.StatusForbidden)
|
|
|
|
// Verify the device is still active
|
|
var found models.GCMDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.True(t, found.Active, "Android device should still be active after failed deletion")
|
|
}
|
|
|
|
func TestDeleteDevice_NonExistent_Returns404(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
|
|
err := service.DeleteDevice(99999, push.PlatformIOS, user.ID)
|
|
require.Error(t, err, "should return error for non-existent device")
|
|
testutil.AssertAppErrorCode(t, err, http.StatusNotFound)
|
|
}
|
|
|
|
// === CreateAndSendNotification — all notification types ===
|
|
|
|
func TestNotificationService_CreateAndSend_TaskOverdue(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskOverdue, "Overdue", "Task is overdue", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
assert.Equal(t, "Overdue", notifs[0].Title)
|
|
}
|
|
|
|
func TestNotificationService_CreateAndSend_TaskCompleted(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskCompleted, "Completed", "Task done", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
}
|
|
|
|
func TestNotificationService_CreateAndSend_TaskAssigned(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskAssigned, "Assigned", "Task assigned to you", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
}
|
|
|
|
func TestNotificationService_CreateAndSend_ResidenceShared(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationResidenceShared, "Shared", "Someone shared a home", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
}
|
|
|
|
func TestNotificationService_CreateAndSend_WarrantyExpiring(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationWarrantyExpiring, "Expiring", "Warranty expiring soon", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
}
|
|
|
|
// === CreateAndSendNotification — disabled preference for each type ===
|
|
|
|
func TestNotificationService_DisabledPrefs_TaskOverdue(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.TaskOverdue = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskOverdue, "Overdue", "Overdue task", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
func TestNotificationService_DisabledPrefs_TaskCompleted(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.TaskCompleted = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskCompleted, "Completed", "Task done", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
func TestNotificationService_DisabledPrefs_TaskAssigned(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.TaskAssigned = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskAssigned, "Assigned", "Task assigned", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
func TestNotificationService_DisabledPrefs_ResidenceShared(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.ResidenceShared = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationResidenceShared, "Shared", "Home shared", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
func TestNotificationService_DisabledPrefs_WarrantyExpiring(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
prefs, err := notifRepo.GetOrCreatePreferences(user.ID)
|
|
require.NoError(t, err)
|
|
prefs.WarrantyExpiring = false
|
|
err = notifRepo.UpdatePreferences(prefs)
|
|
require.NoError(t, err)
|
|
|
|
err = service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationWarrantyExpiring, "Warranty", "Expiring", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, notifs)
|
|
}
|
|
|
|
// === CreateAndSendNotification — unknown type defaults to enabled ===
|
|
|
|
func TestNotificationService_CreateAndSend_UnknownTypeDefaultsEnabled(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationType("unknown_type"), "Unknown", "Unknown notification", nil)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
}
|
|
|
|
// === CreateAndSendNotification with string and non-string data values ===
|
|
|
|
func TestNotificationService_CreateAndSend_WithMixedDataTypes(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
data := map[string]interface{}{
|
|
"task_id": uint(42),
|
|
"task_name": "Fix faucet",
|
|
"residence_id": uint(10),
|
|
"extra_data": map[string]string{"foo": "bar"},
|
|
}
|
|
|
|
err := service.CreateAndSendNotification(context.Background(), user.ID, models.NotificationTaskDueSoon, "Due Soon", "Fix faucet", data)
|
|
require.NoError(t, err)
|
|
|
|
notifs, err := service.GetNotifications(user.ID, 10, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, notifs, 1)
|
|
assert.NotNil(t, notifs[0].Data)
|
|
}
|
|
|
|
// === UpdatePreferences — partial update with multiple fields ===
|
|
|
|
func TestNotificationService_UpdatePreferences_MultipleFields(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
falseVal := false
|
|
trueVal := true
|
|
hour8 := 8
|
|
hour14 := 14
|
|
req := &UpdatePreferencesRequest{
|
|
TaskDueSoon: &falseVal,
|
|
TaskOverdue: &falseVal,
|
|
TaskCompleted: &trueVal,
|
|
TaskAssigned: &trueVal,
|
|
ResidenceShared: &falseVal,
|
|
WarrantyExpiring: &trueVal,
|
|
DailyDigest: &falseVal,
|
|
EmailTaskCompleted: &trueVal,
|
|
TaskDueSoonHour: &hour8,
|
|
TaskOverdueHour: &hour14,
|
|
}
|
|
|
|
resp, err := service.UpdatePreferences(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.False(t, resp.TaskDueSoon)
|
|
assert.False(t, resp.TaskOverdue)
|
|
assert.True(t, resp.TaskCompleted)
|
|
assert.True(t, resp.TaskAssigned)
|
|
assert.False(t, resp.ResidenceShared)
|
|
assert.True(t, resp.WarrantyExpiring)
|
|
assert.False(t, resp.DailyDigest)
|
|
assert.True(t, resp.EmailTaskCompleted)
|
|
assert.Equal(t, 8, *resp.TaskDueSoonHour)
|
|
assert.Equal(t, 14, *resp.TaskOverdueHour)
|
|
}
|
|
|
|
// === UpdatePreferences — negative hour ===
|
|
|
|
func TestNotificationService_UpdatePreferences_NegativeHour(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
negHour := -1
|
|
req := &UpdatePreferencesRequest{
|
|
TaskOverdueHour: &negHour,
|
|
}
|
|
|
|
_, err := service.UpdatePreferences(user.ID, req)
|
|
testutil.AssertAppErrorCode(t, err, http.StatusBadRequest)
|
|
}
|
|
|
|
// === RegisterDevice — re-register Android device with same token ===
|
|
|
|
func TestNotificationService_RegisterDevice_UpdateExistingAndroid(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
req := &RegisterDeviceRequest{
|
|
Name: "Pixel 8",
|
|
DeviceID: "device-android",
|
|
RegistrationID: "token-android-1",
|
|
Platform: push.PlatformAndroid,
|
|
}
|
|
_, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
|
|
// Re-register with same token but new name
|
|
req.Name = "Pixel 8 Pro"
|
|
resp, err := service.RegisterDevice(user.ID, req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Pixel 8 Pro", resp.Name)
|
|
assert.Equal(t, push.PlatformAndroid, resp.Platform)
|
|
}
|
|
|
|
// === DeleteDevice — Android not found ===
|
|
|
|
func TestDeleteDevice_AndroidNotFound_Returns404(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.DeleteDevice(99999, push.PlatformAndroid, user.ID)
|
|
testutil.AssertAppErrorCode(t, err, http.StatusNotFound)
|
|
}
|
|
|
|
// === DeleteDevice — Android correct user succeeds ===
|
|
|
|
func TestDeleteDevice_CorrectUser_Android_Succeeds(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
device := &models.GCMDevice{
|
|
UserID: &owner.ID,
|
|
Name: "Pixel",
|
|
DeviceID: "device-android-1",
|
|
RegistrationID: "token-android-1",
|
|
CloudMessageType: "FCM",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.DeleteDevice(device.ID, push.PlatformAndroid, owner.ID)
|
|
require.NoError(t, err)
|
|
|
|
var found models.GCMDevice
|
|
err = db.First(&found, device.ID).Error
|
|
require.NoError(t, err)
|
|
assert.False(t, found.Active)
|
|
}
|
|
|
|
// === UnregisterDevice — Android wrong user ===
|
|
|
|
func TestNotificationService_UnregisterDevice_WrongUser_Android(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
attacker := testutil.CreateTestUser(t, db, "attacker", "attacker@test.com", "Password123")
|
|
|
|
device := &models.GCMDevice{
|
|
UserID: &owner.ID,
|
|
Name: "Pixel",
|
|
DeviceID: "d2",
|
|
RegistrationID: "owner-android-token",
|
|
CloudMessageType: "FCM",
|
|
Active: true,
|
|
}
|
|
err := db.Create(device).Error
|
|
require.NoError(t, err)
|
|
|
|
err = service.UnregisterDevice("owner-android-token", push.PlatformAndroid, attacker.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
|
|
}
|
|
|
|
// === UnregisterDevice — Android not found ===
|
|
|
|
func TestNotificationService_UnregisterDevice_AndroidNotFound(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
service := NewNotificationService(notifRepo, nil)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
err := service.UnregisterDevice("nonexistent-android", push.PlatformAndroid, user.ID)
|
|
testutil.AssertAppError(t, err, http.StatusNotFound, "error.device_not_found")
|
|
}
|
|
|
|
// === NewNotificationResponse with data and dates ===
|
|
|
|
func TestNewNotificationResponse_WithDataAndDates(t *testing.T) {
|
|
now := time.Now()
|
|
readAt := now.Add(-1 * time.Hour)
|
|
sentAt := now.Add(-2 * time.Hour)
|
|
|
|
n := &models.Notification{
|
|
UserID: 1,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test",
|
|
Body: "Body",
|
|
Data: `{"task_id": 42}`,
|
|
Read: true,
|
|
ReadAt: &readAt,
|
|
Sent: true,
|
|
SentAt: &sentAt,
|
|
}
|
|
n.CreatedAt = now
|
|
|
|
resp := NewNotificationResponse(n)
|
|
assert.Equal(t, "Test", resp.Title)
|
|
assert.True(t, resp.Read)
|
|
assert.True(t, resp.Sent)
|
|
assert.NotNil(t, resp.ReadAt)
|
|
assert.NotNil(t, resp.SentAt)
|
|
assert.NotNil(t, resp.Data)
|
|
assert.Equal(t, float64(42), resp.Data["task_id"])
|
|
}
|
|
|
|
func TestNewNotificationResponse_EmptyData(t *testing.T) {
|
|
now := time.Now()
|
|
n := &models.Notification{
|
|
UserID: 1,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test",
|
|
Body: "Body",
|
|
Data: "",
|
|
}
|
|
n.CreatedAt = now
|
|
|
|
resp := NewNotificationResponse(n)
|
|
assert.Nil(t, resp.Data)
|
|
assert.Nil(t, resp.ReadAt)
|
|
assert.Nil(t, resp.SentAt)
|
|
}
|
|
|
|
// === validateHourField ===
|
|
|
|
func TestValidateHourField_BoundaryValues(t *testing.T) {
|
|
zero := 0
|
|
twentyThree := 23
|
|
twentyFour := 24
|
|
|
|
assert.NoError(t, validateHourField(&zero, "test"))
|
|
assert.NoError(t, validateHourField(&twentyThree, "test"))
|
|
assert.Error(t, validateHourField(&twentyFour, "test"))
|
|
assert.NoError(t, validateHourField(nil, "test"))
|
|
}
|