FEATURE (backups): Add tests for succesful and failed backup notification sending

This commit is contained in:
Rostislav Dugin
2025-06-25 09:52:40 +03:00
parent 3ad4adb355
commit 75acd16c8b
17 changed files with 401 additions and 45 deletions

View File

@@ -102,6 +102,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect

View File

@@ -16,6 +16,7 @@ var backupService = &BackupService{
storages.GetStorageService(),
backupRepository,
notifiers.GetNotifierService(),
notifiers.GetNotifierService(),
usecases.GetCreateBackupUsecase(),
logger.GetLogger(),
}

View File

@@ -0,0 +1,28 @@
package backups
import (
"postgresus-backend/internal/features/databases"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
"github.com/google/uuid"
)
type NotificationSender interface {
SendNotification(
notifier *notifiers.Notifier,
title string,
message string,
)
}
type CreateBackupUsecase interface {
Execute(
backupID uuid.UUID,
database *databases.Database,
storage *storages.Storage,
backupProgressListener func(
completedMBs float64,
),
) error
}

View File

@@ -0,0 +1,19 @@
package backups
import (
"postgresus-backend/internal/features/notifiers"
"github.com/stretchr/testify/mock"
)
type MockNotificationSender struct {
mock.Mock
}
func (m *MockNotificationSender) SendNotification(
notifier *notifiers.Notifier,
title string,
message string,
) {
m.Called(notifier, title, message)
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"log/slog"
"postgresus-backend/internal/features/backups/usecases"
"postgresus-backend/internal/features/databases"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
@@ -16,12 +15,13 @@ import (
)
type BackupService struct {
databaseService *databases.DatabaseService
storageService *storages.StorageService
backupRepository *BackupRepository
notifierService *notifiers.NotifierService
databaseService *databases.DatabaseService
storageService *storages.StorageService
backupRepository *BackupRepository
notifierService *notifiers.NotifierService
notificationSender NotificationSender
createBackupUseCase *usecases.CreateBackupUsecase
createBackupUseCase CreateBackupUsecase
logger *slog.Logger
}
@@ -308,7 +308,7 @@ func (s *BackupService) SendBackupNotification(
)
}
s.notifierService.SendNotification(
s.notificationSender.SendNotification(
&notifier,
title,
message,

View File

@@ -0,0 +1,152 @@
package backups
import (
"errors"
"postgresus-backend/internal/features/databases"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
"postgresus-backend/internal/features/users"
"postgresus-backend/internal/util/logger"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func Test_BackupExecuted_NotificationSent(t *testing.T) {
user := users.GetTestUser()
storage := storages.CreateTestStorage(user.UserID)
notifier := notifiers.CreateTestNotifier(user.UserID)
database := databases.CreateTestDatabase(user.UserID, storage, notifier)
t.Run("BackupFailed_FailNotificationSent", func(t *testing.T) {
mockNotificationSender := &MockNotificationSender{}
backupService := &BackupService{
databases.GetDatabaseService(),
storages.GetStorageService(),
backupRepository,
notifiers.GetNotifierService(),
mockNotificationSender,
&CreateFailedBackupUsecase{},
logger.GetLogger(),
}
// Set up expectations
mockNotificationSender.On("SendNotification",
mock.Anything,
mock.MatchedBy(func(title string) bool {
return strings.Contains(title, "❌ Backup failed")
}),
mock.MatchedBy(func(message string) bool {
return strings.Contains(message, "backup failed")
}),
).Once()
backupService.MakeBackup(database.ID)
// Verify all expectations were met
mockNotificationSender.AssertExpectations(t)
})
t.Run("BackupSuccess_SuccessNotificationSent", func(t *testing.T) {
mockNotificationSender := &MockNotificationSender{}
// Set up expectations
mockNotificationSender.On("SendNotification",
mock.Anything,
mock.MatchedBy(func(title string) bool {
return strings.Contains(title, "✅ Backup completed")
}),
mock.MatchedBy(func(message string) bool {
return strings.Contains(message, "Backup completed successfully")
}),
).Once()
backupService := &BackupService{
databases.GetDatabaseService(),
storages.GetStorageService(),
backupRepository,
notifiers.GetNotifierService(),
mockNotificationSender,
&CreateSuccessBackupUsecase{},
logger.GetLogger(),
}
backupService.MakeBackup(database.ID)
// Verify all expectations were met
mockNotificationSender.AssertExpectations(t)
})
t.Run("BackupSuccess_VerifyNotificationContent", func(t *testing.T) {
mockNotificationSender := &MockNotificationSender{}
backupService := &BackupService{
databases.GetDatabaseService(),
storages.GetStorageService(),
backupRepository,
notifiers.GetNotifierService(),
mockNotificationSender,
&CreateSuccessBackupUsecase{},
logger.GetLogger(),
}
// capture arguments
var capturedNotifier *notifiers.Notifier
var capturedTitle string
var capturedMessage string
mockNotificationSender.On("SendNotification",
mock.Anything,
mock.AnythingOfType("string"),
mock.AnythingOfType("string"),
).Run(func(args mock.Arguments) {
capturedNotifier = args.Get(0).(*notifiers.Notifier)
capturedTitle = args.Get(1).(string)
capturedMessage = args.Get(2).(string)
}).Once()
backupService.MakeBackup(database.ID)
// Verify expectations were met
mockNotificationSender.AssertExpectations(t)
// Additional detailed assertions
assert.Contains(t, capturedTitle, "✅ Backup completed")
assert.Contains(t, capturedTitle, database.Name)
assert.Contains(t, capturedMessage, "Backup completed successfully")
assert.Contains(t, capturedMessage, "10.00 MB")
assert.Equal(t, notifier.ID, capturedNotifier.ID)
})
}
type CreateFailedBackupUsecase struct {
}
func (uc *CreateFailedBackupUsecase) Execute(
backupID uuid.UUID,
database *databases.Database,
storage *storages.Storage,
backupProgressListener func(
completedMBs float64,
),
) error {
backupProgressListener(10) // Assume we completed 10MB
return errors.New("backup failed")
}
type CreateSuccessBackupUsecase struct {
}
func (uc *CreateSuccessBackupUsecase) Execute(
backupID uuid.UUID,
database *databases.Database,
storage *storages.Storage,
backupProgressListener func(
completedMBs float64,
),
) error {
backupProgressListener(10) // Assume we completed 10MB
return nil
}

View File

@@ -10,7 +10,7 @@ import (
type DatabaseRepository struct{}
func (r *DatabaseRepository) Save(database *Database) error {
func (r *DatabaseRepository) Save(database *Database) (*Database, error) {
db := storage.GetDb()
isNew := database.ID == uuid.Nil
@@ -20,7 +20,7 @@ func (r *DatabaseRepository) Save(database *Database) error {
database.StorageID = database.Storage.ID
return db.Transaction(func(tx *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error {
if database.BackupInterval != nil {
if database.BackupInterval.ID == uuid.Nil {
if err := tx.Create(database.BackupInterval).Error; err != nil {
@@ -82,6 +82,12 @@ func (r *DatabaseRepository) Save(database *Database) error {
return nil
})
if err != nil {
return nil, err
}
return database, nil
}
func (r *DatabaseRepository) FindByID(id uuid.UUID) (*Database, error) {

View File

@@ -31,7 +31,12 @@ func (s *DatabaseService) CreateDatabase(
return err
}
return s.dbRepository.Save(database)
_, err := s.dbRepository.Save(database)
if err != nil {
return err
}
return nil
}
func (s *DatabaseService) UpdateDatabase(
@@ -71,7 +76,12 @@ func (s *DatabaseService) UpdateDatabase(
}
}
return s.dbRepository.Save(database)
_, err = s.dbRepository.Save(database)
if err != nil {
return err
}
return nil
}
func (s *DatabaseService) DeleteDatabase(
@@ -142,7 +152,12 @@ func (s *DatabaseService) TestDatabaseConnection(
database.LastBackupErrorMessage = nil
return s.dbRepository.Save(database)
_, err = s.dbRepository.Save(database)
if err != nil {
return err
}
return nil
}
func (s *DatabaseService) TestDatabaseConnectionDirect(
@@ -168,7 +183,12 @@ func (s *DatabaseService) SetBackupError(databaseID uuid.UUID, errorMessage stri
}
database.LastBackupErrorMessage = &errorMessage
return s.dbRepository.Save(database)
_, err = s.dbRepository.Save(database)
if err != nil {
return err
}
return nil
}
func (s *DatabaseService) SetLastBackupTime(databaseID uuid.UUID, backupTime time.Time) error {
@@ -179,5 +199,10 @@ func (s *DatabaseService) SetLastBackupTime(databaseID uuid.UUID, backupTime tim
database.LastBackupTime = &backupTime
database.LastBackupErrorMessage = nil // Clear any previous error
return s.dbRepository.Save(database)
_, err = s.dbRepository.Save(database)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,47 @@
package databases
import (
"postgresus-backend/internal/features/intervals"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
"github.com/google/uuid"
)
func CreateTestDatabase(
userID uuid.UUID,
storage *storages.Storage,
notifier *notifiers.Notifier,
) *Database {
timeOfDay := "16:00"
database := &Database{
UserID: userID,
Name: "test " + uuid.New().String(),
Type: DatabaseTypePostgres,
StorePeriod: PeriodDay,
BackupInterval: &intervals.Interval{
Interval: intervals.IntervalDaily,
TimeOfDay: &timeOfDay,
},
StorageID: storage.ID,
Storage: *storage,
Notifiers: []notifiers.Notifier{
*notifier,
},
SendNotificationsOn: []BackupNotificationType{
NotificationBackupFailed,
NotificationBackupSuccess,
},
}
database, err := databaseRepository.Save(database)
if err != nil {
panic(err)
}
return database
}

View File

@@ -9,10 +9,10 @@ import (
type NotifierRepository struct{}
func (r *NotifierRepository) Save(notifier *Notifier) error {
func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
db := storage.GetDb()
return db.Transaction(func(tx *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error {
switch notifier.NotifierType {
case NotifierTypeTelegram:
if notifier.TelegramNotifier != nil {
@@ -79,6 +79,12 @@ func (r *NotifierRepository) Save(notifier *Notifier) error {
return nil
})
if err != nil {
return nil, err
}
return notifier, nil
}
func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) {

View File

@@ -32,7 +32,12 @@ func (s *NotifierService) SaveNotifier(
notifier.UserID = user.ID
}
return s.notifierRepository.Save(notifier)
_, err := s.notifierRepository.Save(notifier)
if err != nil {
return err
}
return nil
}
func (s *NotifierService) DeleteNotifier(
@@ -91,7 +96,8 @@ func (s *NotifierService) SendTestNotification(
return err
}
if err = s.notifierRepository.Save(notifier); err != nil {
_, err = s.notifierRepository.Save(notifier)
if err != nil {
return err
}
@@ -125,14 +131,14 @@ func (s *NotifierService) SendNotification(
errMsg := err.Error()
notifiedFromDb.LastSendError = &errMsg
err = s.notifierRepository.Save(notifiedFromDb)
_, err = s.notifierRepository.Save(notifiedFromDb)
if err != nil {
s.logger.Error("Failed to save notifier", "error", err)
}
}
notifiedFromDb.LastSendError = nil
err = s.notifierRepository.Save(notifiedFromDb)
_, err = s.notifierRepository.Save(notifiedFromDb)
if err != nil {
s.logger.Error("Failed to save notifier", "error", err)
}

View File

@@ -0,0 +1,26 @@
package notifiers
import (
webhook_notifier "postgresus-backend/internal/features/notifiers/models/webhook"
"github.com/google/uuid"
)
func CreateTestNotifier(userID uuid.UUID) *Notifier {
notifier := &Notifier{
UserID: userID,
Name: "test " + uuid.New().String(),
NotifierType: NotifierTypeWebhook,
WebhookNotifier: &webhook_notifier.WebhookNotifier{
WebhookURL: "https://webhook.site/123e4567-e89b-12d3-a456-426614174000",
WebhookMethod: webhook_notifier.WebhookMethodPOST,
},
}
notifier, err := notifierRepository.Save(notifier)
if err != nil {
panic(err)
}
return notifier
}

View File

@@ -13,7 +13,7 @@ import (
)
func Test_SaveNewStorage_StorageReturnedViaGet(t *testing.T) {
user := users.GetUser()
user := users.GetTestUser()
router := createRouter()
storage := createTestStorage(user.UserID)
@@ -48,7 +48,7 @@ func Test_SaveNewStorage_StorageReturnedViaGet(t *testing.T) {
}
func Test_UpdateExistingStorage_UpdatedStorageReturnedViaGet(t *testing.T) {
user := users.GetUser()
user := users.GetTestUser()
router := createRouter()
storage := createTestStorage(user.UserID)
@@ -95,7 +95,7 @@ func Test_UpdateExistingStorage_UpdatedStorageReturnedViaGet(t *testing.T) {
}
func Test_DeleteStorage_StorageNotReturnedViaGet(t *testing.T) {
user := users.GetUser()
user := users.GetTestUser()
router := createRouter()
storage := createTestStorage(user.UserID)
@@ -127,7 +127,7 @@ func Test_DeleteStorage_StorageNotReturnedViaGet(t *testing.T) {
}
func Test_TestDirectStorageConnection_ConnectionEstablished(t *testing.T) {
user := users.GetUser()
user := users.GetTestUser()
router := createRouter()
storage := createTestStorage(user.UserID)
@@ -139,7 +139,7 @@ func Test_TestDirectStorageConnection_ConnectionEstablished(t *testing.T) {
}
func Test_TestExistingStorageConnection_ConnectionEstablished(t *testing.T) {
user := users.GetUser()
user := users.GetTestUser()
router := createRouter()
storage := createTestStorage(user.UserID)

View File

@@ -9,47 +9,47 @@ import (
type StorageRepository struct{}
func (r *StorageRepository) Save(s *Storage) error {
func (r *StorageRepository) Save(storage *Storage) (*Storage, error) {
database := db.GetDb()
return database.Transaction(func(tx *gorm.DB) error {
switch s.Type {
err := database.Transaction(func(tx *gorm.DB) error {
switch storage.Type {
case StorageTypeLocal:
if s.LocalStorage != nil {
s.LocalStorage.StorageID = s.ID
if storage.LocalStorage != nil {
storage.LocalStorage.StorageID = storage.ID
}
case StorageTypeS3:
if s.S3Storage != nil {
s.S3Storage.StorageID = s.ID
if storage.S3Storage != nil {
storage.S3Storage.StorageID = storage.ID
}
}
if s.ID == uuid.Nil {
if err := tx.Create(s).
if storage.ID == uuid.Nil {
if err := tx.Create(storage).
Omit("LocalStorage", "S3Storage").
Error; err != nil {
return err
}
} else {
if err := tx.Save(s).
if err := tx.Save(storage).
Omit("LocalStorage", "S3Storage").
Error; err != nil {
return err
}
}
switch s.Type {
switch storage.Type {
case StorageTypeLocal:
if s.LocalStorage != nil {
s.LocalStorage.StorageID = s.ID // Ensure ID is set
if err := tx.Save(s.LocalStorage).Error; err != nil {
if storage.LocalStorage != nil {
storage.LocalStorage.StorageID = storage.ID // Ensure ID is set
if err := tx.Save(storage.LocalStorage).Error; err != nil {
return err
}
}
case StorageTypeS3:
if s.S3Storage != nil {
s.S3Storage.StorageID = s.ID // Ensure ID is set
if err := tx.Save(s.S3Storage).Error; err != nil {
if storage.S3Storage != nil {
storage.S3Storage.StorageID = storage.ID // Ensure ID is set
if err := tx.Save(storage.S3Storage).Error; err != nil {
return err
}
}
@@ -57,6 +57,12 @@ func (r *StorageRepository) Save(s *Storage) error {
return nil
})
if err != nil {
return nil, err
}
return storage, nil
}
func (r *StorageRepository) FindByID(id uuid.UUID) (*Storage, error) {

View File

@@ -30,7 +30,12 @@ func (s *StorageService) SaveStorage(
storage.UserID = user.ID
}
return s.storageRepository.Save(storage)
_, err := s.storageRepository.Save(storage)
if err != nil {
return err
}
return nil
}
func (s *StorageService) DeleteStorage(
@@ -92,7 +97,12 @@ func (s *StorageService) TestStorageConnection(
}
storage.LastSaveError = nil
return s.storageRepository.Save(storage)
_, err = s.storageRepository.Save(storage)
if err != nil {
return err
}
return nil
}
func (s *StorageService) TestStorageConnectionDirect(

View File

@@ -0,0 +1,23 @@
package storages
import (
local_storage "postgresus-backend/internal/features/storages/models/local"
"github.com/google/uuid"
)
func CreateTestStorage(userID uuid.UUID) *Storage {
storage := &Storage{
UserID: userID,
Type: StorageTypeLocal,
Name: "Test Storage " + uuid.New().String(),
LocalStorage: &local_storage.LocalStorage{},
}
storage, err := storageRepository.Save(storage)
if err != nil {
panic(err)
}
return storage
}

View File

@@ -1,6 +1,6 @@
package users
func GetUser() *SignInResponse {
func GetTestUser() *SignInResponse {
isAnyUserExists, err := userService.IsAnyUserExist()
if err != nil {
panic(err)