e881d37de0
Every public method on these five services now takes ctx context.Context as the first arg and routes its repo calls through .WithContext(ctx). With TaskService and ResidenceService already migrated, this means every in-process service that touches Postgres now produces a flame graph in Jaeger where the SQL spans nest under the parent HTTP request span. Endpoints now fully traced (HTTP → service → SQL): - /api/auth/login, /register, /logout, /me, /verify-email, /resend-verification - /api/auth/forgot-password, /verify-reset, /reset-password, /update-profile - /api/contractors/* (CRUD + favorite + by-residence + tasks) - /api/documents/* (CRUD + activate/deactivate + image upload/delete) - /api/notifications/* (list, count, mark-read, prefs, devices) - /api/subscription/* (status, purchase, cancel, triggers, promotions) - All previously-migrated /api/tasks/* and /api/residences/* paths Internal helpers also threaded: - TaskService.sendTaskCompletedNotification → forwards ctx - TaskService.UpdateUserTimezone → forwards ctx to NotificationService - ResidenceService.CreateResidence → forwards ctx to SubscriptionService.CheckLimit - NotificationService.registerAPNSDevice / registerGCMDevice → both take ctx ~75 method signatures, ~120 handler/test call sites updated. Tests pass green; the only failure is the pre-existing flaky TaskHandler_QuickComplete SQLite race that fails ~60% of runs on master. Step 3 of the observability plan is now genuinely complete: every API endpoint backed by a Go service emits a per-request flame graph with HTTP → service → SQL spans, plus B2/APNs/FCM/asynq spans where applicable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1185 lines
39 KiB
Go
1185 lines
39 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), notif.ID, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify unread count is 0
|
|
count, err := service.GetUnreadCount(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), user.ID)
|
|
require.NoError(t, err)
|
|
|
|
count, err := service.GetUnreadCount(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), "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(context.Background(), 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(context.Background(), 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(context.Background(), user.ID, "America/New_York")
|
|
|
|
// Set same timezone again — should be a no-op
|
|
service.UpdateUserTimezone(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), user.ID, req)
|
|
require.NoError(t, err)
|
|
|
|
// Re-register with same token but new name
|
|
req.Name = "Pixel 8 Pro"
|
|
resp, err := service.RegisterDevice(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), "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(context.Background(), "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"))
|
|
}
|