From 60f8a68bf61b6c9415bf27b9cbc40f76d5c7eb0f Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 15:10:23 +0200 Subject: [PATCH] Implement database integration tests --- .gitignore | 1 + api/database/database.go | 74 +++++++++++++++++----------- api/graphql/models/models_test.go | 12 +++++ api/graphql/models/site_info.go | 49 +++++++++--------- api/graphql/models/site_info_test.go | 21 ++++++++ api/graphql/models/user_test.go | 25 ++++++++++ api/scanner/exif/exif_test.go | 10 ++-- api/scanner/scanner_test.go | 12 +++++ api/test_utils/integration_setup.go | 70 ++++++++++++++++++++++++++ api/test_utils/test_db_manager.go | 60 ++++++++++++++++++++++ 10 files changed, 281 insertions(+), 53 deletions(-) create mode 100644 api/graphql/models/models_test.go create mode 100644 api/graphql/models/site_info_test.go create mode 100644 api/graphql/models/user_test.go create mode 100644 api/scanner/scanner_test.go create mode 100644 api/test_utils/integration_setup.go create mode 100644 api/test_utils/test_db_manager.go diff --git a/.gitignore b/.gitignore index 541190a5..e24ed426 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ photoview.db photoview.db-journal .env +testing.env # docker docker-compose.yml diff --git a/api/database/database.go b/api/database/database.go index 82b8b61f..597e3e44 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -20,8 +20,7 @@ import ( "gorm.io/gorm/logger" ) -func getMysqlAddress() (string, error) { - addressString := utils.EnvMysqlURL.GetValue() +func GetMysqlAddress(addressString string) (string, error) { if addressString == "" { return "", errors.New(fmt.Sprintf("Environment variable %s missing, exiting", utils.EnvMysqlURL.GetName())) } @@ -37,8 +36,7 @@ func getMysqlAddress() (string, error) { return config.FormatDSN(), nil } -func getPostgresAddress() (*url.URL, error) { - addressString := utils.EnvPostgresURL.GetValue() +func GetPostgresAddress(addressString string) (*url.URL, error) { if addressString == "" { return nil, errors.New(fmt.Sprintf("Environment variable %s missing, exiting", utils.EnvPostgresURL.GetName())) } @@ -51,8 +49,7 @@ func getPostgresAddress() (*url.URL, error) { return address, nil } -func getSqliteAddress() (*url.URL, error) { - path := utils.EnvSqlitePath.GetValue() +func GetSqliteAddress(path string) (*url.URL, error) { if path == "" { path = "photoview.db" } @@ -73,11 +70,11 @@ func getSqliteAddress() (*url.URL, error) { return address, nil } -func configureDatabase(config *gorm.Config) (*gorm.DB, error) { +func ConfigureDatabase(config *gorm.Config) (*gorm.DB, error) { var databaseDialect gorm.Dialector switch drivers.DatabaseDriver() { case drivers.DatabaseDriverMysql: - mysqlAddress, err := getMysqlAddress() + mysqlAddress, err := GetMysqlAddress(utils.EnvMysqlURL.GetValue()) if err != nil { return nil, err } @@ -85,7 +82,7 @@ func configureDatabase(config *gorm.Config) (*gorm.DB, error) { databaseDialect = gorm_mysql.Open(mysqlAddress) case drivers.DatabaseDriverSqlite: - sqliteAddress, err := getSqliteAddress() + sqliteAddress, err := GetSqliteAddress(utils.EnvSqlitePath.GetValue()) if err != nil { return nil, err } @@ -93,7 +90,7 @@ func configureDatabase(config *gorm.Config) (*gorm.DB, error) { databaseDialect = sqlite.Open(sqliteAddress.String()) case drivers.DatabaseDriverPostgres: - postgresAddress, err := getPostgresAddress() + postgresAddress, err := GetPostgresAddress(utils.EnvPostgresURL.GetValue()) if err != nil { return nil, err } @@ -126,7 +123,7 @@ func SetupDatabase() (*gorm.DB, error) { for retryCount := 1; retryCount <= 5; retryCount++ { var err error - db, err = configureDatabase(&config) + db, err = ConfigureDatabase(&config) if err == nil { sqlDB, dbErr := db.DB() if dbErr != nil { @@ -151,24 +148,26 @@ func SetupDatabase() (*gorm.DB, error) { return db, nil } -func MigrateDatabase(db *gorm.DB) error { - err := db.AutoMigrate( - &models.User{}, - &models.AccessToken{}, - &models.SiteInfo{}, - &models.Media{}, - &models.MediaURL{}, - &models.Album{}, - &models.MediaEXIF{}, - &models.VideoMetadata{}, - &models.ShareToken{}, - &models.UserMediaData{}, - &models.UserPreferences{}, +var database_models []interface{} = []interface{}{ + &models.User{}, + &models.AccessToken{}, + &models.SiteInfo{}, + &models.Media{}, + &models.MediaURL{}, + &models.Album{}, + &models.MediaEXIF{}, + &models.VideoMetadata{}, + &models.ShareToken{}, + &models.UserMediaData{}, + &models.UserPreferences{}, - // Face detection - &models.FaceGroup{}, - &models.ImageFace{}, - ) + // Face detection + &models.FaceGroup{}, + &models.ImageFace{}, +} + +func MigrateDatabase(db *gorm.DB) error { + err := db.AutoMigrate(database_models...) if err != nil { log.Printf("Auto migration failed: %v\n", err) @@ -188,3 +187,22 @@ func MigrateDatabase(db *gorm.DB) error { return nil } + +func ClearDatabase(db *gorm.DB) error { + err := db.Transaction(func(tx *gorm.DB) error { + sess := tx.Session(&gorm.Session{AllowGlobalUpdate: true}) + for _, model := range database_models { + if err := sess.Delete(model).Error; err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} diff --git a/api/graphql/models/models_test.go b/api/graphql/models/models_test.go new file mode 100644 index 00000000..615307e2 --- /dev/null +++ b/api/graphql/models/models_test.go @@ -0,0 +1,12 @@ +package models_test + +import ( + "os" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + os.Exit(test_utils.IntegrationTestRun(m)) +} diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go index 76d2dd7e..593f4fa7 100644 --- a/api/graphql/models/site_info.go +++ b/api/graphql/models/site_info.go @@ -16,32 +16,37 @@ func (SiteInfo) TableName() string { return "site_info" } +func DefaultSiteInfo() SiteInfo { + defaultConcurrentWorkers := 3 + if db_drivers.DatabaseDriver() == db_drivers.DatabaseDriverSqlite { + defaultConcurrentWorkers = 1 + } + + return SiteInfo{ + InitialSetup: true, + PeriodicScanInterval: 0, + ConcurrentWorkers: defaultConcurrentWorkers, + } +} + // GetSiteInfo gets the site info row from the database, and creates it if it does not exist func GetSiteInfo(db *gorm.DB) (*SiteInfo, error) { - var siteInfo SiteInfo + var siteInfo []*SiteInfo - if err := db.First(&siteInfo).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - defaultConcurrentWorkers := 3 - if db_drivers.DatabaseDriver() == db_drivers.DatabaseDriverSqlite { - defaultConcurrentWorkers = 1 - } - - siteInfo = SiteInfo{ - InitialSetup: true, - PeriodicScanInterval: 0, - ConcurrentWorkers: defaultConcurrentWorkers, - } - - if err := db.Create(&siteInfo).Error; err != nil { - return nil, errors.Wrap(err, "initialize site_info") - } - } else { - return nil, errors.Wrap(err, "get site info from database") - } + if err := db.Limit(1).Find(&siteInfo).Error; err != nil { + return nil, errors.Wrap(err, "get site info from database") } - return &siteInfo, nil + if len(siteInfo) == 0 { + newSiteInfo := DefaultSiteInfo() + + if err := db.Create(&newSiteInfo).Error; err != nil { + return nil, errors.Wrap(err, "initialize site_info") + } + + return &newSiteInfo, nil + } else { + return siteInfo[0], nil + } } diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go new file mode 100644 index 00000000..a947dce2 --- /dev/null +++ b/api/graphql/models/site_info_test.go @@ -0,0 +1,21 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestSiteInfo(t *testing.T) { + db := test_utils.DatabaseTest(t) + + site_info, err := models.GetSiteInfo(db) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, models.DefaultSiteInfo(), *site_info) +} diff --git a/api/graphql/models/user_test.go b/api/graphql/models/user_test.go new file mode 100644 index 00000000..a8232ffc --- /dev/null +++ b/api/graphql/models/user_test.go @@ -0,0 +1,25 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestUserRegistrationAuthorization(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "1234" + user, err := models.RegisterUser(db, "admin", &password, true) + + if !assert.NoError(t, err) { + return + } + + assert.EqualValues(t, "admin", user.Username) + assert.NotNil(t, user.Password) + assert.NotEqualValues(t, "1234", user.Password) // should be hashed + assert.True(t, user.Admin) +} diff --git a/api/scanner/exif/exif_test.go b/api/scanner/exif/exif_test.go index f125be95..956b2aa8 100644 --- a/api/scanner/exif/exif_test.go +++ b/api/scanner/exif/exif_test.go @@ -2,19 +2,23 @@ package exif import ( "fmt" + "os" "path" "testing" "time" "github.com/barasher/go-exiftool" "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" "github.com/stretchr/testify/assert" ) +func TestMain(m *testing.M) { + os.Exit(test_utils.IntegrationTestRun(m)) +} + func TestExifParsers(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } + test_utils.FilesystemTest(t) parsers := []struct { name string diff --git a/api/scanner/scanner_test.go b/api/scanner/scanner_test.go new file mode 100644 index 00000000..9f76d568 --- /dev/null +++ b/api/scanner/scanner_test.go @@ -0,0 +1,12 @@ +package scanner_test + +import ( + "os" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + os.Exit(test_utils.UnitTestRun(m)) +} diff --git a/api/test_utils/integration_setup.go b/api/test_utils/integration_setup.go new file mode 100644 index 00000000..edf231ab --- /dev/null +++ b/api/test_utils/integration_setup.go @@ -0,0 +1,70 @@ +package test_utils + +import ( + "flag" + "log" + "path" + "runtime" + "testing" + + "github.com/joho/godotenv" + "gorm.io/gorm" +) + +type integration_options struct { + Database *bool + Filesystem *bool +} + +var integration_flags integration_options = integration_options{ + Database: flag.Bool("database", false, "run database integration tests"), + Filesystem: flag.Bool("filesystem", false, "run filesystem integration tests"), +} + +var test_dbm TestDBManager = TestDBManager{} + +func UnitTestRun(m *testing.M) int { + flag.Parse() + return m.Run() +} + +func IntegrationTestRun(m *testing.M) int { + flag.Parse() + + if *integration_flags.Database { + _, file, _, ok := runtime.Caller(0) + if !ok { + log.Fatal("could not get runtime file path") + } + + envPath := path.Join(path.Dir(file), "..", "testing.env") + + if err := godotenv.Load(envPath); err != nil { + log.Println("No testing.env file found") + } + } + + result := m.Run() + + test_dbm.Close() + + return result +} + +func FilesystemTest(t *testing.T) { + if !*integration_flags.Filesystem { + t.Skip("Filesystem integration tests disabled") + } +} + +func DatabaseTest(t *testing.T) *gorm.DB { + if !*integration_flags.Database { + t.Skip("Database integration tests disabled") + } + + if err := test_dbm.SetupOrReset(); err != nil { + t.Fatalf("failed to setup or reset test database: %v", err) + } + + return test_dbm.DB +} diff --git a/api/test_utils/test_db_manager.go b/api/test_utils/test_db_manager.go new file mode 100644 index 00000000..03028d48 --- /dev/null +++ b/api/test_utils/test_db_manager.go @@ -0,0 +1,60 @@ +package test_utils + +import ( + "github.com/photoview/photoview/api/database" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type TestDBManager struct { + DB *gorm.DB +} + +func (dbm *TestDBManager) SetupOrReset() error { + if dbm.DB == nil { + return dbm.setup() + } else { + return dbm.reset() + } +} + +func (dbm *TestDBManager) Close() error { + if dbm.DB == nil { + return nil + } + + sqlDB, err := dbm.DB.DB() + if err != nil { + return errors.Wrap(err, "get db instance when closing test database") + } + + sqlDB.Close() + dbm.DB = nil + + return nil +} + +func (dbm *TestDBManager) setup() error { + config := gorm.Config{} + db, err := database.ConfigureDatabase(&config) + if err != nil { + return errors.Wrap(err, "configure test database") + } + + if err := database.MigrateDatabase(db); err != nil { + return errors.Wrap(err, "migrate test database") + } + + dbm.DB = db + + return nil +} + +func (dbm *TestDBManager) reset() error { + + if err := database.ClearDatabase(dbm.DB); err != nil { + return errors.Wrap(err, "reset test database") + } + + return nil +}