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>
This commit is contained in:
126
internal/services/notification_service_test.go
Normal file
126
internal/services/notification_service_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user