package db import ( "fmt" "os" "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "gorm.io/gorm" ) // setupTestDB creates an in-memory SQLite database for testing func setupTestDB(t *testing.T) *DB { gormDB, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("Failed to open in-memory database: %v", err) } // Initialize the database schema err = gormDB.AutoMigrate( &User{}, &PasswordHistory{}, &PasswordResetToken{}, &TransferConfig{}, &Job{}, &JobHistory{}, &FileMetadata{}, ) if err != nil { t.Fatalf("Failed to migrate database: %v", err) } return &DB{DB: gormDB} } func TestUserCRUD(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", IsAdmin: true, LastPasswordChange: time.Now(), } // Test Create err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } assert.NotZero(t, testUser.ID, "User ID should be set after creation") // Test Read retrievedUser, err := db.GetUserByEmail(testUser.Email) if err != nil { t.Fatalf("Failed to get user by email: %v", err) } assert.Equal(t, testUser.ID, retrievedUser.ID, "Retrieved user should have the same ID") assert.Equal(t, testUser.Email, retrievedUser.Email, "Retrieved user should have the same email") assert.Equal(t, testUser.PasswordHash, retrievedUser.PasswordHash, "Retrieved user should have the same password hash") assert.Equal(t, testUser.IsAdmin, retrievedUser.IsAdmin, "Retrieved user should have the same admin status") // Test Update retrievedUser.Email = fmt.Sprintf("updated-%d@example.com", time.Now().UnixNano()) err = db.UpdateUser(retrievedUser) if err != nil { t.Fatalf("Failed to update user: %v", err) } // Verify update updatedUser, err := db.GetUserByID(retrievedUser.ID) if err != nil { t.Fatalf("Failed to get user by ID: %v", err) } assert.Equal(t, retrievedUser.Email, updatedUser.Email, "User email should be updated") } func TestPasswordResetToken(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create a password reset token tokenString := fmt.Sprintf("test-token-%d", time.Now().UnixNano()) expiresAt := time.Now().Add(24 * time.Hour) testToken := &PasswordResetToken{ UserID: testUser.ID, Token: tokenString, ExpiresAt: expiresAt, } err = db.CreatePasswordResetToken(testToken) if err != nil { t.Fatalf("Failed to create password reset token: %v", err) } assert.NotZero(t, testToken.ID, "Token ID should be set after creation") // Retrieve the token retrievedToken, err := db.GetPasswordResetToken(tokenString) if err != nil { t.Fatalf("Failed to get password reset token: %v", err) } assert.Equal(t, testToken.ID, retrievedToken.ID, "Retrieved token should have the same ID") assert.Equal(t, testUser.ID, retrievedToken.UserID, "Retrieved token should reference the correct user") assert.False(t, retrievedToken.Used, "Token should not be marked as used initially") // Mark token as used err = db.MarkPasswordResetTokenAsUsed(retrievedToken.ID) if err != nil { t.Fatalf("Failed to mark token as used: %v", err) } // Verify token is marked as used // Note: We need to use GetPasswordResetTokenByID instead of GetPasswordResetToken // because GetPasswordResetToken filters out used tokens var updatedToken PasswordResetToken result := db.DB.First(&updatedToken, retrievedToken.ID) if result.Error != nil { t.Fatalf("Failed to get updated password reset token: %v", result.Error) } assert.True(t, updatedToken.Used, "Token should be marked as used") } func TestTransferConfigCRUD(t *testing.T) { db := setupTestDB(t) // Create a test user first testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create a test transfer config testConfig := &TransferConfig{ Name: fmt.Sprintf("Test Transfer %d", time.Now().UnixNano()), SourceType: "local", SourcePath: "/source/path", DestinationType: "local", DestinationPath: "/destination/path", FilePattern: "*.txt", CreatedBy: testUser.ID, } // Test Create err = db.CreateTransferConfig(testConfig) if err != nil { t.Fatalf("Failed to create transfer config: %v", err) } assert.NotZero(t, testConfig.ID, "Config ID should be set after creation") // Test Read retrievedConfig, err := db.GetTransferConfig(testConfig.ID) if err != nil { t.Fatalf("Failed to get transfer config: %v", err) } assert.Equal(t, testConfig.Name, retrievedConfig.Name, "Retrieved config should have the same name") assert.Equal(t, testConfig.SourcePath, retrievedConfig.SourcePath, "Retrieved config should have the same source path") // Test Update retrievedConfig.Name = fmt.Sprintf("Updated Transfer %d", time.Now().UnixNano()) err = db.UpdateTransferConfig(retrievedConfig) if err != nil { t.Fatalf("Failed to update transfer config: %v", err) } // Verify update updatedConfig, err := db.GetTransferConfig(retrievedConfig.ID) if err != nil { t.Fatalf("Failed to get updated transfer config: %v", err) } assert.Equal(t, retrievedConfig.Name, updatedConfig.Name, "Config name should be updated") // Test listing configs configs, err := db.GetTransferConfigs(testUser.ID) if err != nil { t.Fatalf("Failed to list transfer configs: %v", err) } assert.GreaterOrEqual(t, len(configs), 1, "There should be at least one config in the list") // Test Delete err = db.DeleteTransferConfig(testConfig.ID) if err != nil { t.Fatalf("Failed to delete transfer config: %v", err) } // Verify deletion _, err = db.GetTransferConfig(testConfig.ID) assert.Error(t, err, "Getting deleted config should return an error") } func TestJobCRUD(t *testing.T) { db := setupTestDB(t) // Create a test user first testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create a test transfer config testConfig := &TransferConfig{ Name: fmt.Sprintf("Test Transfer %d", time.Now().UnixNano()), SourceType: "local", SourcePath: "/source/path", DestinationType: "local", DestinationPath: "/destination/path", FilePattern: "*.txt", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(testConfig) if err != nil { t.Fatalf("Failed to create transfer config: %v", err) } // Create a test job now := time.Now() nextRun := now.Add(24 * time.Hour) testJob := &Job{ Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()), ConfigID: testConfig.ID, Schedule: "0 * * * *", // Run every hour Enabled: true, LastRun: &now, NextRun: &nextRun, CreatedBy: testUser.ID, } // Test Create err = db.CreateJob(testJob) if err != nil { t.Fatalf("Failed to create job: %v", err) } assert.NotZero(t, testJob.ID, "Job ID should be set after creation") // Test Read retrievedJob, err := db.GetJob(testJob.ID) if err != nil { t.Fatalf("Failed to get job: %v", err) } assert.Equal(t, testJob.Name, retrievedJob.Name, "Retrieved job should have the same name") assert.Equal(t, testJob.ConfigID, retrievedJob.ConfigID, "Retrieved job should have the same config ID") assert.Equal(t, testJob.Schedule, retrievedJob.Schedule, "Retrieved job should have the same schedule") // Test listing jobs jobs, err := db.GetJobs(testUser.ID) if err != nil { t.Fatalf("Failed to list jobs: %v", err) } assert.GreaterOrEqual(t, len(jobs), 1, "There should be at least one job in the list") // Test Get Active Jobs activeJobs, err := db.GetActiveJobs() if err != nil { t.Fatalf("Failed to get active jobs: %v", err) } assert.GreaterOrEqual(t, len(activeJobs), 1, "There should be at least one active job") // Test Update retrievedJob.Name = fmt.Sprintf("Updated Job %d", time.Now().UnixNano()) retrievedJob.Enabled = false err = db.UpdateJob(retrievedJob) if err != nil { t.Fatalf("Failed to update job: %v", err) } // Verify update updatedJob, err := db.GetJob(retrievedJob.ID) if err != nil { t.Fatalf("Failed to get updated job: %v", err) } assert.Equal(t, retrievedJob.Name, updatedJob.Name, "Job name should be updated") assert.Equal(t, retrievedJob.Enabled, updatedJob.Enabled, "Job enabled status should be updated") // Test Delete err = db.DeleteJob(testJob.ID) if err != nil { t.Fatalf("Failed to delete job: %v", err) } // Verify deletion _, err = db.GetJob(testJob.ID) assert.Error(t, err, "Getting deleted job should return an error") } // Helper function to test if a config ID is selected for a job func configSelected(job *Job, configID uint) bool { // Check if the job has the config ID in its list for _, id := range job.GetConfigIDsList() { if id == configID { return true } } // As a fallback, check the primary ConfigID return job.ConfigID == configID } func TestJobMultipleConfigs(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: fmt.Sprintf("test-multi-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create multiple test configs config1 := &TransferConfig{ Name: "Test Config 1", SourceType: "local", SourcePath: "/source/path1", DestinationType: "local", DestinationPath: "/destination/path1", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(config1) assert.NoError(t, err) config2 := &TransferConfig{ Name: "Test Config 2", SourceType: "local", SourcePath: "/source/path2", DestinationType: "local", DestinationPath: "/destination/path2", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(config2) assert.NoError(t, err) config3 := &TransferConfig{ Name: "Test Config 3", SourceType: "local", SourcePath: "/source/path3", DestinationType: "local", DestinationPath: "/destination/path3", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(config3) assert.NoError(t, err) // Test 1: Create job with multiple configs testJob := &Job{ Name: "Multi Config Job", Schedule: "0 * * * *", Enabled: true, CreatedBy: testUser.ID, } // Set multiple config IDs configIDs := []uint{config1.ID, config2.ID, config3.ID} testJob.SetConfigIDsList(configIDs) // Verify ConfigIDs string format assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config1.ID)) assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config2.ID)) assert.Contains(t, testJob.ConfigIDs, fmt.Sprintf("%d", config3.ID)) // Verify ConfigID is set to the first config assert.Equal(t, config1.ID, testJob.ConfigID) // Save the job err = db.CreateJob(testJob) assert.NoError(t, err) // Test 2: Retrieve job and check config IDs retrievedJob, err := db.GetJob(testJob.ID) assert.NoError(t, err) // Verify retrieved config IDs retrievedIDs := retrievedJob.GetConfigIDsList() assert.Len(t, retrievedIDs, 3) assert.Contains(t, retrievedIDs, config1.ID) assert.Contains(t, retrievedIDs, config2.ID) assert.Contains(t, retrievedIDs, config3.ID) // Test 3: Test configSelected function assert.True(t, configSelected(retrievedJob, config1.ID)) assert.True(t, configSelected(retrievedJob, config2.ID)) assert.True(t, configSelected(retrievedJob, config3.ID)) assert.False(t, configSelected(retrievedJob, uint(999))) // Test 4: Get configs for job configs, err := db.GetConfigsForJob(testJob.ID) assert.NoError(t, err) assert.Len(t, configs, 3) // Verify config names are correct configNames := make([]string, len(configs)) for i, config := range configs { configNames[i] = config.Name } assert.Contains(t, configNames, "Test Config 1") assert.Contains(t, configNames, "Test Config 2") assert.Contains(t, configNames, "Test Config 3") // Test 5: Update config IDs updatedIDs := []uint{config1.ID, config3.ID} // Remove config2 retrievedJob.SetConfigIDsList(updatedIDs) err = db.UpdateJob(retrievedJob) assert.NoError(t, err) // Verify update updatedJob, err := db.GetJob(testJob.ID) assert.NoError(t, err) updatedRetrievedIDs := updatedJob.GetConfigIDsList() assert.Len(t, updatedRetrievedIDs, 2) assert.Contains(t, updatedRetrievedIDs, config1.ID) assert.Contains(t, updatedRetrievedIDs, config3.ID) assert.NotContains(t, updatedRetrievedIDs, config2.ID) } func TestJobHistoryCRUD(t *testing.T) { db := setupTestDB(t) // Create a test user first testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create a test transfer config testConfig := &TransferConfig{ Name: fmt.Sprintf("Test Transfer %d", time.Now().UnixNano()), SourceType: "local", SourcePath: "/source/path", DestinationType: "local", DestinationPath: "/destination/path", FilePattern: "*.txt", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(testConfig) if err != nil { t.Fatalf("Failed to create transfer config: %v", err) } // Create a test job testJob := &Job{ Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()), ConfigID: testConfig.ID, Schedule: "0 * * * *", // Run every hour Enabled: true, CreatedBy: testUser.ID, } err = db.CreateJob(testJob) if err != nil { t.Fatalf("Failed to create job: %v", err) } // Create a test job history record startTime := time.Now().Add(-1 * time.Hour) endTime := time.Now() testHistory := &JobHistory{ JobID: testJob.ID, StartTime: startTime, EndTime: &endTime, Status: "completed", BytesTransferred: 1024, FilesTransferred: 5, ErrorMessage: "", } // Test Create err = db.CreateJobHistory(testHistory) if err != nil { t.Fatalf("Failed to create job history: %v", err) } assert.NotZero(t, testHistory.ID, "Job history ID should be set after creation") // Test Update testHistory.Status = "failed" testHistory.ErrorMessage = "Test error message" err = db.UpdateJobHistory(testHistory) if err != nil { t.Fatalf("Failed to update job history: %v", err) } // Test getting job history histories, err := db.GetJobHistory(testJob.ID) if err != nil { t.Fatalf("Failed to get job history: %v", err) } assert.Equal(t, 1, len(histories), "There should be one job history record") assert.Equal(t, "failed", histories[0].Status, "Job history status should be 'failed'") assert.Equal(t, "Test error message", histories[0].ErrorMessage, "Job history error message should be set") } func TestFileMetadataCRUD(t *testing.T) { db := setupTestDB(t) // Create a test user first testUser := &User{ Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), PasswordHash: "hashed_password", LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) if err != nil { t.Fatalf("Failed to create user: %v", err) } // Create a test transfer config testConfig := &TransferConfig{ Name: fmt.Sprintf("Test Transfer %d", time.Now().UnixNano()), SourceType: "local", SourcePath: "/source/path", DestinationType: "local", DestinationPath: "/destination/path", FilePattern: "*.txt", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(testConfig) if err != nil { t.Fatalf("Failed to create transfer config: %v", err) } // Create a test job testJob := &Job{ Name: fmt.Sprintf("Test Job %d", time.Now().UnixNano()), ConfigID: testConfig.ID, Schedule: "0 * * * *", // Run every hour Enabled: true, CreatedBy: testUser.ID, } err = db.CreateJob(testJob) if err != nil { t.Fatalf("Failed to create job: %v", err) } // Create a test file metadata record fileName := fmt.Sprintf("testfile-%d.txt", time.Now().UnixNano()) fileHash := fmt.Sprintf("md5-%d", time.Now().UnixNano()) testMetadata := &FileMetadata{ JobID: testJob.ID, FileName: fileName, OriginalPath: "/source/path/" + fileName, FileSize: 1024, FileHash: fileHash, CreationTime: time.Now().Add(-2 * time.Hour), ModTime: time.Now().Add(-1 * time.Hour), ProcessedTime: time.Now(), DestinationPath: "/destination/path/" + fileName, Status: "processed", ErrorMessage: "", } // Test Create err = db.CreateFileMetadata(testMetadata) if err != nil { t.Fatalf("Failed to create file metadata: %v", err) } assert.NotZero(t, testMetadata.ID, "File metadata ID should be set after creation") // Test GetFileMetadataByJobAndName retrievedMetadata, err := db.GetFileMetadataByJobAndName(testJob.ID, fileName) if err != nil { t.Fatalf("Failed to get file metadata by job and name: %v", err) } assert.Equal(t, testMetadata.ID, retrievedMetadata.ID, "Retrieved metadata should have the same ID") assert.Equal(t, fileName, retrievedMetadata.FileName, "Retrieved metadata should have the same file name") assert.Equal(t, fileHash, retrievedMetadata.FileHash, "Retrieved metadata should have the same file hash") // Test GetFileMetadataByHash hashMetadata, err := db.GetFileMetadataByHash(fileHash) if err != nil { t.Fatalf("Failed to get file metadata by hash: %v", err) } assert.Equal(t, testMetadata.ID, hashMetadata.ID, "Retrieved metadata should have the same ID") // Test Delete err = db.DeleteFileMetadata(testMetadata.ID) if err != nil { t.Fatalf("Failed to delete file metadata: %v", err) } // Verify deletion _, err = db.GetFileMetadataByJobAndName(testJob.ID, fileName) assert.Error(t, err, "Getting deleted file metadata should return an error") } func TestDBInitialize(t *testing.T) { // Create a temporary file path for testing tempDBPath := "test_init.db" // Initialize the database db, err := Initialize(tempDBPath) assert.NoError(t, err) assert.NotNil(t, db) // Cleanup err = db.Close() assert.NoError(t, err) // Remove test file err = os.Remove(tempDBPath) if err != nil && !os.IsNotExist(err) { t.Logf("Warning: could not remove test database file: %v", err) } } func TestGetConfigRclonePath(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: "rclone-test@example.com", PasswordHash: "hashed_password", IsAdmin: false, LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) assert.NoError(t, err) // Create a test config testConfig := &TransferConfig{ Name: "Test Rclone Config", SourceType: "local", SourcePath: "/source/path", DestinationType: "sftp", DestHost: "example.com", DestPort: 22, DestUser: "testuser", DestinationPath: "/remote/path", DestKeyFile: "private_key_content", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(testConfig) assert.NoError(t, err) // Test GetConfigRclonePath configPath := db.GetConfigRclonePath(testConfig) assert.NotEmpty(t, configPath) assert.Contains(t, configPath, fmt.Sprintf("%d", testConfig.ID)) } func TestGenerateRcloneConfig(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: "rclone-gen-test@example.com", PasswordHash: "hashed_password", IsAdmin: false, LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) assert.NoError(t, err) // SFTP config test sftpConfig := &TransferConfig{ Name: "Test SFTP Config", SourceType: "local", SourcePath: "/local/path", DestinationType: "sftp", DestHost: "sftp.example.com", DestPort: 22, DestUser: "testuser", DestinationPath: "/remote/path", DestKeyFile: "private_key_content", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(sftpConfig) assert.NoError(t, err) // Test generating rclone config err = db.GenerateRcloneConfig(sftpConfig) assert.NoError(t, err) // FTP config test ftpConfig := &TransferConfig{ Name: "Test FTP Config", SourceType: "local", SourcePath: "/local/ftp", DestinationType: "ftp", DestHost: "ftp.example.com", DestPort: 21, DestUser: "ftpuser", DestPassiveMode: true, CreatedBy: testUser.ID, } err = db.CreateTransferConfig(ftpConfig) assert.NoError(t, err) // Test generating rclone config err = db.GenerateRcloneConfig(ftpConfig) assert.NoError(t, err) // S3 config test s3Config := &TransferConfig{ Name: "Test S3 Config", SourceType: "local", SourcePath: "/local/s3", DestinationType: "s3", DestBucket: "mybucket", DestAccessKey: "accessKey", DestRegion: "us-east-1", DestEndpoint: "s3.amazonaws.com", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(s3Config) assert.NoError(t, err) // Test generating rclone config err = db.GenerateRcloneConfig(s3Config) assert.NoError(t, err) // Test generating config for unsupported protocol invalidConfig := &TransferConfig{ Name: "Invalid Protocol Config", SourceType: "local", SourcePath: "/local/path", DestinationType: "unsupported", DestHost: "example.com", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(invalidConfig) assert.NoError(t, err) // This should NOT return an error for unsupported protocol // as it defaults to local type err = db.GenerateRcloneConfig(invalidConfig) assert.NoError(t, err) // Verify the config file exists configPath := db.GetConfigRclonePath(invalidConfig) _, err = os.Stat(configPath) assert.NoError(t, err, "Config file should exist") } func TestUpdateJobStatus(t *testing.T) { db := setupTestDB(t) // Create a test user testUser := &User{ Email: "job-status-test@example.com", PasswordHash: "hashed_password", IsAdmin: false, LastPasswordChange: time.Now(), } err := db.CreateUser(testUser) assert.NoError(t, err) // Create a test transfer config testConfig := &TransferConfig{ Name: "Test Config for Job Status", SourceType: "local", SourcePath: "/source/path", DestinationType: "local", DestinationPath: "/destination/path", FilePattern: "*.txt", CreatedBy: testUser.ID, } err = db.CreateTransferConfig(testConfig) assert.NoError(t, err) // Create a test job now := time.Now() lastRun := now.Add(-time.Hour) nextRun := now.Add(time.Hour) testJob := &Job{ Name: "Test Job Status", ConfigID: testConfig.ID, Schedule: "0 * * * *", // Run hourly Enabled: true, LastRun: &lastRun, NextRun: &nextRun, CreatedBy: testUser.ID, } err = db.CreateJob(testJob) assert.NoError(t, err) // Update job's last run time updatedLastRun := time.Now() testJob.LastRun = &updatedLastRun err = db.UpdateJobStatus(testJob) assert.NoError(t, err) // Verify the job was updated updatedJob, err := db.GetJob(testJob.ID) assert.NoError(t, err) assert.NotEqual(t, lastRun.Unix(), updatedJob.LastRun.Unix()) // Update job's next run time updatedNextRun := time.Now().Add(2 * time.Hour) testJob.NextRun = &updatedNextRun err = db.UpdateJobStatus(testJob) assert.NoError(t, err) // Verify the job was updated again updatedJob, err = db.GetJob(testJob.ID) assert.NoError(t, err) assert.Equal(t, updatedNextRun.Unix(), updatedJob.NextRun.Unix()) }