Files
honeyDueAPI/internal/services/notification_service_test.go
Trey t 7690f07a2b Harden API security: input validation, safe auth extraction, new tests, and deploy config
Comprehensive security hardening from audit findings:
- Add validation tags to all DTO request structs (max lengths, ranges, enums)
- Replace unsafe type assertions with MustGetAuthUser helper across all handlers
- Remove query-param token auth from admin middleware (prevents URL token leakage)
- Add request validation calls in handlers that were missing c.Validate()
- Remove goroutines in handlers (timezone update now synchronous)
- Add sanitize middleware and path traversal protection (path_utils)
- Stop resetting admin passwords on migration restart
- Warn on well-known default SECRET_KEY
- Add ~30 new test files covering security regressions, auth safety, repos, and services
- Add deploy/ config, audit digests, and AUDIT_FINDINGS documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:48:01 -06:00

127 lines
4.3 KiB
Go

package services
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/push"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-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
}
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)
}