Files
GoMFT/internal/db/db_test.go
T
StarFleetCPTN 3193bf5111 feat: Update dependencies and enhance job run details template
- Add new dependencies: `github.com/joho/godotenv`, `github.com/stretchr/testify`, and `gopkg.in/natefinch/lumberjack.v2`
- Remove indirect dependency on `github.com/joho/godotenv`
- Refactor JobRunDetails template to separate content rendering for improved testing
- Enhance error message display in JobRunDetails template
- Introduce new test files for JWT and password functionalities
- Add comprehensive tests for database operations and error handling
2025-03-13 18:40:40 -07:00

705 lines
20 KiB
Go

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")
}
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())
}