refactor: Remove obsolete test files

- Deleted outdated test files for providers, authentication, password management, configuration, database, email, scheduler, and web handlers.
- Cleaned up the codebase by removing tests that are no longer relevant or have been replaced by new implementations.
- This cleanup helps maintain a more manageable and efficient testing environment.
This commit is contained in:
StarFleetCPTN
2025-03-26 16:51:14 -07:00
parent 8424e97cf8
commit a7cd021ff2
27 changed files with 0 additions and 11348 deletions
-367
View File
@@ -1,367 +0,0 @@
package providers
import (
"context"
"strings"
"testing"
"github.com/starfleetcptn/gomft/components/providers/common"
"github.com/starfleetcptn/gomft/components/providers/destination"
"github.com/starfleetcptn/gomft/components/providers/source"
"github.com/stretchr/testify/assert"
)
// Test that both source and destination providers can be rendered together with common components
func TestProvidersIntegration(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test rendering common components
{
var buf strings.Builder
err := common.NameField().Render(ctx, &buf)
assert.NoError(err, "Failed to render NameField")
html := buf.String()
assert.Contains(html, `<label for="name"`)
}
// Test rendering source components
{
var buf strings.Builder
err := source.LocalSourceForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render LocalSourceForm")
html := buf.String()
assert.Contains(html, `<label for="source_path"`)
}
// Test rendering destination components
{
var buf strings.Builder
err := destination.LocalDestinationForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render LocalDestinationForm")
html := buf.String()
assert.Contains(html, `<label for="destination_path"`)
}
}
// Test that all source providers are available
func TestSourceProviders(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test each source provider can be rendered
providers := []struct {
name string
template func() (string, error)
}{
{"LocalSourceForm", func() (string, error) {
var buf strings.Builder
err := source.LocalSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"SFTPSourceForm", func() (string, error) {
var buf strings.Builder
err := source.SFTPSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"S3SourceForm", func() (string, error) {
var buf strings.Builder
err := source.S3SourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"FTPSourceForm", func() (string, error) {
var buf strings.Builder
err := source.FTPSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"SMBSourceForm", func() (string, error) {
var buf strings.Builder
err := source.SMBSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"WebDAVSourceForm", func() (string, error) {
var buf strings.Builder
err := source.WebDAVSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
{"GoogleDriveSourceForm", func() (string, error) {
var buf strings.Builder
err := source.GoogleDriveSourceForm().Render(ctx, &buf)
return buf.String(), err
}},
}
for _, provider := range providers {
t.Run(provider.name, func(t *testing.T) {
html, err := provider.template()
assert.NoError(err, "Failed to render "+provider.name)
assert.NotEmpty(html, provider.name+" rendered empty HTML")
})
}
}
// Test that all destination providers are available
func TestDestinationProviders(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test each destination provider can be rendered
providers := []struct {
name string
template func() (string, error)
}{
{"LocalDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.LocalDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"SFTPDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.SFTPDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"S3DestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.S3DestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"FTPDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.FTPDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"SMBDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.SMBDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"WebDAVDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.WebDAVDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
{"GoogleDriveDestinationForm", func() (string, error) {
var buf strings.Builder
err := destination.GoogleDriveDestinationForm().Render(ctx, &buf)
return buf.String(), err
}},
}
for _, provider := range providers {
t.Run(provider.name, func(t *testing.T) {
html, err := provider.template()
assert.NoError(err, "Failed to render "+provider.name)
assert.NotEmpty(html, provider.name+" rendered empty HTML")
})
}
}
// Test the complete configuration wizard flow
func TestConfigurationWizard(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// First test common configuration fields
var buf strings.Builder
err := common.NameField().Render(ctx, &buf)
assert.NoError(err, "Failed to render name field")
nameField := buf.String()
assert.Contains(nameField, `<input type="text" name="name" id="name"`)
// Test source selection
buf.Reset()
err = common.SourceSelection().Render(ctx, &buf)
assert.NoError(err, "Failed to render source selection")
sourceSelection := buf.String()
assert.Contains(sourceSelection, `<select id="source_type" name="source_type"`)
// Test specific source form (local example)
buf.Reset()
err = source.LocalSourceForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render local source form")
localSource := buf.String()
assert.Contains(localSource, `<input type="text" name="source_path" id="source_path"`)
// Test destination selection
buf.Reset()
err = common.DestinationSelection().Render(ctx, &buf)
assert.NoError(err, "Failed to render destination selection")
destinationSelection := buf.String()
assert.Contains(destinationSelection, `<select id="destination_type" name="destination_type"`)
// Test specific destination form (S3 example)
buf.Reset()
err = destination.S3DestinationForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render S3 destination form")
s3Destination := buf.String()
assert.Contains(s3Destination, `<input type="text" name="dest_bucket" id="dest_bucket"`)
// Test advanced options
buf.Reset()
err = common.ArchiveOptions().Render(ctx, &buf)
assert.NoError(err, "Failed to render archive options")
archiveOptions := buf.String()
assert.Contains(archiveOptions, `Enable archiving`)
buf.Reset()
err = common.FilePatternFields().Render(ctx, &buf)
assert.NoError(err, "Failed to render file pattern fields")
filePatterns := buf.String()
assert.Contains(filePatterns, `<input type="text" name="file_pattern" id="file_pattern"`)
// All essential components for the configuration wizard are present and renderable
}
// Test that provider forms have proper conditional logic
func TestProviderFormConditionals(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test SFTP Source form conditionals (password vs key file)
{
var buf strings.Builder
err := source.SFTPSourceForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render SFTP source form")
html := buf.String()
// Should have auth type selection
assert.Contains(html, `<select id="source_auth_type" name="source_auth_type"`)
// Should have password field that's conditionally shown
assert.Contains(html, `x-show="sourceAuthType === &#39;password&#39;"`)
assert.Contains(html, `<input type="password" name="source_password"`)
// Should have key file field that's conditionally shown
assert.Contains(html, `x-show="sourceAuthType === &#39;key_file&#39;"`)
assert.Contains(html, `<input type="text" name="source_key_file"`)
}
// Test S3 Source form conditionals
{
var buf strings.Builder
err := source.S3SourceForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render S3 source form")
html := buf.String()
// Should have both required and optional fields
assert.Contains(html, `<input type="text" name="source_bucket" id="source_bucket" x-model="sourceBucket" required`)
assert.Contains(html, `<input type="text" name="source_region" id="source_region"`)
}
// Test advanced options show/hide behavior
{
var buf strings.Builder
err := common.ArchiveOptions().Render(ctx, &buf)
assert.NoError(err, "Failed to render archive options")
html := buf.String()
// Archive path should only show when archive is enabled
assert.Contains(html, `x-show="archiveEnabled"`)
assert.Contains(html, `<input id="archive_path" name="archive_path" type="text"`)
// Toggle behavior
assert.Contains(html, `x-model="archiveEnabled"`)
assert.Contains(html, `<input id="archive_enabled" name="archive_enabled" type="checkbox"`)
}
}
// Test for accessibility attributes in provider forms
func TestProviderFormsAccessibility(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test source form for accessibility
{
var buf strings.Builder
err := source.LocalSourceForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render local source form")
html := buf.String()
// Should have labels with proper for attributes
assert.Contains(html, `<label for="source_path"`)
// Should have input with id matching label's for attribute
assert.Contains(html, `<input type="text" name="source_path" id="source_path"`)
}
// Test destination form for accessibility
{
var buf strings.Builder
err := destination.LocalDestinationForm().Render(ctx, &buf)
assert.NoError(err, "Failed to render local destination form")
html := buf.String()
// Should have labels with proper for attributes
assert.Contains(html, `<label for="destination_path"`)
// Should have input with id matching label's for attribute
assert.Contains(html, `<input type="text" name="destination_path" id="destination_path"`)
}
}
// Test dynamic form rendering based on provider selection
func TestDynamicFormRendering(t *testing.T) {
// Create context for test
ctx := context.Background()
assert := assert.New(t)
// Test source selection dynamic rendering
{
var buf strings.Builder
err := common.SourceSelection().Render(ctx, &buf)
assert.NoError(err, "Failed to render source selection")
html := buf.String()
// Should have x-model for binding selected value
assert.Contains(html, `x-model="sourceType"`)
// The source selection component doesn't contain x-show attributes
// These assertions are removed as they're not part of the actual component
}
// Test destination selection dynamic rendering
{
var buf strings.Builder
err := common.DestinationSelection().Render(ctx, &buf)
assert.NoError(err, "Failed to render destination selection")
html := buf.String()
// Should have x-model for binding selected value
assert.Contains(html, `x-model="destinationType"`)
// The destination selection component doesn't contain x-show attributes
// These assertions are removed as they're not part of the actual component
}
// Test for proper Alpine.js initialization
{
var buf strings.Builder
err := source.LocalSourceForm().Render(ctx, &buf)
assert.NoError(err)
html := buf.String()
// The LocalSourceForm doesn't initialize Alpine.js data
// It's expected to be used within a parent component that does
assert.Contains(html, `x-model="sourcePath"`)
}
// Test that wizard has a submission handler
{
var buf strings.Builder
// The source selection component doesn't contain form tags
// These assertions are checking for elements that should be in a parent component
err := common.SourceSelection().Render(ctx, &buf)
assert.NoError(err)
html := buf.String()
// Check for the select element instead
assert.Contains(html, `<select id="source_type" name="source_type"`)
assert.Contains(html, `x-model="sourceType"`)
}
}
-74
View File
@@ -1,74 +0,0 @@
package auth
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGenerateAndValidateToken(t *testing.T) {
// Setup test data
userID := uint(1)
email := "test@example.com"
secret := "test-jwt-secret"
expirationTime := 1 * time.Hour
// Generate a token
token, err := GenerateToken(userID, email, secret, expirationTime)
assert.NoError(t, err, "Should not return an error when generating a token")
assert.NotEmpty(t, token, "Token should not be empty")
// Validate the token
claims, err := ValidateToken(token, secret)
assert.NoError(t, err, "Should not return an error when validating a valid token")
assert.NotNil(t, claims, "Claims should not be nil")
assert.Equal(t, userID, claims.UserID, "UserID should match")
assert.Equal(t, email, claims.Email, "Email should match")
}
func TestInvalidToken(t *testing.T) {
// Setup
invalidToken := "invalid.token.string"
secret := "test-jwt-secret"
// Validate the invalid token
claims, err := ValidateToken(invalidToken, secret)
assert.Error(t, err, "Should return an error when validating an invalid token")
assert.Nil(t, claims, "Claims should be nil for an invalid token")
}
func TestExpiredToken(t *testing.T) {
// Setup test data
userID := uint(1)
email := "test@example.com"
secret := "test-jwt-secret"
expirationTime := -1 * time.Hour // Negative duration to create an expired token
// Generate an expired token
token, err := GenerateToken(userID, email, secret, expirationTime)
assert.NoError(t, err, "Should not return an error when generating a token")
// Validate the expired token
claims, err := ValidateToken(token, secret)
assert.Error(t, err, "Should return an error when validating an expired token")
assert.Nil(t, claims, "Claims should be nil for an expired token")
}
func TestInvalidSecret(t *testing.T) {
// Setup test data
userID := uint(1)
email := "test@example.com"
secret := "original-secret"
wrongSecret := "wrong-secret"
expirationTime := 1 * time.Hour
// Generate a token with the original secret
token, err := GenerateToken(userID, email, secret, expirationTime)
assert.NoError(t, err, "Should not return an error when generating a token")
// Validate the token with the wrong secret
claims, err := ValidateToken(token, wrongSecret)
assert.Error(t, err, "Should return an error when validating with the wrong secret")
assert.Nil(t, claims, "Claims should be nil when validating with the wrong secret")
}
-174
View File
@@ -1,174 +0,0 @@
package auth
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// MockDB is a mock implementation of *gorm.DB for testing
type MockDB struct {
mock.Mock
}
func (m *MockDB) Where(query interface{}, args ...interface{}) *gorm.DB {
m.Called(query, args)
return &gorm.DB{}
}
func (m *MockDB) Order(value interface{}) *gorm.DB {
m.Called(value)
return &gorm.DB{}
}
func (m *MockDB) Limit(limit int) *gorm.DB {
m.Called(limit)
return &gorm.DB{}
}
func (m *MockDB) Find(dest interface{}, conds ...interface{}) *gorm.DB {
m.Called(dest, conds)
return &gorm.DB{}
}
func (m *MockDB) Create(value interface{}) *gorm.DB {
m.Called(value)
return &gorm.DB{}
}
func (m *MockDB) Delete(value interface{}, conds ...interface{}) *gorm.DB {
m.Called(value, conds)
return &gorm.DB{}
}
func (m *MockDB) Model(value interface{}) *gorm.DB {
m.Called(value)
return &gorm.DB{}
}
func (m *MockDB) Count(count *int64) *gorm.DB {
m.Called(count)
*count = 10 // Mock count for testing
return &gorm.DB{}
}
func TestDefaultPasswordPolicy(t *testing.T) {
policy := DefaultPasswordPolicy()
assert.Equal(t, 8, policy.MinLength, "Default min length should be 8")
assert.True(t, policy.RequireUppercase, "Should require uppercase by default")
assert.True(t, policy.RequireLowercase, "Should require lowercase by default")
assert.True(t, policy.RequireNumbers, "Should require numbers by default")
assert.True(t, policy.RequireSpecial, "Should require special chars by default")
assert.Equal(t, 90, policy.ExpirationDays, "Default expiration should be 90 days")
assert.Equal(t, 5, policy.HistoryCount, "Default history count should be 5")
assert.True(t, policy.DisallowCommon, "Should disallow common passwords by default")
assert.Equal(t, 5, policy.MaxLoginAttempts, "Default max login attempts should be 5")
assert.Equal(t, 15*time.Minute, policy.LockoutDuration, "Default lockout duration should be 15 minutes")
}
func TestValidatePassword(t *testing.T) {
policy := DefaultPasswordPolicy()
// Test valid password
err := ValidatePassword("Test1234!", policy)
assert.NoError(t, err, "Valid password should pass validation")
// Test password too short
err = ValidatePassword("Test1!", policy)
assert.Error(t, err, "Password shorter than minimum length should fail")
assert.Contains(t, err.Error(), "at least 8 characters")
// Test password without uppercase
err = ValidatePassword("test1234!", policy)
assert.Error(t, err, "Password without uppercase should fail")
assert.Contains(t, err.Error(), "uppercase letter")
// Test password without lowercase
err = ValidatePassword("TEST1234!", policy)
assert.Error(t, err, "Password without lowercase should fail")
assert.Contains(t, err.Error(), "lowercase letter")
// Test password without numbers
err = ValidatePassword("TestTest!", policy)
assert.Error(t, err, "Password without numbers should fail")
assert.Contains(t, err.Error(), "number")
// Test password without special characters
err = ValidatePassword("Test1234", policy)
assert.Error(t, err, "Password without special characters should fail")
assert.Contains(t, err.Error(), "special character")
// Test common password - we need to disable other validations to test just the common password check
customPolicy := DefaultPasswordPolicy()
customPolicy.RequireUppercase = false
customPolicy.RequireLowercase = false
customPolicy.RequireNumbers = false
customPolicy.RequireSpecial = false
err = ValidatePassword("password", customPolicy)
assert.Error(t, err, "Common password should fail even with relaxed requirements")
assert.Contains(t, err.Error(), "common or easily guessable")
// Test with custom policy (all validations disabled)
verySimplePolicy := PasswordPolicy{
MinLength: 6,
RequireUppercase: false,
RequireLowercase: false,
RequireNumbers: false,
RequireSpecial: false,
DisallowCommon: false,
}
err = ValidatePassword("simple", verySimplePolicy)
assert.NoError(t, err, "Simple password should pass with all validations disabled")
}
func TestComparePasswords(t *testing.T) {
// Generate a hashed password
plainPassword := "TestPassword123!"
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
assert.NoError(t, err, "Password hashing should not error")
// Test valid password comparison
err = ComparePasswords(string(hashedPassword), plainPassword)
assert.NoError(t, err, "Correct password should match hash")
// Test invalid password comparison
err = ComparePasswords(string(hashedPassword), "WrongPassword123!")
assert.Error(t, err, "Incorrect password should not match hash")
}
func TestIsPasswordExpired(t *testing.T) {
policy := DefaultPasswordPolicy()
// Test password within expiration period
lastChange := time.Now().Add(-80 * 24 * time.Hour) // 80 days ago
assert.False(t, IsPasswordExpired(lastChange, policy), "Password changed 80 days ago should not be expired")
// Test expired password
lastChange = time.Now().Add(-100 * 24 * time.Hour) // 100 days ago
assert.True(t, IsPasswordExpired(lastChange, policy), "Password changed 100 days ago should be expired")
// Test with expiration disabled
customPolicy := PasswordPolicy{
ExpirationDays: 0, // Disabled
}
lastChange = time.Now().Add(-1000 * 24 * time.Hour) // 1000 days ago
assert.False(t, IsPasswordExpired(lastChange, customPolicy), "Password should not expire when expiration is disabled")
}
func TestIsCommonPassword(t *testing.T) {
// Test with common passwords
assert.True(t, isCommonPassword("password"), "Should detect 'password' as common")
assert.True(t, isCommonPassword("admin123"), "Should detect 'admin123' as common")
assert.True(t, isCommonPassword("QWERTY"), "Should detect 'QWERTY' as common (case insensitive)")
// Test with uncommon passwords
assert.False(t, isCommonPassword("G4x8qT2!pL9z"), "Should not detect complex password as common")
assert.False(t, isCommonPassword("UniquePassword123!"), "Should not detect unique password as common")
}
-89
View File
@@ -1,89 +0,0 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoad(t *testing.T) {
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "gomft-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Set up test environment variables
testEnvVars := map[string]string{
"SERVER_ADDRESS": ":9090",
"DATA_DIR": filepath.Join(tempDir, "data"),
"BACKUP_DIR": filepath.Join(tempDir, "backups"),
"JWT_SECRET": "test-jwt-secret",
"BASE_URL": "http://test.example.com",
"EMAIL_ENABLED": "true",
"EMAIL_HOST": "smtp.test.com",
"EMAIL_PORT": "2525",
"EMAIL_USERNAME": "test@example.com",
"EMAIL_PASSWORD": "test-password",
}
// Create a temporary .env file
envContent := ""
for key, value := range testEnvVars {
envContent += key + "=" + value + "\n"
os.Setenv(key, value)
}
// Save temporary .env file
envPath := filepath.Join(tempDir, ".env")
if err := os.WriteFile(envPath, []byte(envContent), 0644); err != nil {
t.Fatalf("Failed to write test .env file: %v", err)
}
// Create a symlink to the temp .env file from the project root
// This is a hack for testing, as the Load() function looks for .env in the root
currentEnv := ".env"
// Backup existing .env if it exists
if _, err := os.Stat(currentEnv); err == nil {
if err := os.Rename(currentEnv, currentEnv+".bak"); err != nil {
t.Fatalf("Failed to backup existing .env file: %v", err)
}
defer os.Rename(currentEnv+".bak", currentEnv)
}
// Create temporary .env for test
if err := os.WriteFile(currentEnv, []byte(envContent), 0644); err != nil {
t.Fatalf("Failed to write test .env file: %v", err)
}
defer os.Remove(currentEnv)
// Load configuration
cfg, err := Load()
if err != nil {
t.Fatalf("Failed to load configuration: %v", err)
}
// Verify loaded configuration matches expected values
if cfg.ServerAddress != testEnvVars["SERVER_ADDRESS"] {
t.Errorf("Expected ServerAddress to be %s, got %s", testEnvVars["SERVER_ADDRESS"], cfg.ServerAddress)
}
if cfg.DataDir != testEnvVars["DATA_DIR"] {
t.Errorf("Expected DataDir to be %s, got %s", testEnvVars["DATA_DIR"], cfg.DataDir)
}
if cfg.BackupDir != testEnvVars["BACKUP_DIR"] {
t.Errorf("Expected BackupDir to be %s, got %s", testEnvVars["BACKUP_DIR"], cfg.BackupDir)
}
if cfg.JWTSecret != testEnvVars["JWT_SECRET"] {
t.Errorf("Expected JWTSecret to be %s, got %s", testEnvVars["JWT_SECRET"], cfg.JWTSecret)
}
if cfg.BaseURL != testEnvVars["BASE_URL"] {
t.Errorf("Expected BaseURL to be %s, got %s", testEnvVars["BASE_URL"], cfg.BaseURL)
}
if !cfg.Email.Enabled {
t.Errorf("Expected Email.Enabled to be true")
}
if cfg.Email.Host != testEnvVars["EMAIL_HOST"] {
t.Errorf("Expected Email.Host to be %s, got %s", testEnvVars["EMAIL_HOST"], cfg.Email.Host)
}
}
File diff suppressed because it is too large Load Diff
-250
View File
@@ -1,250 +0,0 @@
package db
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestDeleteTransferConfigEdgeCases tests edge cases for the DeleteTransferConfig function
func TestDeleteTransferConfigEdgeCases(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "config-edge-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Create multiple configs
configs := make([]*TransferConfig, 5)
for i := 0; i < 5; i++ {
config := &TransferConfig{
Name: fmt.Sprintf("Edge Config %d", i),
SourceType: "local",
SourcePath: fmt.Sprintf("/source/path/%d", i),
DestinationType: "local",
DestinationPath: fmt.Sprintf("/destination/path/%d", i),
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(config)
assert.NoError(t, err)
configs[i] = config
}
// Delete them in reverse order
for i := 4; i >= 0; i-- {
err = db.DeleteTransferConfig(configs[i].ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetTransferConfig(configs[i].ID)
assert.Error(t, err, "Config should be deleted")
}
// Test deleting a config that has a job associated with it
configWithJob := &TransferConfig{
Name: "Config with Job",
SourceType: "local",
SourcePath: "/source/path/job",
DestinationType: "local",
DestinationPath: "/destination/path/job",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(configWithJob)
assert.NoError(t, err)
// Create a job for this config
job := &Job{
Name: "Job for Config",
ConfigID: configWithJob.ID,
Schedule: "0 * * * *",
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = db.CreateJob(job)
assert.NoError(t, err)
// Try to delete the config - this should fail due to foreign key constraint
err = db.DeleteTransferConfig(configWithJob.ID)
assert.Error(t, err, "Should not be able to delete config with associated jobs")
assert.Contains(t, err.Error(), "jobs are using this configuration", "Error should mention jobs")
// Delete the job first
err = db.DeleteJob(job.ID)
assert.NoError(t, err)
// Now delete the config - this should succeed
err = db.DeleteTransferConfig(configWithJob.ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetTransferConfig(configWithJob.ID)
assert.Error(t, err, "Config should be deleted")
}
// TestDeleteJobEdgeCases tests edge cases for the DeleteJob function
func TestDeleteJobEdgeCases(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "job-edge-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Create a test config
config := &TransferConfig{
Name: "Config for Job Edge Cases",
SourceType: "local",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/destination/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(config)
assert.NoError(t, err)
// Create multiple jobs
jobs := make([]*Job, 5)
for i := 0; i < 5; i++ {
job := &Job{
Name: fmt.Sprintf("Edge Job %d", i),
ConfigID: config.ID,
Schedule: "0 * * * *",
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = db.CreateJob(job)
assert.NoError(t, err)
jobs[i] = job
}
// Delete them in reverse order
for i := 4; i >= 0; i-- {
err = db.DeleteJob(jobs[i].ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetJob(jobs[i].ID)
assert.Error(t, err, "Job should be deleted")
}
// Create a job with history records
jobWithHistory := &Job{
Name: "Job with History",
ConfigID: config.ID,
Schedule: "0 * * * *",
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = db.CreateJob(jobWithHistory)
assert.NoError(t, err)
// Create history records
for i := 0; i < 3; i++ {
startTime := time.Now().Add(time.Duration(-i) * time.Hour)
endTime := startTime.Add(30 * time.Minute)
history := &JobHistory{
JobID: jobWithHistory.ID,
StartTime: startTime,
EndTime: &endTime,
Status: "completed",
BytesTransferred: int64(1024 * (i + 1)),
FilesTransferred: i + 1,
}
err = db.CreateJobHistory(history)
assert.NoError(t, err)
}
// Now delete the job - this should succeed even with history records
// (due to foreign key constraints in the database)
err = db.DeleteJob(jobWithHistory.ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetJob(jobWithHistory.ID)
assert.Error(t, err, "Job should be deleted")
}
// TestInitializeEdgeCases tests edge cases for the Initialize function
func TestInitializeEdgeCases(t *testing.T) {
// Test with a read-only directory (if possible)
tempDir, err := os.MkdirTemp("", "gomft_test_readonly")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Try to make the directory read-only
// Note: This may not work on all systems due to permissions
origPerms, err := os.Stat(tempDir)
if err != nil {
t.Fatalf("Failed to stat directory: %v", err)
}
// Try to make it read-only
err = os.Chmod(tempDir, 0400) // read-only
if err != nil {
t.Logf("Warning: Could not set directory to read-only: %v", err)
t.Skip("Could not set directory to read-only, skipping test")
}
defer os.Chmod(tempDir, origPerms.Mode()) // restore original permissions
dbPath := filepath.Join(tempDir, "readonly.db")
// This might fail because the directory is read-only
db, err := Initialize(dbPath)
if err != nil {
// Expected error due to read-only directory
t.Logf("Got expected error for read-only directory: %v", err)
} else {
// If it succeeded, clean up
t.Logf("Warning: DB initialization succeeded even with read-only directory!")
err = db.Close()
assert.NoError(t, err)
}
}
// TestCloseEdgeCases tests edge cases for the Close function
func TestCloseEdgeCases(t *testing.T) {
// Create a temporary database
tempDir, err := os.MkdirTemp("", "gomft_test_close_edge")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
dbPath := filepath.Join(tempDir, "close_edge.db")
db, err := Initialize(dbPath)
assert.NoError(t, err)
// Test calling methods after close
sqlDB, err := db.DB.DB()
assert.NoError(t, err)
// Get initial stats
stats := sqlDB.Stats()
t.Logf("Initial stats: MaxOpenConnections=%d, OpenConnections=%d, InUse=%d",
stats.MaxOpenConnections, stats.OpenConnections, stats.InUse)
// Close the DB
err = db.Close()
assert.NoError(t, err)
// Try to get stats again - this might fail
stats = sqlDB.Stats()
t.Logf("After close stats: MaxOpenConnections=%d, OpenConnections=%d, InUse=%d",
stats.MaxOpenConnections, stats.OpenConnections, stats.InUse)
// Verify that DB operations fail after close
_, err = db.GetUserByEmail("test@example.com")
assert.Error(t, err, "DB operations should fail after close")
}
-163
View File
@@ -1,163 +0,0 @@
package db
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Tests for error handling in GetUserByEmail
func TestGetUserByEmailError(t *testing.T) {
db := setupTestDB(t)
// Test the error case with a non-existent email
user, err := db.GetUserByEmail("nonexistent@example.com")
// Verify expectations
assert.Error(t, err, "Should return an error when user is not found")
assert.Nil(t, user, "User should be nil when an error occurs")
}
// Tests for error handling in GetUserByID
func TestGetUserByIDError(t *testing.T) {
db := setupTestDB(t)
// Test the error case with a non-existent ID
user, err := db.GetUserByID(9999)
// Verify expectations
assert.Error(t, err, "Should return an error when user is not found")
assert.Nil(t, user, "User should be nil when an error occurs")
}
// Tests for error handling in GetPasswordResetToken
func TestGetPasswordResetTokenError(t *testing.T) {
db := setupTestDB(t)
// Test the error case with an invalid token
token, err := db.GetPasswordResetToken("invalid-token")
// Verify expectations
assert.Error(t, err, "Should return an error when token is not found")
assert.Nil(t, token, "Token should be nil when an error occurs")
// Test with an expired token
testUser := &User{
Email: "expired-token@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err = db.CreateUser(testUser)
assert.NoError(t, err)
// Create an expired token (expired 1 hour ago)
expiredToken := &PasswordResetToken{
UserID: testUser.ID,
Token: "expired-token",
ExpiresAt: time.Now().Add(-1 * time.Hour),
}
err = db.CreatePasswordResetToken(expiredToken)
assert.NoError(t, err)
// Try to get the expired token
retrievedToken, err := db.GetPasswordResetToken("expired-token")
assert.Error(t, err, "Should return an error for expired token")
assert.Nil(t, retrievedToken, "Token should be nil for expired token")
// Create a used token
usedToken := &PasswordResetToken{
UserID: testUser.ID,
Token: "used-token",
ExpiresAt: time.Now().Add(1 * time.Hour),
Used: BoolPtr(true),
}
err = db.CreatePasswordResetToken(usedToken)
assert.NoError(t, err)
// Try to get the used token
retrievedToken, err = db.GetPasswordResetToken("used-token")
assert.Error(t, err, "Should return an error for used token")
assert.Nil(t, retrievedToken, "Token should be nil for used token")
}
// Tests for error handling in DeleteTransferConfig
func TestDeleteTransferConfigError(t *testing.T) {
db := setupTestDB(t)
// Test deleting a non-existent config
err := db.DeleteTransferConfig(9999)
// Verify expectations - should not return an error even if the record doesn't exist
assert.NoError(t, err, "Should not return an error when deleting non-existent config")
}
// Tests for error handling in DeleteJob
func TestDeleteJobError(t *testing.T) {
db := setupTestDB(t)
// Test deleting a non-existent job
err := db.DeleteJob(9999)
// Verify expectations - should not return an error even if the record doesn't exist
assert.NoError(t, err, "Should not return an error when deleting non-existent job")
}
// Tests for error handling in GetFileMetadataByHash
func TestGetFileMetadataByHashError(t *testing.T) {
db := setupTestDB(t)
// Test the error case with an invalid hash
metadata, err := db.GetFileMetadataByHash("invalid-hash")
// Verify expectations
assert.Error(t, err, "Should return an error when metadata is not found")
assert.Nil(t, metadata, "Metadata should be nil when an error occurs")
}
// Tests for error handling in Initialize
func TestInitializeErrors(t *testing.T) {
// Test with a path that is a directory, not a file
// This should cause an error when trying to open a SQLite database
_, err := Initialize("/dev/null/cannot_be_a_db")
assert.Error(t, err, "Should return an error with invalid path")
}
// Tests for error handling in GenerateRcloneConfig
func TestGenerateRcloneConfigErrors(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "config-error-test@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Create a config with invalid credentials for an SFTP connection
invalidConfig := &TransferConfig{
Name: "Invalid Config",
SourceType: "sftp", // Using SFTP with invalid host to force error
SourceHost: "nonexistent.host",
SourcePort: 22,
SourceUser: "invaliduser",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/destination/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(invalidConfig)
assert.NoError(t, err)
// Set a non-existent RCLONE_PATH to force error
t.Setenv("RCLONE_PATH", "/nonexistent/rclone")
// This should return an error because the rclone command doesn't exist
err = db.GenerateRcloneConfig(invalidConfig)
assert.Error(t, err, "Should return an error when rclone command fails")
}
-134
View File
@@ -1,134 +0,0 @@
package db
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
// TestInitializeWithNonExistentDirectory tests initialization with a directory that doesn't exist
func TestInitializeWithNonExistentDirectory(t *testing.T) {
// Create a temporary directory path
tempDir := filepath.Join(os.TempDir(), "gomft_test_nonexistent")
// Make sure the directory doesn't exist
_ = os.RemoveAll(tempDir)
// Create a path inside the non-existent directory
dbPath := filepath.Join(tempDir, "test.db")
// Initialize the database - this should create the directory
db, err := Initialize(dbPath)
assert.NoError(t, err)
assert.NotNil(t, db)
// Verify the directory was created
_, err = os.Stat(tempDir)
assert.NoError(t, err, "Directory should be created")
// Close and clean up
err = db.Close()
assert.NoError(t, err)
// Clean up
_ = os.RemoveAll(tempDir)
}
// TestInitializeWithInvalidDBPath tests initialization with an invalid DB path
func TestInitializeWithInvalidDBPath(t *testing.T) {
// Create a file path that can't be a SQLite database
invalidPath := "/dev/null/invalid.db"
// Attempt to initialize with an invalid path
db, err := Initialize(invalidPath)
assert.Error(t, err)
assert.Nil(t, db)
}
// TestInitializeWithExistingDB tests initialization with an existing database
func TestInitializeWithExistingDB(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "gomft_test_existing")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
// Create a database path
dbPath := filepath.Join(tempDir, "existing.db")
// Initialize the database for the first time
db1, err := Initialize(dbPath)
assert.NoError(t, err)
assert.NotNil(t, db1)
// Create a test user to verify the database works
user := &User{
Email: "test@example.com",
PasswordHash: "hash",
IsAdmin: BoolPtr(true),
}
err = db1.CreateUser(user)
assert.NoError(t, err)
assert.NotZero(t, user.ID)
// Close the first database connection
err = db1.Close()
assert.NoError(t, err)
// Initialize the database again with the same path
db2, err := Initialize(dbPath)
assert.NoError(t, err)
assert.NotNil(t, db2)
// Verify we can read the user that was created earlier
retrievedUser, err := db2.GetUserByEmail("test@example.com")
assert.NoError(t, err)
assert.Equal(t, user.ID, retrievedUser.ID)
// Close the second database connection
err = db2.Close()
assert.NoError(t, err)
}
// TestCloseMultipleTimes tests closing the database multiple times
func TestCloseMultipleTimes(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "gomft_test_close")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
// Create a database path
dbPath := filepath.Join(tempDir, "close.db")
// Initialize the database
db, err := Initialize(dbPath)
assert.NoError(t, err)
assert.NotNil(t, db)
// Close the database
err = db.Close()
assert.NoError(t, err)
// Trying to close it again - for some DB drivers this might cause an error
// but SQLite in-memory seems to handle this gracefully
err = db.Close()
// We won't assert error here since it depends on the driver
t.Logf("Second close resulted in: %v", err)
// Instead, let's test that DB operations fail after close
_, err = db.GetUserByEmail("test@example.com")
assert.Error(t, err, "DB operations should fail after close")
}
// TestInitializeWithMigrationFailure tests when AutoMigrate fails
func TestInitializeWithMigrationFailure(t *testing.T) {
// We can't easily cause a migration failure with SQLite
// but we can skip this test and document that it's hard to test
t.Skip("Testing migration failure is difficult with SQLite")
// In a real-world scenario, this might happen if:
// 1. The schema changed significantly between versions
// 2. The database is corrupted
// 3. There are permission issues
}
-251
View File
@@ -1,251 +0,0 @@
package db
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestGetConfigRclonePathWithEnv tests the GetConfigRclonePath function with different environment variables
func TestGetConfigRclonePathWithEnv(t *testing.T) {
// Save original environment variable
originalDataDir := os.Getenv("DATA_DIR")
defer os.Setenv("DATA_DIR", originalDataDir)
// Set a custom data directory
customDir := "/tmp/custom_data_dir"
os.Setenv("DATA_DIR", customDir)
db := setupTestDB(t)
// Create a test config
testUser := &User{
Email: "rclone-env-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
testConfig := &TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/dest/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// Test GetConfigRclonePath with custom DATA_DIR
configPath := db.GetConfigRclonePath(testConfig)
assert.Equal(t,
filepath.Join(customDir, "configs", fmt.Sprintf("config_%d.conf", testConfig.ID)),
configPath,
"Should use DATA_DIR environment variable")
}
// TestGenerateRcloneConfigWithoutRclone tests error handling when rclone executable is not available
func TestGenerateRcloneConfigWithoutRclone(t *testing.T) {
// Save original environment variable
originalRclonePath := os.Getenv("RCLONE_PATH")
defer os.Setenv("RCLONE_PATH", originalRclonePath)
// Set a nonexistent rclone path
os.Setenv("RCLONE_PATH", "/nonexistent/rclone")
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "rclone-missing-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Test configs for different source types
sourceTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
for _, sourceType := range sourceTypes {
testConfig := &TransferConfig{
Name: fmt.Sprintf("Test %s Config", sourceType),
SourceType: sourceType,
SourceHost: "example.com",
SourcePort: 22,
SourceUser: "testuser",
SourcePath: "/source/path",
SourceAccessKey: "access_key",
SourceSecretKey: "secret_key",
SourceRegion: "us-east-1",
SourceEndpoint: "endpoint.example.com",
SourceClientID: "client_id",
SourceClientSecret: "client_secret",
DestinationType: "local",
DestinationPath: "/dest/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// This should return an error because rclone is not available
err = db.GenerateRcloneConfig(testConfig)
assert.Error(t, err, "Should return an error when rclone executable is not found for source type: %s", sourceType)
}
// Test configs for different destination types
destTypes := []string{"sftp", "s3", "minio", "b2", "smb", "ftp", "webdav", "nextcloud", "onedrive", "gdrive"}
for _, destType := range destTypes {
testConfig := &TransferConfig{
Name: fmt.Sprintf("Test Dest %s Config", destType),
SourceType: "local",
SourcePath: "/source/path",
DestinationType: destType,
DestHost: "example.com",
DestPort: 22,
DestUser: "testuser",
DestinationPath: "/dest/path",
DestAccessKey: "access_key",
DestSecretKey: "secret_key",
DestRegion: "us-east-1",
DestEndpoint: "endpoint.example.com",
DestClientID: "client_id",
DestClientSecret: "client_secret",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// This should return an error because rclone is not available
err = db.GenerateRcloneConfig(testConfig)
if destType != "local" {
assert.Error(t, err, "Should return an error when rclone executable is not found for dest type: %s", destType)
} else {
// Local destination type might not error since it doesn't need to call rclone
t.Logf("Local destination type might not error")
}
}
}
func TestGoogleDriveRcloneConfig(t *testing.T) {
// Skip if rclone not available
rclonePath := os.Getenv("RCLONE_PATH")
if rclonePath == "" {
rclonePath = "rclone" // default to PATH lookup
}
_, err := exec.Command(rclonePath, "--version").CombinedOutput()
if err != nil {
t.Skip("Skipping test as rclone is not available")
}
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: fmt.Sprintf("google-rclone-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 Google Drive source config
googleSourceConfig := &TransferConfig{
Name: "Google Drive Source Rclone Test",
SourceType: "gdrive",
SourcePath: "/path/in/google/drive",
SourceClientID: "source_google_client_id",
SourceClientSecret: "source_google_client_secret",
SourceTeamDrive: "source_team_drive_id",
DestinationType: "local",
DestinationPath: "/local/destination/path",
FilePattern: "*.pdf",
CreatedBy: testUser.ID,
}
// Set authenticated status
authenticated := true
googleSourceConfig.GoogleDriveAuthenticated = &authenticated
// Create the config
err = db.CreateTransferConfig(googleSourceConfig)
assert.NoError(t, err)
err = db.GenerateRcloneConfigWithToken(googleSourceConfig, "test_token")
assert.NoError(t, err)
// Generate rclone config for source
configPath := db.GetConfigRclonePath(googleSourceConfig)
// Check that the file exists
_, err = os.Stat(configPath)
assert.NoError(t, err, "Rclone config file should exist")
// Read the config file
configContent, err := os.ReadFile(configPath)
assert.NoError(t, err)
content := string(configContent)
// Verify it contains Google Drive specific content
assert.Contains(t, content, "type = drive")
assert.Contains(t, content, fmt.Sprintf("client_id = %s", googleSourceConfig.SourceClientID))
assert.Contains(t, content, "source")
assert.Contains(t, content, fmt.Sprintf("team_drive = %s", googleSourceConfig.SourceTeamDrive))
// Create Google Drive destination config
googleDestConfig := &TransferConfig{
Name: "Google Drive Dest Rclone Test",
SourceType: "local",
SourcePath: "/local/source/path",
DestinationType: "gdrive",
DestinationPath: "/dest/path/in/google/drive",
DestClientID: "dest_google_client_id",
DestClientSecret: "dest_google_client_secret",
DestTeamDrive: "dest_team_drive_id",
FilePattern: "*.pdf",
CreatedBy: testUser.ID,
}
// Set authenticated status
googleDestConfig.GoogleDriveAuthenticated = &authenticated
// Create the config
err = db.CreateTransferConfig(googleDestConfig)
assert.NoError(t, err)
// Generate rclone config for destination
configPath = db.GetConfigRclonePath(googleDestConfig)
// Check that the file exists
_, err = os.Stat(configPath)
assert.NoError(t, err, "Rclone config file should exist")
// Read the config file
configContent, err = os.ReadFile(configPath)
assert.NoError(t, err)
content = string(configContent)
// Verify it contains Google Drive specific content
assert.Contains(t, content, "type = drive")
assert.Contains(t, content, fmt.Sprintf("client_id = %s", googleDestConfig.DestClientID))
assert.Contains(t, content, "dest")
assert.Contains(t, content, fmt.Sprintf("team_drive = %s", googleDestConfig.DestTeamDrive))
// Clean up
err = db.Delete(&googleSourceConfig).Error
assert.NoError(t, err)
err = db.Delete(&googleDestConfig).Error
assert.NoError(t, err)
}
-199
View File
@@ -1,199 +0,0 @@
package db
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
// TestDeleteTransferConfigWithTransaction tests the DeleteTransferConfig function with transaction scenarios
func TestDeleteTransferConfigWithTransaction(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "delete-config-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Create a test config
testConfig := &TransferConfig{
Name: "Test Delete Config",
SourceType: "local",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/destination/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// Test successful deletion
err = db.DeleteTransferConfig(testConfig.ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetTransferConfig(testConfig.ID)
assert.Error(t, err, "Config should be deleted")
// Test deletion with transaction that's rolled back
// Create another config
testConfig2 := &TransferConfig{
Name: "Test Delete Config 2",
SourceType: "local",
SourcePath: "/source/path2",
DestinationType: "local",
DestinationPath: "/destination/path2",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig2)
assert.NoError(t, err)
// Start a transaction
tx := db.Begin()
assert.NotNil(t, tx)
// Delete the config within the transaction
err = tx.Delete(&TransferConfig{}, testConfig2.ID).Error
assert.NoError(t, err)
// Rollback the transaction
tx.Rollback()
// Verify the config still exists
config, err := db.GetTransferConfig(testConfig2.ID)
assert.NoError(t, err)
assert.NotNil(t, config)
assert.Equal(t, testConfig2.ID, config.ID)
// Test deletion with a committed transaction
tx = db.Begin()
assert.NotNil(t, tx)
// Delete the config within the transaction
err = tx.Delete(&TransferConfig{}, testConfig2.ID).Error
assert.NoError(t, err)
// Commit the transaction
tx.Commit()
// Verify the config is deleted
_, err = db.GetTransferConfig(testConfig2.ID)
assert.Error(t, err, "Config should be deleted after commit")
}
// TestDeleteJobWithTransaction tests the DeleteJob function with transaction scenarios
func TestDeleteJobWithTransaction(t *testing.T) {
db := setupTestDB(t)
// Create a test user
testUser := &User{
Email: "delete-job-test@example.com",
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := db.CreateUser(testUser)
assert.NoError(t, err)
// Create a test transfer config
testConfig := &TransferConfig{
Name: "Test Delete Job Config",
SourceType: "local",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/destination/path",
CreatedBy: testUser.ID,
}
err = db.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// Create a test job
testJob := &Job{
Name: "Test Delete Job",
ConfigID: testConfig.ID,
Schedule: "0 * * * *", // Run hourly
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = db.CreateJob(testJob)
assert.NoError(t, err)
// Test successful deletion
err = db.DeleteJob(testJob.ID)
assert.NoError(t, err)
// Verify deletion
_, err = db.GetJob(testJob.ID)
assert.Error(t, err, "Job should be deleted")
// Test deletion with transaction that's rolled back
// Create another job
testJob2 := &Job{
Name: "Test Delete Job 2",
ConfigID: testConfig.ID,
Schedule: "0 * * * *", // Run hourly
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = db.CreateJob(testJob2)
assert.NoError(t, err)
// Start a transaction
tx := db.Begin()
assert.NotNil(t, tx)
// Delete the job within the transaction
err = tx.Delete(&Job{}, testJob2.ID).Error
assert.NoError(t, err)
// Rollback the transaction
tx.Rollback()
// Verify the job still exists
job, err := db.GetJob(testJob2.ID)
assert.NoError(t, err)
assert.NotNil(t, job)
assert.Equal(t, testJob2.ID, job.ID)
// Test deletion with a committed transaction
tx = db.Begin()
assert.NotNil(t, tx)
// Delete the job within the transaction
err = tx.Delete(&Job{}, testJob2.ID).Error
assert.NoError(t, err)
// Commit the transaction
tx.Commit()
// Verify the job is deleted
_, err = db.GetJob(testJob2.ID)
assert.Error(t, err, "Job should be deleted after commit")
}
// TestTransactionHelpers tests transaction helper methods
func TestTransactionHelpers(t *testing.T) {
db := setupTestDB(t)
// Test Begin and Rollback
tx := db.Begin()
assert.NotNil(t, tx)
assert.IsType(t, &gorm.DB{}, tx)
// Rollback should succeed
err := tx.Rollback().Error
assert.NoError(t, err)
// Test Begin and Commit
tx = db.Begin()
assert.NotNil(t, tx)
// Commit should succeed
err = tx.Commit().Error
assert.NoError(t, err)
}
-129
View File
@@ -1,129 +0,0 @@
package email
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/starfleetcptn/gomft/internal/config"
)
// Setup test configuration without using testutils (to avoid import cycles)
func setupTestConfig(t *testing.T) *config.Config {
tempDir, err := os.MkdirTemp("", "gomft-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
t.Cleanup(func() {
os.RemoveAll(tempDir)
})
return &config.Config{
ServerAddress: ":9090",
DataDir: filepath.Join(tempDir, "data"),
BackupDir: filepath.Join(tempDir, "backups"),
JWTSecret: "test-jwt-secret",
BaseURL: "http://test.example.com",
Email: config.EmailConfig{
Enabled: false,
Host: "smtp.test.com",
Port: 587,
Username: "test@example.com",
Password: "test-password",
FromEmail: "test@example.com",
FromName: "Test",
EnableTLS: true,
RequireAuth: true,
},
}
}
func TestEmailServiceDisabled(t *testing.T) {
// Set up test config with email disabled
cfg := setupTestConfig(t)
cfg.Email.Enabled = false
// Create the email service
service := NewService(cfg)
// Send a password reset email
err := service.SendPasswordResetEmail("test@example.com", "Test User", "token123")
// Expect an error indicating the service is disabled
if err == nil {
t.Error("Expected error when email service is disabled, but got none")
}
// Check that the error message contains the reset link
expectedMsg := cfg.BaseURL + "/reset-password?token=token123"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain the reset link %s, got: %s", expectedMsg, err.Error())
}
}
func TestGeneratePasswordResetEmailHTML(t *testing.T) {
// Set up test config
cfg := setupTestConfig(t)
service := NewService(cfg)
// Test cases
tests := []struct {
name string
data map[string]interface{}
expected []string // Strings that should be included in the HTML
}{
{
name: "Complete user data",
data: map[string]interface{}{
"Username": "John Doe",
"ResetLink": "http://example.com/reset?token=abc123",
"AppName": "GoMFT",
"Year": 2023,
"ExpiresHours": 0.25,
},
expected: []string{
"Hello John Doe",
"http://example.com/reset?token=abc123",
"GoMFT",
"2023",
"15 minutes",
},
},
{
name: "No username",
data: map[string]interface{}{
"ResetLink": "http://example.com/reset?token=abc123",
"AppName": "GoMFT",
"Year": 2023,
"ExpiresHours": 0.25,
},
expected: []string{
"Hello",
"http://example.com/reset?token=abc123",
"GoMFT",
"2023",
"15 minutes",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Generate HTML
html, err := service.generatePasswordResetEmailHTML(tc.data)
// Check for errors
if err != nil {
t.Fatalf("Error generating HTML: %v", err)
}
// Check that all expected strings are included
for _, expected := range tc.expected {
if !strings.Contains(html, expected) {
t.Errorf("Expected HTML to contain %q, but it doesn't", expected)
}
}
})
}
}
-75
View File
@@ -1,75 +0,0 @@
package scheduler
import (
"testing"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
)
func TestMockScheduler_MultiConfig(t *testing.T) {
// Create a new mock scheduler
mockScheduler := NewMockScheduler()
// Create a job with multiple configurations
job := &db.Job{
ID: 1,
Name: "Multi-Config Test Job",
Schedule: "*/5 * * * *",
ConfigID: 1, // Primary config ID
}
job.SetEnabled(true)
// Set multiple config IDs
job.SetConfigIDsList([]uint{1, 2, 3})
// Schedule the job
err := mockScheduler.ScheduleJob(job)
assert.NoError(t, err)
// Check if the job is marked as scheduled
assert.True(t, mockScheduler.ScheduledJobs[job.ID])
// Verify that the job is detected as having multiple configs
assert.True(t, mockScheduler.IsJobWithMultipleConfigs(job.ID))
// Verify the configs associated with the job
configs := mockScheduler.GetConfigsForJob(job.ID)
assert.Len(t, configs, 3)
assert.Contains(t, configs, uint(1))
assert.Contains(t, configs, uint(2))
assert.Contains(t, configs, uint(3))
// Test unscheduling the job
mockScheduler.UnscheduleJob(job.ID)
assert.True(t, mockScheduler.UnscheduledJobs[job.ID])
assert.False(t, mockScheduler.ScheduledJobs[job.ID])
// Verify the job is no longer tracked in multi-config jobs
assert.False(t, mockScheduler.IsJobWithMultipleConfigs(job.ID))
assert.Empty(t, mockScheduler.GetConfigsForJob(job.ID))
// Test a job with a single config
singleConfigJob := &db.Job{
ID: 2,
Name: "Single Config Job",
Schedule: "0 0 * * *",
ConfigID: 4,
}
singleConfigJob.SetEnabled(true)
// Set a single config ID
singleConfigJob.SetConfigIDsList([]uint{4})
// Schedule the job
err = mockScheduler.ScheduleJob(singleConfigJob)
assert.NoError(t, err)
// Not considered a multi-config job if it has only one config
assert.False(t, mockScheduler.IsJobWithMultipleConfigs(singleConfigJob.ID))
// Should still contain the single config
singleConfigs := mockScheduler.GetConfigsForJob(singleConfigJob.ID)
assert.Len(t, singleConfigs, 1)
assert.Contains(t, singleConfigs, uint(4))
}
File diff suppressed because it is too large Load Diff
@@ -1,567 +0,0 @@
package scheduler
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestJobExecutionWebhook tests that webhooks are correctly sent during actual job execution
func TestJobExecutionWebhook(t *testing.T) {
// Skip in short mode
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-integration@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Set up a mock HTTP server to receive webhook notifications
var (
receivedPayload []byte
receivedHeaders http.Header
webhookCalled bool
webhookMutex sync.Mutex
waitCh = make(chan struct{})
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookMutex.Lock()
defer webhookMutex.Unlock()
receivedHeaders = r.Header.Clone()
var err error
receivedPayload, err = io.ReadAll(r.Body)
if err != nil {
t.Logf("Error reading request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
t.Logf("Received webhook payload: %s", string(receivedPayload))
webhookCalled = true
close(waitCh)
w.WriteHeader(http.StatusOK)
}))
defer mockServer.Close()
t.Logf("Mock server URL: %s", mockServer.URL)
// Create local source and destination directories
sourceDir := t.TempDir()
destDir := t.TempDir()
t.Logf("Source directory: %s", sourceDir)
t.Logf("Destination directory: %s", destDir)
// Create a test transfer config with local source and destination
config := &db.TransferConfig{
Name: "Webhook Integration Config",
SourceType: "local",
SourcePath: sourceDir,
DestinationType: "local",
DestinationPath: destDir,
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
t.Logf("Created config with ID: %d", config.ID)
// Create a test job with webhook enabled
job := &db.Job{
Name: "Webhook Integration Job",
ConfigID: config.ID,
Schedule: "*/5 * * * *", // not actually used in this test
Enabled: BoolPtr(true),
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
}
err = database.DB.Create(job).Error
require.NoError(t, err)
t.Logf("Created job with ID %d, NotifyOnSuccess=%v", job.ID, job.NotifyOnSuccess)
// Create and initialize the scheduler
scheduler := New(database)
defer scheduler.Stop()
// Create rclone config directory and file
configDir := filepath.Join(tempDir, "configs")
err = os.MkdirAll(configDir, 0755)
require.NoError(t, err)
// Create a minimal rclone config file
rcloneConfig := `
[source_1]
type = local
[dest_1]
type = local
`
configFile := filepath.Join(configDir, "config_1.conf")
err = os.WriteFile(configFile, []byte(rcloneConfig), 0644)
require.NoError(t, err)
t.Logf("Created rclone config file: %s", configFile)
// Put a test file in the source directory
testFile := filepath.Join(sourceDir, "test.txt")
testFileContent := []byte("This is a test file for webhook integration testing.")
err = os.WriteFile(testFile, testFileContent, 0644)
require.NoError(t, err)
t.Logf("Created test file: %s", testFile)
// Check that the file exists
fileInfo, err := os.Stat(testFile)
require.NoError(t, err, "Test file should exist")
t.Logf("Test file size: %d bytes", fileInfo.Size())
// Manually trigger job execution
t.Logf("Running job now...")
err = scheduler.RunJobNow(job.ID)
require.NoError(t, err)
// Wait for the job to complete and webhook to be called (up to 15 seconds)
t.Logf("Waiting for webhook to be called...")
timeout := time.After(15 * time.Second)
select {
case <-waitCh:
t.Logf("Webhook was called")
case <-timeout:
// Before failing, check job status
var histories []db.JobHistory
err = database.DB.Where("job_id = ?", job.ID).Find(&histories).Error
require.NoError(t, err)
if len(histories) > 0 {
t.Logf("Job history found: status=%s, error=%s",
histories[0].Status, histories[0].ErrorMessage)
} else {
t.Logf("No job history found")
}
// Check if destination file exists
destFile := filepath.Join(destDir, "test.txt")
if _, err := os.Stat(destFile); err == nil {
t.Logf("Destination file exists, but webhook was not called")
} else {
t.Logf("Destination file does not exist: %v", err)
}
webhookMutex.Lock()
called := webhookCalled
webhookMutex.Unlock()
if called {
t.Logf("Webhook was actually called but channel synchronization failed")
} else {
t.Fatal("Timed out waiting for webhook to be called")
}
return
}
// Verify the webhook notification
webhookMutex.Lock()
payload := receivedPayload
headers := receivedHeaders
webhookMutex.Unlock()
assert.NotNil(t, payload, "Webhook notification should have been sent")
// Verify the payload content
var payloadMap map[string]interface{}
err = json.Unmarshal(payload, &payloadMap)
require.NoError(t, err, "Failed to unmarshal webhook payload")
// Check essential fields
assert.Equal(t, "job_execution", payloadMap["event_type"])
assert.Equal(t, float64(job.ID), payloadMap["job_id"])
assert.Equal(t, job.Name, payloadMap["job_name"])
assert.Equal(t, float64(config.ID), payloadMap["config_id"])
assert.Equal(t, config.Name, payloadMap["config_name"])
// Check status (should be "completed" or "completed_with_errors")
status, ok := payloadMap["status"].(string)
require.True(t, ok, "Status should be a string")
assert.Contains(t, []string{"completed", "completed_with_errors"}, status)
// Check that we have bytes transferred
bytesTransferred, ok := payloadMap["bytes_transferred"].(float64)
require.True(t, ok, "bytes_transferred should be a number")
assert.Greater(t, bytesTransferred, float64(0))
// Check that we have files transferred
filesTransferred, ok := payloadMap["files_transferred"].(float64)
require.True(t, ok, "files_transferred should be a number")
assert.Equal(t, float64(1), filesTransferred)
// Check standard headers
assert.Equal(t, "application/json", headers.Get("Content-Type"))
assert.Equal(t, "GoMFT-Webhook/1.0", headers.Get("User-Agent"))
// Check that the file was actually transferred
destFile := filepath.Join(destDir, "test.txt")
_, err = os.Stat(destFile)
assert.NoError(t, err, "The file should have been transferred")
// Clean up
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
err = database.DB.Unscoped().Where("job_id = ?", job.ID).Delete(&db.JobHistory{}).Error
require.NoError(t, err)
}
// TestFailedJobWebhook tests that webhooks are correctly sent for failed jobs
func TestFailedJobWebhook(t *testing.T) {
// Skip in short mode
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-failure@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Set up a mock HTTP server to receive webhook notifications
var (
receivedPayload []byte
webhookCalled bool
webhookMutex sync.Mutex
waitCh = make(chan struct{})
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookMutex.Lock()
defer webhookMutex.Unlock()
var err error
receivedPayload, err = io.ReadAll(r.Body)
if err != nil {
t.Logf("Error reading request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
t.Logf("Received webhook payload: %s", string(receivedPayload))
webhookCalled = true
close(waitCh)
w.WriteHeader(http.StatusOK)
}))
defer mockServer.Close()
// Get a non-existent directory for source
nonexistentDir := filepath.Join(t.TempDir(), "non-existent-subdirectory")
// Create a legitimate destination directory
destDir := t.TempDir()
// Create a test transfer config with invalid source (to trigger failure)
config := &db.TransferConfig{
Name: "Webhook Failure Config",
SourceType: "local",
SourcePath: nonexistentDir,
DestinationType: "local",
DestinationPath: destDir,
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
t.Logf("Created config with invalid source path: %s", nonexistentDir)
// Create a test job with webhook enabled
job := &db.Job{
Name: "Webhook Failure Job",
ConfigID: config.ID,
Schedule: "*/5 * * * *", // not actually used in this test
Enabled: BoolPtr(true),
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
}
err = database.DB.Create(job).Error
require.NoError(t, err)
// Create and initialize the scheduler
scheduler := New(database)
defer scheduler.Stop()
// Create rclone config directory and file
configDir := filepath.Join(tempDir, "configs")
err = os.MkdirAll(configDir, 0755)
require.NoError(t, err)
// Create a minimal rclone config file
rcloneConfig := `
[source_1]
type = local
[dest_1]
type = local
`
configFile := filepath.Join(configDir, "config_1.conf")
err = os.WriteFile(configFile, []byte(rcloneConfig), 0644)
require.NoError(t, err)
t.Logf("Created rclone config file: %s", configFile)
// Manually trigger job execution
t.Logf("Running job now (expecting failure)...")
err = scheduler.RunJobNow(job.ID)
require.NoError(t, err)
// Wait for the job to complete and webhook to be called (up to 15 seconds)
t.Logf("Waiting for webhook to be called with failure notification...")
timeout := time.After(15 * time.Second)
select {
case <-waitCh:
t.Logf("Webhook was called")
case <-timeout:
// Before failing, check job status
var histories []db.JobHistory
err = database.DB.Where("job_id = ?", job.ID).Find(&histories).Error
require.NoError(t, err)
if len(histories) > 0 {
t.Logf("Job history found: status=%s, error=%s",
histories[0].Status, histories[0].ErrorMessage)
} else {
t.Logf("No job history found")
}
webhookMutex.Lock()
called := webhookCalled
webhookMutex.Unlock()
if called {
t.Logf("Webhook was actually called but channel synchronization failed")
} else {
t.Fatal("Timed out waiting for webhook to be called")
}
return
}
// Verify the webhook notification
assert.NotNil(t, receivedPayload, "Webhook notification should have been sent")
// Verify the payload content
var payload map[string]interface{}
err = json.Unmarshal(receivedPayload, &payload)
require.NoError(t, err, "Failed to unmarshal webhook payload")
// Check essential fields
assert.Equal(t, "job_execution", payload["event_type"])
assert.Equal(t, float64(job.ID), payload["job_id"])
assert.Equal(t, "failed", payload["status"])
// Ensure there's an error message
errorMsg, ok := payload["error_message"].(string)
require.True(t, ok, "error_message should be a string")
assert.NotEmpty(t, errorMsg)
t.Logf("Error message from webhook: %s", errorMsg)
// Clean up
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
err = database.DB.Unscoped().Where("job_id = ?", job.ID).Delete(&db.JobHistory{}).Error
require.NoError(t, err)
}
// TestWebhookDisabledForSuccessNotification tests that webhooks are not sent for
// successful jobs when notify_on_success is disabled
func TestWebhookDisabledForSuccessNotification(t *testing.T) {
// Skip in short mode
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-disabled@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Set up a mock HTTP server to receive webhook notifications
var (
webhookCalled bool
webhookMutex sync.Mutex
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookMutex.Lock()
defer webhookMutex.Unlock()
// Log the fact that webhook was called (it shouldn't be)
body, _ := io.ReadAll(r.Body)
t.Logf("Unexpected webhook call received: %s", string(body))
webhookCalled = true
w.WriteHeader(http.StatusOK)
}))
defer mockServer.Close()
// Create local source and destination directories
sourceDir := t.TempDir()
destDir := t.TempDir()
// Create a test transfer config with local source and destination
config := &db.TransferConfig{
Name: "Webhook Disabled Config",
SourceType: "local",
SourcePath: sourceDir,
DestinationType: "local",
DestinationPath: destDir,
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
// Create a test job with webhook enabled but notify_on_success disabled
job := &db.Job{
Name: "Webhook Disabled Job",
ConfigID: config.ID,
Schedule: "*/5 * * * *", // not actually used in this test
Enabled: BoolPtr(true),
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(false), // This is the key setting we're testing
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
}
err = database.DB.Create(job).Error
require.NoError(t, err)
// Update the job to ensure the notification settings are correctly set
// This is necessary because the database has default values for these fields
err = database.DB.Model(job).Updates(map[string]interface{}{
"notify_on_success": false,
}).Error
require.NoError(t, err)
// Reload the job to make sure we have the correct values
var reloadedJob db.Job
err = database.DB.First(&reloadedJob, job.ID).Error
require.NoError(t, err)
job = &reloadedJob
t.Logf("Created job with ID %d, NotifyOnSuccess=%v", job.ID, job.NotifyOnSuccess)
// Create and initialize the scheduler
scheduler := New(database)
defer scheduler.Stop()
// Create rclone config directory and file
configDir := filepath.Join(tempDir, "configs")
err = os.MkdirAll(configDir, 0755)
require.NoError(t, err)
// Create a minimal rclone config file
rcloneConfig := `
[source_1]
type = local
[dest_1]
type = local
`
configFile := filepath.Join(configDir, "config_1.conf")
err = os.WriteFile(configFile, []byte(rcloneConfig), 0644)
require.NoError(t, err)
t.Logf("Created rclone config file: %s", configFile)
// Put a test file in the source directory
testFile := filepath.Join(sourceDir, "test.txt")
testFileContent := []byte("This is a test file for disabled webhook testing.")
err = os.WriteFile(testFile, testFileContent, 0644)
require.NoError(t, err)
// Manually trigger job execution
t.Logf("Running job now...")
err = scheduler.RunJobNow(job.ID)
require.NoError(t, err)
// Wait for a bit to ensure job completes (10 seconds should be plenty)
time.Sleep(10 * time.Second)
// Check if webhook was called (it should not have been)
webhookMutex.Lock()
called := webhookCalled
webhookMutex.Unlock()
assert.False(t, called, "Webhook should not have been called for successful job with NotifyOnSuccess=false")
// Verify the job actually ran successfully by checking for the file
destFile := filepath.Join(destDir, "test.txt")
_, err = os.Stat(destFile)
assert.NoError(t, err, "The job should have completed and transferred the file")
// Verify job history has been created and shows completion
var histories []db.JobHistory
err = database.DB.Where("job_id = ?", job.ID).Find(&histories).Error
require.NoError(t, err)
if len(histories) > 0 {
t.Logf("Job history found: status=%s", histories[0].Status)
assert.Equal(t, "completed", histories[0].Status, "Job should have completed successfully")
}
// Clean up
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
err = database.DB.Unscoped().Where("job_id = ?", job.ID).Delete(&db.JobHistory{}).Error
require.NoError(t, err)
}
-610
View File
@@ -1,610 +0,0 @@
package scheduler
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
"time"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestWebhookNotification tests the webhook notification functionality
func TestWebhookNotification(t *testing.T) {
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-test@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Create a test transfer config
config := &db.TransferConfig{
Name: "Webhook Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
// Create a mock HTTP server to receive webhook notifications
var (
receivedPayload []byte
receivedHeaders http.Header
webhookCalled bool
webhookMutex sync.Mutex
waitCh chan struct{}
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookMutex.Lock()
defer webhookMutex.Unlock()
receivedHeaders = r.Header.Clone()
var err error
receivedPayload, err = io.ReadAll(r.Body)
if err != nil {
t.Logf("Error reading request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Debug output to help understand what's happening
t.Logf("Webhook called with payload: %s", string(receivedPayload))
webhookCalled = true
w.WriteHeader(http.StatusOK)
// Signal that webhook was called
if waitCh != nil {
close(waitCh)
}
}))
defer mockServer.Close()
// Create a test scheduler
scheduler := New(database)
defer scheduler.Stop()
// Test cases
tests := []struct {
name string
job *db.Job
history *db.JobHistory
webhookEnabled bool
webhookURL string
webhookSecret string
webhookHeaders map[string]string
notifyOnSuccess bool
notifyOnFailure bool
status string
expectNotification bool
}{
{
name: "Successful job with notification",
job: &db.Job{
Name: "Success Job",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
},
history: &db.JobHistory{
Status: "completed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
BytesTransferred: 1024,
FilesTransferred: 2,
},
webhookEnabled: true,
webhookURL: mockServer.URL,
notifyOnSuccess: true,
notifyOnFailure: true,
status: "completed",
expectNotification: true,
},
{
name: "Failed job with notification",
job: &db.Job{
Name: "Failed Job",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
},
history: &db.JobHistory{
Status: "failed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
ErrorMessage: "Test error message",
},
webhookEnabled: true,
webhookURL: mockServer.URL,
notifyOnSuccess: true,
notifyOnFailure: true,
status: "failed",
expectNotification: true,
},
{
name: "Successful job with notification disabled for success",
job: &db.Job{
Name: "Success Job No Notify",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(false),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
},
history: &db.JobHistory{
Status: "completed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
},
webhookEnabled: true,
webhookURL: mockServer.URL,
notifyOnSuccess: false,
notifyOnFailure: true,
status: "completed",
expectNotification: false,
},
{
name: "Failed job with notification disabled for failure",
job: &db.Job{
Name: "Failed Job No Notify",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(false),
CreatedBy: user.ID,
},
history: &db.JobHistory{
Status: "failed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
ErrorMessage: "Test error message",
},
webhookEnabled: true,
webhookURL: mockServer.URL,
notifyOnSuccess: true,
notifyOnFailure: false,
status: "failed",
expectNotification: false,
},
{
name: "Webhook disabled",
job: &db.Job{
Name: "Webhook Disabled",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
},
history: &db.JobHistory{
Status: "completed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
},
webhookEnabled: false,
webhookURL: mockServer.URL,
notifyOnSuccess: true,
notifyOnFailure: true,
status: "completed",
expectNotification: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Reset received data
webhookMutex.Lock()
receivedPayload = nil
receivedHeaders = nil
webhookCalled = false
waitCh = make(chan struct{})
webhookMutex.Unlock()
// Debug the test case configuration
t.Logf("Test configuration: name=%s, webhookEnabled=%v, notifyOnSuccess=%v, notifyOnFailure=%v, status=%s, expectNotification=%v",
tc.name, tc.webhookEnabled, tc.notifyOnSuccess, tc.notifyOnFailure, tc.status, tc.expectNotification)
// Create a new job instance for each test case
job := &db.Job{
Name: tc.job.Name,
ConfigID: tc.job.ConfigID,
WebhookEnabled: BoolPtr(tc.webhookEnabled),
WebhookURL: tc.webhookURL,
NotifyOnSuccess: BoolPtr(tc.notifyOnSuccess),
NotifyOnFailure: BoolPtr(tc.notifyOnFailure),
CreatedBy: tc.job.CreatedBy,
}
t.Logf("Job before DB create: WebhookEnabled=%v, NotifyOnSuccess=%v, NotifyOnFailure=%v",
job.WebhookEnabled, job.NotifyOnSuccess, job.NotifyOnFailure)
err := database.DB.Create(job).Error
require.NoError(t, err)
// Update the job to ensure the notification settings are correctly set
// This is necessary because the database has default values for these fields
err = database.DB.Model(job).Updates(map[string]interface{}{
"notify_on_success": tc.notifyOnSuccess,
"notify_on_failure": tc.notifyOnFailure,
}).Error
require.NoError(t, err)
// Reload the job to make sure we have the correct values
var reloadedJob db.Job
err = database.DB.First(&reloadedJob, job.ID).Error
require.NoError(t, err)
job = &reloadedJob
t.Logf("Job after DB create: WebhookEnabled=%v, NotifyOnSuccess=%v, NotifyOnFailure=%v",
job.WebhookEnabled, job.NotifyOnSuccess, job.NotifyOnFailure)
// Create and save job history
history := tc.history
history.JobID = job.ID
err = database.DB.Create(history).Error
require.NoError(t, err)
// Debug info
t.Logf("Test case: %s", tc.name)
t.Logf("Job settings: WebhookEnabled=%v, NotifyOnSuccess=%v, NotifyOnFailure=%v",
job.WebhookEnabled, job.NotifyOnSuccess, job.NotifyOnFailure)
t.Logf("History status: %s", history.Status)
// Send webhook notification
scheduler.sendWebhookNotification(job, history, config)
// Wait for webhook call to complete if expected
if tc.expectNotification {
// Wait with timeout for webhook to be called
select {
case <-waitCh:
// Webhook was called
case <-time.After(2 * time.Second):
t.Fatalf("Timed out waiting for webhook to be called")
}
} else {
// Give it a small window to ensure it doesn't call when not expected
time.Sleep(500 * time.Millisecond)
}
// Check if notification was sent as expected
webhookMutex.Lock()
called := webhookCalled
payload := receivedPayload
headers := receivedHeaders
webhookMutex.Unlock()
if tc.expectNotification {
assert.True(t, called, "Expected webhook notification to be sent")
require.NotNil(t, payload, "Expected webhook payload to be non-nil")
// Verify the payload
var payloadMap map[string]interface{}
err := json.Unmarshal(payload, &payloadMap)
require.NoError(t, err, "Failed to unmarshal webhook payload")
// Check common fields
assert.Equal(t, "job_execution", payloadMap["event_type"])
assert.Equal(t, float64(job.ID), payloadMap["job_id"])
assert.Equal(t, job.Name, payloadMap["job_name"])
assert.Equal(t, float64(config.ID), payloadMap["config_id"])
assert.Equal(t, config.Name, payloadMap["config_name"])
assert.Equal(t, history.Status, payloadMap["status"])
// Check headers
assert.Equal(t, "application/json", headers.Get("Content-Type"))
assert.Equal(t, "GoMFT-Webhook/1.0", headers.Get("User-Agent"))
// Additional checks for specific status
if history.Status == "failed" {
assert.Equal(t, history.ErrorMessage, payloadMap["error_message"])
}
} else {
assert.False(t, called, "Expected no webhook notification to be sent")
}
// Clean up
err = database.DB.Unscoped().Delete(history).Error
require.NoError(t, err)
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
})
}
}
// TestWebhookAuthentication tests the webhook authentication functionality
func TestWebhookAuthentication(t *testing.T) {
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-auth-test@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Create a test transfer config
config := &db.TransferConfig{
Name: "Webhook Auth Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
// Create a mock HTTP server to receive webhook notifications
var (
receivedPayload []byte
receivedHeaders http.Header
waitCh = make(chan struct{})
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeaders = r.Header.Clone()
var err error
receivedPayload, err = io.ReadAll(r.Body)
if err != nil {
t.Logf("Error reading request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
close(waitCh)
}))
defer mockServer.Close()
// Create a test scheduler
scheduler := New(database)
defer scheduler.Stop()
// Set up job with webhook secret
secret := "test-webhook-secret"
job := &db.Job{
Name: "Auth Test Job",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
WebhookSecret: secret,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
}
err = database.DB.Create(job).Error
require.NoError(t, err)
// Create job history
history := &db.JobHistory{
JobID: job.ID,
Status: "completed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
BytesTransferred: 1024,
FilesTransferred: 2,
}
err = database.DB.Create(history).Error
require.NoError(t, err)
// Send webhook notification
scheduler.sendWebhookNotification(job, history, config)
// Wait for webhook to be called
select {
case <-waitCh:
// Webhook was called
case <-time.After(2 * time.Second):
t.Fatalf("Timed out waiting for webhook to be called")
}
// Verify the signature
require.NotNil(t, receivedPayload, "Expected webhook notification to be sent")
// Check that the X-Hub-Signature-256 header exists
signature := receivedHeaders.Get("X-Hub-Signature-256")
require.NotEmpty(t, signature, "Expected X-Hub-Signature-256 header to be set")
// Verify that the signature matches the expected HMAC-SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write(receivedPayload)
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Print both signatures for debugging if they don't match
if expectedSignature != signature {
t.Logf("Expected signature: %s", expectedSignature)
t.Logf("Actual signature: %s", signature)
t.Logf("Secret used: %s", secret)
t.Logf("Payload length: %d", len(receivedPayload))
}
assert.Equal(t, expectedSignature, signature, "Signature does not match expected value")
// Clean up
err = database.DB.Unscoped().Delete(history).Error
require.NoError(t, err)
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
}
// TestWebhookCustomHeaders tests the custom headers functionality for webhooks
func TestWebhookCustomHeaders(t *testing.T) {
// Set up a temporary data directory for logs
tempDir := t.TempDir()
// Set DATA_DIR environment variable for the test
originalDataDir := os.Getenv("DATA_DIR")
t.Setenv("DATA_DIR", tempDir)
defer os.Setenv("DATA_DIR", originalDataDir)
// Create a test database
database := setupTestDB(t)
// Create a test user
user := &db.User{
Email: "webhook-headers-test@example.com",
PasswordHash: "hashed_password",
IsAdmin: BoolPtr(true),
}
err := database.CreateUser(user)
require.NoError(t, err)
// Create a test transfer config
config := &db.TransferConfig{
Name: "Webhook Headers Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
err = database.DB.Create(config).Error
require.NoError(t, err)
// Create a mock HTTP server to receive webhook notifications
var (
receivedPayload []byte
receivedHeaders http.Header
waitCh = make(chan struct{})
)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedHeaders = r.Header.Clone()
var err error
receivedPayload, err = io.ReadAll(r.Body)
if err != nil {
t.Logf("Error reading request body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
close(waitCh)
}))
defer mockServer.Close()
// Create a test scheduler
scheduler := New(database)
defer scheduler.Stop()
// Define custom headers
customHeaders := map[string]string{
"X-API-Key": "test-api-key",
"X-Client-ID": "test-client-id",
"X-Source": "gomft-test",
}
customHeadersJSON, err := json.Marshal(customHeaders)
require.NoError(t, err)
// Set up job with custom headers
job := &db.Job{
Name: "Custom Headers Test Job",
ConfigID: config.ID,
WebhookEnabled: BoolPtr(true),
WebhookURL: mockServer.URL,
WebhookHeaders: string(customHeadersJSON),
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
}
err = database.DB.Create(job).Error
require.NoError(t, err)
// Create job history
history := &db.JobHistory{
JobID: job.ID,
Status: "completed",
StartTime: time.Now().Add(-5 * time.Minute),
EndTime: timePtr(time.Now()),
BytesTransferred: 1024,
FilesTransferred: 2,
}
err = database.DB.Create(history).Error
require.NoError(t, err)
// Send webhook notification
scheduler.sendWebhookNotification(job, history, config)
// Wait for webhook to be called
select {
case <-waitCh:
// Webhook was called
case <-time.After(2 * time.Second):
t.Fatalf("Timed out waiting for webhook to be called")
}
// Verify the headers
require.NotNil(t, receivedPayload, "Expected webhook notification to be sent")
// Check that all custom headers are present
for key, value := range customHeaders {
actualValue := receivedHeaders.Get(key)
if actualValue != value {
t.Logf("Custom header mismatch for %s: expected=%s, got=%s", key, value, actualValue)
}
assert.Equal(t, value, actualValue, "Expected custom header %s to be set", key)
}
// Also check standard headers
assert.Equal(t, "application/json", receivedHeaders.Get("Content-Type"))
assert.Equal(t, "GoMFT-Webhook/1.0", receivedHeaders.Get("User-Agent"))
// Clean up
err = database.DB.Unscoped().Delete(history).Error
require.NoError(t, err)
err = database.DB.Unscoped().Delete(job).Error
require.NoError(t, err)
}
// Helper function to create a pointer to a time.Time value
func timePtr(t time.Time) *time.Time {
return &t
}
-659
View File
@@ -1,659 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/scheduler"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt"
)
func setupAPITest(t *testing.T) (*Handlers, *gin.Engine, *db.DB, *db.User) {
// Set up test database
database := testutils.SetupTestDB(t)
// Create test user
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
user := &db.User{
Email: "test@example.com",
PasswordHash: string(hashedPassword),
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(user)
// Create mock scheduler
mockScheduler := scheduler.NewMockScheduler()
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers
handlers := &Handlers{
DB: database,
JWTSecret: "test-jwt-secret",
Scheduler: mockScheduler,
}
return handlers, router, database, user
}
func setupAuthenticatedAPITest(t *testing.T, isAdmin bool) (*Handlers, *gin.Engine, *db.DB, *db.User) {
handlers, router, database, user := setupAPITest(t)
// Update user admin status if needed
user.SetIsAdmin(isAdmin)
// Set up authentication middleware
router.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("email", user.Email)
c.Set("username", "testuser")
c.Set("isAdmin", user.IsAdmin)
c.Next()
})
return handlers, router, database, user
}
func TestHandleAPILogin(t *testing.T) {
handlers, router, _, user := setupAPITest(t)
// Set up route
router.POST("/api/login", handlers.HandleAPILogin)
// Test case 1: Successful login
loginData := map[string]string{
"email": user.Email,
"password": "password123",
}
jsonData, _ := json.Marshal(loginData)
req, _ := http.NewRequest("POST", "/api/login", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
var response map[string]interface{}
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify token exists
token, exists := response["token"]
assert.True(t, exists)
assert.NotEmpty(t, token)
// Verify user data
userData, exists := response["user"]
assert.True(t, exists)
userMap := userData.(map[string]interface{})
assert.Equal(t, float64(user.ID), userMap["id"])
assert.Equal(t, user.Email, userMap["email"])
assert.Equal(t, user.IsAdmin, userMap["is_admin"])
// Test case 2: Invalid credentials
loginData = map[string]string{
"email": user.Email,
"password": "wrongpassword",
}
jsonData, _ = json.Marshal(loginData)
req, _ = http.NewRequest("POST", "/api/login", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusUnauthorized, resp.Code)
// Test case 3: Invalid request format
invalidJSON := []byte(`{"email": "test@example.com", "password":}`)
req, _ = http.NewRequest("POST", "/api/login", bytes.NewBuffer(invalidJSON))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusBadRequest, resp.Code)
}
func TestHandleAPIConfigs(t *testing.T) {
handlers, router, database, user := setupAuthenticatedAPITest(t, false)
// Create test configs
config1 := &db.TransferConfig{
Name: "Test Config 1",
SourceType: "local",
SourcePath: "/source1",
DestinationType: "local",
DestinationPath: "/dest1",
CreatedBy: user.ID,
}
database.Create(config1)
config2 := &db.TransferConfig{
Name: "Test Config 2",
SourceType: "local",
SourcePath: "/source2",
DestinationType: "local",
DestinationPath: "/dest2",
CreatedBy: user.ID,
}
database.Create(config2)
// Create config for another user
otherUser := &db.User{
Email: "other@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(otherUser)
otherConfig := &db.TransferConfig{
Name: "Other User Config",
SourceType: "local",
SourcePath: "/source3",
DestinationType: "local",
DestinationPath: "/dest3",
CreatedBy: otherUser.ID,
}
database.Create(otherConfig)
// Set up route
router.GET("/api/configs", handlers.HandleAPIConfigs)
// Create request
req, _ := http.NewRequest("GET", "/api/configs", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
var response map[string]interface{}
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify configs
configs, exists := response["configs"]
assert.True(t, exists)
configsArray := configs.([]interface{})
assert.Equal(t, 2, len(configsArray))
// Verify only user's configs are returned
foundConfig1 := false
foundConfig2 := false
foundOtherConfig := false
for _, c := range configsArray {
configMap := c.(map[string]interface{})
if configMap["name"] == config1.Name {
foundConfig1 = true
}
if configMap["name"] == config2.Name {
foundConfig2 = true
}
if configMap["name"] == otherConfig.Name {
foundOtherConfig = true
}
}
assert.True(t, foundConfig1)
assert.True(t, foundConfig2)
assert.False(t, foundOtherConfig)
}
func TestHandleAPIConfig(t *testing.T) {
handlers, router, database, user := setupAuthenticatedAPITest(t, false)
// Create test config
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
database.Create(config)
// Create config for another user
otherUser := &db.User{
Email: "other@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(otherUser)
otherConfig := &db.TransferConfig{
Name: "Other User Config",
SourceType: "local",
SourcePath: "/source2",
DestinationType: "local",
DestinationPath: "/dest2",
CreatedBy: otherUser.ID,
}
database.Create(otherConfig)
// Set up route
router.GET("/api/configs/:id", handlers.HandleAPIConfig)
// Test case 1: Get own config
req, _ := http.NewRequest("GET", "/api/configs/"+strconv.Itoa(int(config.ID)), nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
var response map[string]interface{}
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify config
configData, exists := response["config"]
assert.True(t, exists)
configMap := configData.(map[string]interface{})
assert.Equal(t, config.Name, configMap["name"])
// Test case 2: Try to get another user's config
req, _ = http.NewRequest("GET", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be forbidden
assert.Equal(t, http.StatusForbidden, resp.Code)
// Test case 3: Admin can access any config
// Create admin router
adminHandlers, adminRouter, _, _ := setupAuthenticatedAPITest(t, true)
adminRouter.GET("/api/configs/:id", adminHandlers.HandleAPIConfig)
req, _ = http.NewRequest("GET", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), nil)
resp = httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
// Check response - admin should be able to access
assert.Equal(t, http.StatusOK, resp.Code)
// Test case 4: Non-existent config
req, _ = http.NewRequest("GET", "/api/configs/9999", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code)
}
func TestHandleAPICreateConfig(t *testing.T) {
handlers, router, _, user := setupAuthenticatedAPITest(t, false)
// Set up route
router.POST("/api/configs", handlers.HandleAPICreateConfig)
// Create config data
configData := map[string]interface{}{
"name": "New API Config",
"source_type": "local",
"source_path": "/api/source",
"destination_type": "local",
"destination_path": "/api/dest",
}
jsonData, _ := json.Marshal(configData)
// Create request
req, _ := http.NewRequest("POST", "/api/configs", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusCreated, resp.Code)
var response map[string]interface{}
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify config was created
configResponse, exists := response["config"]
assert.True(t, exists)
configMap, ok := configResponse.(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, "New API Config", configMap["name"])
assert.Equal(t, float64(user.ID), configMap["created_by"])
// Test case 2: Invalid request data
invalidJSON := []byte(`{"name": "Invalid Config", "source_type":}`)
req, _ = http.NewRequest("POST", "/api/configs", bytes.NewBuffer(invalidJSON))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusBadRequest, resp.Code)
}
func TestHandleAPIUpdateConfig(t *testing.T) {
handlers, router, database, user := setupAuthenticatedAPITest(t, false)
// Create test config
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
database.Create(config)
// Create config for another user
otherUser := &db.User{
Email: "other@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(otherUser)
otherConfig := &db.TransferConfig{
Name: "Other User Config",
SourceType: "local",
SourcePath: "/source2",
DestinationType: "local",
DestinationPath: "/dest2",
CreatedBy: otherUser.ID,
}
database.Create(otherConfig)
// Set up route
router.PUT("/api/configs/:id", handlers.HandleAPIUpdateConfig)
// Test case 1: Update own config
updateData := map[string]interface{}{
"name": "Updated Config",
"source_type": "local",
"source_path": "/updated/source",
"destination_type": "local",
"destination_path": "/updated/dest",
}
jsonData, _ := json.Marshal(updateData)
req, _ := http.NewRequest("PUT", "/api/configs/"+strconv.Itoa(int(config.ID)), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
var response map[string]interface{}
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify config was updated
configData, exists := response["config"]
assert.True(t, exists)
configMap := configData.(map[string]interface{})
assert.Equal(t, "Updated Config", configMap["name"])
assert.Equal(t, "/updated/source", configMap["source_path"])
// Test case 2: Try to update another user's config
updateData = map[string]interface{}{
"name": "Trying to update other's config",
}
jsonData, _ = json.Marshal(updateData)
req, _ = http.NewRequest("PUT", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be forbidden
assert.Equal(t, http.StatusForbidden, resp.Code)
// Test case 3: Admin can update any config
// Create admin router
adminHandlers, adminRouter, _, _ := setupAuthenticatedAPITest(t, true)
adminRouter.PUT("/api/configs/:id", adminHandlers.HandleAPIUpdateConfig)
updateData = map[string]interface{}{
"name": "Admin Updated Config",
}
jsonData, _ = json.Marshal(updateData)
req, _ = http.NewRequest("PUT", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
// Check response - admin should be able to update
assert.Equal(t, http.StatusOK, resp.Code)
// Test case 4: Non-existent config
req, _ = http.NewRequest("PUT", "/api/configs/9999", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code)
}
func TestHandleAPIDeleteConfig(t *testing.T) {
handlers, router, database, user := setupAuthenticatedAPITest(t, false)
// Create test config
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
database.Create(config)
// Create config for another user
otherUser := &db.User{
Email: "other@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(otherUser)
otherConfig := &db.TransferConfig{
Name: "Other User Config",
SourceType: "local",
SourcePath: "/source2",
DestinationType: "local",
DestinationPath: "/dest2",
CreatedBy: otherUser.ID,
}
database.Create(otherConfig)
// Create config with associated job
configWithJob := &db.TransferConfig{
Name: "Config With Job",
SourceType: "local",
SourcePath: "/source3",
DestinationType: "local",
DestinationPath: "/dest3",
CreatedBy: user.ID,
}
database.Create(configWithJob)
job := &db.Job{
Name: "Test Job",
Schedule: "* * * * *",
ConfigID: configWithJob.ID,
Enabled: BoolPtr(true),
CreatedBy: user.ID,
}
database.Create(job)
// Set up route
router.DELETE("/api/configs/:id", handlers.HandleAPIDeleteConfig)
// Test case 1: Delete own config
req, _ := http.NewRequest("DELETE", "/api/configs/"+strconv.Itoa(int(config.ID)), nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Verify config was deleted
var deletedConfig db.TransferConfig
err := database.First(&deletedConfig, config.ID).Error
assert.Error(t, err) // Should not find the config
// Test case 2: Try to delete another user's config
req, _ = http.NewRequest("DELETE", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be forbidden
assert.Equal(t, http.StatusForbidden, resp.Code)
// Test case 3: Try to delete config with associated job
req, _ = http.NewRequest("DELETE", "/api/configs/"+strconv.Itoa(int(configWithJob.ID)), nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be bad request
assert.Equal(t, http.StatusBadRequest, resp.Code)
// Test case 4: Admin can delete any config
// Create admin router
adminHandlers, adminRouter, _, _ := setupAuthenticatedAPITest(t, true)
adminRouter.DELETE("/api/configs/:id", adminHandlers.HandleAPIDeleteConfig)
req, _ = http.NewRequest("DELETE", "/api/configs/"+strconv.Itoa(int(otherConfig.ID)), nil)
resp = httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
// Check response - admin should be able to delete
assert.Equal(t, http.StatusOK, resp.Code)
// Test case 5: Non-existent config
req, _ = http.NewRequest("DELETE", "/api/configs/9999", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code)
}
func TestHandleAPIRunJob(t *testing.T) {
// Setup test environment
handlers, router, database, user := setupAuthenticatedAPITest(t, false)
// Create test config
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
database.Create(config)
// Create test job
job := &db.Job{
Name: "Test Job",
Schedule: "* * * * *",
ConfigID: config.ID,
Enabled: BoolPtr(true),
CreatedBy: user.ID,
}
database.Create(job)
// Create job for another user
otherUser := &db.User{
Email: "other@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(otherUser)
otherJob := &db.Job{
Name: "Other User Job",
Schedule: "* * * * *",
ConfigID: config.ID,
Enabled: BoolPtr(true),
CreatedBy: otherUser.ID,
}
database.Create(otherJob)
// Set up route
router.POST("/api/jobs/:id/run", handlers.HandleAPIRunJob)
// Test case 1: Run own job
req, _ := http.NewRequest("POST", "/api/jobs/"+strconv.Itoa(int(job.ID))+"/run", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Test case 2: Try to run another user's job
req, _ = http.NewRequest("POST", "/api/jobs/"+strconv.Itoa(int(otherJob.ID))+"/run", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be forbidden
assert.Equal(t, http.StatusForbidden, resp.Code)
// Test case 3: Admin can run any job
// Create a new router with admin permissions but using the same handlers
adminRouter := gin.New()
adminRouter.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("email", user.Email)
c.Set("username", "testuser")
c.Set("isAdmin", true) // Set admin flag to true
c.Next()
})
adminRouter.POST("/api/jobs/:id/run", handlers.HandleAPIRunJob)
req, _ = http.NewRequest("POST", "/api/jobs/"+strconv.Itoa(int(otherJob.ID))+"/run", nil)
resp = httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
// Check response - admin should be able to run
assert.Equal(t, http.StatusOK, resp.Code)
// Test case 4: Non-existent job
req, _ = http.NewRequest("POST", "/api/jobs/9999/run", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response - should be not found
assert.Equal(t, http.StatusNotFound, resp.Code)
}
-834
View File
@@ -1,834 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/email"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt"
)
func TestAuthMiddleware(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
jwtSecret := "test-jwt-secret"
handlers.JWTSecret = jwtSecret
// Create test route with auth middleware
router.GET("/protected", handlers.AuthMiddleware(), func(c *gin.Context) {
c.String(http.StatusOK, "protected content")
})
// Test case 1: No JWT token
req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to login page
assert.Equal(t, http.StatusFound, resp.Code, "Should redirect to login page")
assert.Equal(t, "/login", resp.Header().Get("Location"), "Should redirect to /login")
// Test case 2: Invalid JWT token
req, _ = http.NewRequest(http.MethodGet, "/protected", nil)
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: "invalid-token",
})
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to login page due to invalid token
assert.Equal(t, http.StatusFound, resp.Code, "Should redirect to login page on invalid token")
assert.Equal(t, "/login", resp.Header().Get("Location"), "Should redirect to /login on invalid token")
// Test case 3: Valid JWT token
// Generate a valid token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": "test@example.com",
"username": "testuser",
"is_admin": false,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(jwtSecret))
req, _ = http.NewRequest(http.MethodGet, "/protected", nil)
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: tokenString,
})
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should allow access to protected content
assert.Equal(t, http.StatusOK, resp.Code, "Should allow access with valid token")
assert.Equal(t, "protected content", resp.Body.String(), "Should return protected content")
}
func TestAdminMiddleware(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Create test route with admin middleware
router.GET("/admin", handlers.AuthMiddleware(), handlers.AdminMiddleware(), func(c *gin.Context) {
c.String(http.StatusOK, "admin content")
})
// Test case 1: Regular user (non-admin)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": "test@example.com",
"username": "testuser",
"is_admin": false,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(handlers.JWTSecret))
req, _ := http.NewRequest(http.MethodGet, "/admin", nil)
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: tokenString,
})
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to dashboard
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/dashboard", resp.Header().Get("Location"))
// Test case 2: Admin user
adminToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 2,
"email": "admin@example.com",
"username": "admin",
"is_admin": true,
"exp": time.Now().Add(time.Hour).Unix(),
})
adminTokenString, _ := adminToken.SignedString([]byte(handlers.JWTSecret))
req, _ = http.NewRequest(http.MethodGet, "/admin", nil)
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: adminTokenString,
})
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should allow access
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "admin content", resp.Body.String())
}
func TestAPIAuthMiddleware(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Create test route with API auth middleware
router.GET("/api/test", handlers.APIAuthMiddleware(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "success"})
})
// Test case 1: No Authorization header
req, _ := http.NewRequest(http.MethodGet, "/api/test", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should return 401 Unauthorized
assert.Equal(t, http.StatusUnauthorized, resp.Code)
assert.Contains(t, resp.Body.String(), "Authorization header is required")
// Test case 2: Invalid Authorization format
req, _ = http.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Authorization", "InvalidFormat")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should return 401 Unauthorized
assert.Equal(t, http.StatusUnauthorized, resp.Code)
assert.Contains(t, resp.Body.String(), "Authorization header format must be Bearer")
// Test case 3: Invalid token
req, _ = http.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should return 401 Unauthorized
assert.Equal(t, http.StatusUnauthorized, resp.Code)
assert.Contains(t, resp.Body.String(), "Invalid or expired token")
// Test case 4: Valid token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": "test@example.com",
"username": "testuser",
"is_admin": false,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(handlers.JWTSecret))
req, _ = http.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Authorization", "Bearer "+tokenString)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should allow access
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "success")
}
func TestAPIAdminMiddleware(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Create test route with API auth and admin middleware
router.GET("/api/admin", handlers.APIAuthMiddleware(), handlers.APIAdminMiddleware(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "admin success"})
})
// Test case 1: Regular user (non-admin)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 1,
"email": "test@example.com",
"username": "testuser",
"is_admin": false,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(handlers.JWTSecret))
req, _ := http.NewRequest(http.MethodGet, "/api/admin", nil)
req.Header.Set("Authorization", "Bearer "+tokenString)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should return 403 Forbidden
assert.Equal(t, http.StatusForbidden, resp.Code)
assert.Contains(t, resp.Body.String(), "Admin privileges required")
// Test case 2: Admin user
adminToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 2,
"email": "admin@example.com",
"username": "admin",
"is_admin": true,
"exp": time.Now().Add(time.Hour).Unix(),
})
adminTokenString, _ := adminToken.SignedString([]byte(handlers.JWTSecret))
req, _ = http.NewRequest(http.MethodGet, "/api/admin", nil)
req.Header.Set("Authorization", "Bearer "+adminTokenString)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should allow access
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "admin success")
}
func TestGenerateJWT(t *testing.T) {
// Setup
handlers, _ := setupTestHandlers(t)
handlers.JWTSecret = "test-jwt-secret"
// Generate JWT
token, err := handlers.GenerateJWT(1, "testuser", false)
// Check token was generated
assert.NoError(t, err)
assert.NotEmpty(t, token)
// Validate token
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(handlers.JWTSecret), nil
})
assert.NoError(t, err)
assert.True(t, parsedToken.Valid)
// Check claims
claims, ok := parsedToken.Claims.(jwt.MapClaims)
assert.True(t, ok)
assert.Equal(t, float64(1), claims["user_id"])
assert.Equal(t, "testuser", claims["username"])
assert.Equal(t, false, claims["is_admin"])
assert.NotEmpty(t, claims["exp"])
}
func TestHandleLoginPage(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Add route
router.GET("/login", handlers.HandleLoginPage)
// Test case 1: Basic login page
req, _ := http.NewRequest(http.MethodGet, "/login", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Login - GoMFT")
assert.Contains(t, resp.Body.String(), "Sign In")
assert.Contains(t, resp.Body.String(), "Access your GoMFT account")
// Test case 2: Login page with message
req, _ = http.NewRequest(http.MethodGet, "/login?message=Password+expired", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Password expired")
}
func TestHandleLogin(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup database and test user
database := testutils.SetupTestDB(t)
// Create test user with password "password123"
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
user := &db.User{
Email: "test@example.com",
PasswordHash: string(hashedPassword),
IsAdmin: BoolPtr(false),
FailedLoginAttempts: 0,
AccountLocked: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(user)
// Setup handlers
handlers := &Handlers{
DB: database,
JWTSecret: "test-jwt-secret",
}
// Setup router
router := gin.New()
router.POST("/login", handlers.HandleLogin)
// Test case 1: Successful login
formData := url.Values{
"email": {"test@example.com"},
"password": {"password123"},
}
req, _ := http.NewRequest(http.MethodPost, "/login", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to dashboard
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/dashboard", resp.Header().Get("Location"))
// Should set JWT cookie
cookies := resp.Result().Cookies()
var jwtCookie *http.Cookie
for _, cookie := range cookies {
if cookie.Name == "jwt_token" {
jwtCookie = cookie
break
}
}
assert.NotNil(t, jwtCookie)
assert.NotEmpty(t, jwtCookie.Value)
// Test case 2: Invalid password
formData = url.Values{
"email": {"test@example.com"},
"password": {"wrongpassword"},
}
req, _ = http.NewRequest(http.MethodPost, "/login", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Invalid credentials")
// Test case 3: Non-existent user
formData = url.Values{
"email": {"nonexistent@example.com"},
"password": {"password123"},
}
req, _ = http.NewRequest(http.MethodPost, "/login", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Invalid credentials")
}
func TestHandleLogout(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Add route
router.GET("/logout", handlers.HandleLogout)
// Create request
req, _ := http.NewRequest(http.MethodGet, "/logout", nil)
resp := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusFound, resp.Code, "Should redirect")
assert.Equal(t, "/login", resp.Header().Get("Location"), "Should redirect to login page")
// Check that cookie is cleared
cookies := resp.Result().Cookies()
found := false
for _, cookie := range cookies {
if cookie.Name == "jwt_token" {
assert.Equal(t, "", cookie.Value, "JWT cookie should be cleared")
assert.True(t, cookie.Expires.Before(time.Now()), "Cookie should be expired")
found = true
break
}
}
assert.True(t, found, "Should find jwt_token cookie in response")
}
func TestHandleChangePassword(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup database and test user
database := testutils.SetupTestDB(t)
// Create test user with password "OldPassword123!"
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("OldPassword123!"), bcrypt.DefaultCost)
user := &db.User{
Email: "test@example.com",
PasswordHash: string(hashedPassword),
IsAdmin: BoolPtr(false),
FailedLoginAttempts: 0,
AccountLocked: BoolPtr(false),
LastPasswordChange: time.Now().Add(-24 * time.Hour), // 1 day ago
}
database.Create(user)
// Setup handlers with email mock
mockEmail := email.NewMockService()
handlers := &Handlers{
DB: database,
JWTSecret: "test-jwt-secret",
Email: mockEmail,
}
// Create JWT token for this user
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"email": user.Email,
"username": "testuser",
"is_admin": false,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte(handlers.JWTSecret))
// Setup router
router := gin.New()
router.POST("/change-password", handlers.HandleChangePassword)
// Test case 1: Successful password change
formData := url.Values{
"current_password": {"OldPassword123!"},
"new_password": {"NewPassword456@"},
"confirm_password": {"NewPassword456@"},
}
req, _ := http.NewRequest(http.MethodPost, "/change-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: tokenString,
})
req.Header.Set("HX-Request", "true") // Simulate HTMX request
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show success message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Password updated successfully")
assert.Contains(t, resp.Body.String(), "bg-green-100")
assert.Contains(t, resp.Body.String(), "border-green-400")
// Verify password was updated in the database
var updatedUser db.User
err := database.First(&updatedUser, user.ID).Error
assert.NoError(t, err, "Should be able to find the user")
err = bcrypt.CompareHashAndPassword([]byte(updatedUser.PasswordHash), []byte("NewPassword456@"))
assert.NoError(t, err, "Password should be updated in the database")
// Test case 2: Incorrect current password
formData = url.Values{
"current_password": {"WrongPassword123!"},
"new_password": {"AnotherPassword789#"},
"confirm_password": {"AnotherPassword789#"},
}
req, _ = http.NewRequest(http.MethodPost, "/change-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: tokenString,
})
req.Header.Set("HX-Request", "true") // Simulate HTMX request
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Current password is incorrect")
assert.Contains(t, resp.Body.String(), "bg-red-100")
assert.Contains(t, resp.Body.String(), "border-red-400")
// Test case 3: Passwords don't match
formData = url.Values{
"current_password": {"NewPassword456@"}, // Using the updated password
"new_password": {"DiffPassword123!"},
"confirm_password": {"DiffPassword456@"},
}
req, _ = http.NewRequest(http.MethodPost, "/change-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{
Name: "jwt_token",
Value: tokenString,
})
req.Header.Set("HX-Request", "true") // Simulate HTMX request
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "New password and confirmation do not match")
assert.Contains(t, resp.Body.String(), "bg-red-100")
assert.Contains(t, resp.Body.String(), "border-red-400")
}
func TestHandleForgotPasswordPage(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup
handlers, router := setupTestHandlers(t)
// Add route
router.GET("/forgot-password", handlers.HandleForgotPasswordPage)
// Create request
req, _ := http.NewRequest(http.MethodGet, "/forgot-password", nil)
resp := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Forgot Password - GoMFT")
assert.Contains(t, resp.Body.String(), "Password Reset")
assert.Contains(t, resp.Body.String(), "Enter your email to receive a reset link")
}
func TestHandleForgotPassword(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup database and test user
database := testutils.SetupTestDB(t)
// Create test user
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
user := &db.User{
Email: "test@example.com",
PasswordHash: string(hashedPassword),
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(user)
// Setup handlers with email mock
mockEmail := email.NewMockService()
handlers := &Handlers{
DB: database,
JWTSecret: "test-jwt-secret",
Email: mockEmail,
}
// Setup router
router := gin.New()
router.POST("/forgot-password", handlers.HandleForgotPassword)
// Test case 1: Valid email
formData := url.Values{
"email": {"test@example.com"},
}
req, _ := http.NewRequest(http.MethodPost, "/forgot-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show generic success message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "If your email is registered")
// Check if reset token was created
var resetToken db.PasswordResetToken
result := database.Where("user_id = ?", user.ID).First(&resetToken)
assert.NoError(t, result.Error, "Reset token should be created")
assert.NotEmpty(t, resetToken.Token, "Token should not be empty")
assert.False(t, resetToken.GetUsed(), "Token should not be marked as used")
// Verify email would have been sent (if not mocked)
// Note: We can't check SendPasswordResetEmailCalls with our current mock
// assert.Equal(t, 1, mockEmail.SendPasswordResetEmailCalls)
// Test case 2: Non-existent email
formData = url.Values{
"email": {"nonexistent@example.com"},
}
req, _ = http.NewRequest(http.MethodPost, "/forgot-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show generic success message (even though user doesn't exist)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "If your email is registered")
// Test case 3: Missing email
formData = url.Values{}
req, _ = http.NewRequest(http.MethodPost, "/forgot-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error message
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Email is required")
}
func TestHandleResetPasswordPage(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup database
database := testutils.SetupTestDB(t)
// Create test user
user := &db.User{
Email: "test@example.com",
PasswordHash: "hashedpassword",
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now(),
}
database.Create(user)
// Create reset token
token := "valid-reset-token"
resetToken := &db.PasswordResetToken{
UserID: user.ID,
Token: token,
ExpiresAt: time.Now().Add(15 * time.Minute),
Used: BoolPtr(false),
}
database.Create(resetToken)
// Setup handlers
handlers := &Handlers{
DB: database,
}
// Setup router
router := gin.New()
router.GET("/reset-password", handlers.HandleResetPasswordPage)
// Test case 1: Valid token
req, _ := http.NewRequest(http.MethodGet, "/reset-password?token="+token, nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show reset password form
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Reset Password")
assert.Contains(t, resp.Body.String(), token) // Token should be in the form
// Test case 2: No token
req, _ = http.NewRequest(http.MethodGet, "/reset-password", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to forgot password page
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/forgot-password", resp.Header().Get("Location"))
// Test case 3: Invalid token
req, _ = http.NewRequest(http.MethodGet, "/reset-password?token=invalid-token", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to forgot password page
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/forgot-password", resp.Header().Get("Location"))
}
func TestHandleResetPassword(t *testing.T) {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Setup database
database := testutils.SetupTestDB(t)
// Create test user
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), bcrypt.DefaultCost)
user := &db.User{
Email: "test@example.com",
PasswordHash: string(hashedPassword),
IsAdmin: BoolPtr(false),
LastPasswordChange: time.Now().Add(-24 * time.Hour), // 1 day ago
}
database.Create(user)
// Create reset token
token := "valid-reset-token"
resetToken := &db.PasswordResetToken{
UserID: user.ID,
Token: token,
ExpiresAt: time.Now().Add(15 * time.Minute),
Used: BoolPtr(false),
}
database.Create(resetToken)
// Setup handlers
handlers := &Handlers{
DB: database,
}
// Setup router
router := gin.New()
router.POST("/reset-password", handlers.HandleResetPassword)
// Test case 1: Successful password reset
formData := url.Values{
"token": {token},
"password": {"newpassword123"},
"confirm-password": {"newpassword123"},
}
req, _ := http.NewRequest(http.MethodPost, "/reset-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to login with success message
assert.Equal(t, http.StatusFound, resp.Code)
assert.Contains(t, resp.Header().Get("Location"), "/login?message=Password+reset+successful")
// Verify password was updated
var updatedUser db.User
database.First(&updatedUser, user.ID)
err := bcrypt.CompareHashAndPassword([]byte(updatedUser.PasswordHash), []byte("newpassword123"))
assert.NoError(t, err, "Password should be updated in the database")
// Verify token is marked as used
var updatedToken db.PasswordResetToken
database.First(&updatedToken, resetToken.ID)
assert.True(t, updatedToken.GetUsed(), "Token should be marked as used")
// Test case 2: Passwords don't match
// Create another token first
token2 := "another-valid-token"
resetToken2 := &db.PasswordResetToken{
UserID: user.ID,
Token: token2,
ExpiresAt: time.Now().Add(15 * time.Minute),
Used: BoolPtr(false),
}
database.Create(resetToken2)
formData = url.Values{
"token": {token2},
"password": {"newpass1"},
"confirm-password": {"newpass2"},
}
req, _ = http.NewRequest(http.MethodPost, "/reset-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Passwords do not match")
// Test case 3: Password too short
token3 := "yet-another-valid-token"
resetToken3 := &db.PasswordResetToken{
UserID: user.ID,
Token: token3,
ExpiresAt: time.Now().Add(15 * time.Minute),
Used: BoolPtr(false),
}
database.Create(resetToken3)
formData = url.Values{
"token": {token3},
"password": {"short"},
"confirm-password": {"short"},
}
req, _ = http.NewRequest(http.MethodPost, "/reset-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should show error
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Password must be at least 8 characters long")
// Test case 4: No token
formData = url.Values{
"password": {"validpassword"},
"confirm-password": {"validpassword"},
}
req, _ = http.NewRequest(http.MethodPost, "/reset-password", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to forgot password page
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/forgot-password", resp.Header().Get("Location"))
}
@@ -1,29 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandleHome(t *testing.T) {
// Set up test environment
handlers, router := setupTestHandlers(t)
// Set up the route
router.GET("/", handlers.HandleHome)
// Create a test request
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(w, req)
// Check response
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Home - GoMFT")
assert.Contains(t, w.Body.String(), "Welcome to GoMFT")
}
@@ -1,436 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
)
func setupConfigTest(t *testing.T) (*Handlers, *gin.Engine, *db.DB, *db.User) {
// Set up test database
database := testutils.SetupTestDB(t)
// Create test user
user := testutils.CreateTestUser(t, database, "test@example.com", false)
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers
handlers := &Handlers{
DB: database,
}
// Set up authentication middleware
router.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("isAdmin", false)
c.Next()
})
return handlers, router, database, user
}
func createTestConfig(t *testing.T, database *db.DB, userID uint) *db.TransferConfig {
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: userID,
}
if err := database.Create(config).Error; err != nil {
t.Fatalf("Failed to create test config: %v", err)
}
return config
}
func TestHandleConfigs(t *testing.T) {
handlers, router, database, user := setupConfigTest(t)
// Create test configs
config1 := createTestConfig(t, database, user.ID)
config2 := createTestConfig(t, database, user.ID)
// Create a config for another user
otherUser := testutils.CreateTestUser(t, database, "other@example.com", false)
createTestConfig(t, database, otherUser.ID)
// Set up route
router.GET("/configs", handlers.HandleConfigs)
// Create request
req, _ := http.NewRequest("GET", "/configs", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Response should include user's configs
assert.Contains(t, resp.Body.String(), config1.Name)
assert.Contains(t, resp.Body.String(), config2.Name)
// Should not contain configs from other users
assert.Contains(t, resp.Body.String(), strconv.Itoa(int(config1.ID)))
assert.Contains(t, resp.Body.String(), strconv.Itoa(int(config2.ID)))
assert.NotContains(t, resp.Body.String(), "other@example.com")
}
func TestHandleNewConfig(t *testing.T) {
handlers, router, _, _ := setupConfigTest(t)
// Set up route
router.GET("/configs/new", handlers.HandleNewConfig)
// Create request
req, _ := http.NewRequest("GET", "/configs/new", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "New Configuration")
assert.Contains(t, resp.Body.String(), "Source Type")
assert.Contains(t, resp.Body.String(), "Destination Type")
}
func TestHandleEditConfig(t *testing.T) {
handlers, router, database, user := setupConfigTest(t)
// Create test config
config := createTestConfig(t, database, user.ID)
// Create a config for another user
otherUser := testutils.CreateTestUser(t, database, "other@example.com", false)
otherConfig := createTestConfig(t, database, otherUser.ID)
// Set up route
router.GET("/configs/:id/edit", handlers.HandleEditConfig)
// Test cases
testCases := []struct {
name string
configID uint
expectedCode int
expectedBody string
}{
{
name: "Edit own config",
configID: config.ID,
expectedCode: http.StatusOK,
expectedBody: "Edit Configuration",
},
{
name: "Cannot edit other user's config",
configID: otherConfig.ID,
expectedCode: http.StatusFound, // Redirect to /configs
expectedBody: "",
},
{
name: "Non-existent config",
configID: 9999,
expectedCode: http.StatusFound, // Redirect to /configs
expectedBody: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create request
req, _ := http.NewRequest("GET", "/configs/"+strconv.Itoa(int(tc.configID))+"/edit", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
if tc.expectedBody != "" {
assert.Contains(t, resp.Body.String(), tc.expectedBody)
}
})
}
// Test admin access to other user's config
adminRouter := gin.New()
adminRouter.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("isAdmin", true) // Set as admin
c.Next()
})
adminRouter.GET("/configs/:id/edit", handlers.HandleEditConfig)
// Admin should be able to edit other user's config
req, _ := http.NewRequest("GET", "/configs/"+strconv.Itoa(int(otherConfig.ID))+"/edit", nil)
resp := httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Edit Configuration")
}
func TestHandleCreateConfig(t *testing.T) {
handlers, router, database, user := setupConfigTest(t)
// Set up route
router.POST("/configs", handlers.HandleCreateConfig)
// Prepare form data
formData := url.Values{
"name": {"New Test Config"},
"source_type": {"local"},
"source_path": {"/test/source"},
"destination_type": {"local"},
"destination_path": {"/test/dest"},
"file_pattern": {"*.txt"},
}
// Create request
req, _ := http.NewRequest("POST", "/configs", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response (should redirect on success)
assert.Equal(t, http.StatusFound, resp.Code)
assert.Equal(t, "/configs", resp.Header().Get("Location"))
// Verify config was created in database
var configs []db.TransferConfig
database.Where("created_by = ?", user.ID).Find(&configs)
assert.Equal(t, 1, len(configs))
assert.Equal(t, "New Test Config", configs[0].Name)
assert.Equal(t, "local", configs[0].SourceType)
assert.Equal(t, "/test/source", configs[0].SourcePath)
assert.Equal(t, "local", configs[0].DestinationType)
assert.Equal(t, "/test/dest", configs[0].DestinationPath)
}
func TestHandleUpdateConfig(t *testing.T) {
handlers, router, database, user := setupConfigTest(t)
// Create test config
config := createTestConfig(t, database, user.ID)
// Create a config for another user
otherUser := testutils.CreateTestUser(t, database, "other@example.com", false)
otherConfig := createTestConfig(t, database, otherUser.ID)
// Set up route
router.PUT("/configs/:id", handlers.HandleUpdateConfig)
// Prepare form data for update
formData := url.Values{
"name": {"Updated Config"},
"source_type": {"local"},
"source_path": {"/updated/source"},
"destination_type": {"local"},
"destination_path": {"/updated/dest"},
"file_pattern": {"*.csv"},
}
// Test cases
testCases := []struct {
name string
configID uint
expectedCode int
checkUpdate bool
}{
{
name: "Update own config",
configID: config.ID,
expectedCode: http.StatusFound, // Redirect to /configs
checkUpdate: true,
},
{
name: "Cannot update other user's config",
configID: otherConfig.ID,
expectedCode: http.StatusForbidden,
checkUpdate: false,
},
{
name: "Non-existent config",
configID: 9999,
expectedCode: http.StatusNotFound,
checkUpdate: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create request
req, _ := http.NewRequest("PUT", "/configs/"+strconv.Itoa(int(tc.configID)), strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
// Verify config was updated if expected
if tc.checkUpdate {
var updatedConfig db.TransferConfig
database.First(&updatedConfig, tc.configID)
assert.Equal(t, "Updated Config", updatedConfig.Name)
assert.Equal(t, "/updated/source", updatedConfig.SourcePath)
assert.Equal(t, "/updated/dest", updatedConfig.DestinationPath)
assert.Equal(t, "*.csv", updatedConfig.FilePattern)
}
})
}
// Test admin access to update other user's config
adminRouter := gin.New()
adminRouter.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("isAdmin", true) // Set as admin
c.Next()
})
adminRouter.PUT("/configs/:id", handlers.HandleUpdateConfig)
// Admin should be able to update other user's config
req, _ := http.NewRequest("PUT", "/configs/"+strconv.Itoa(int(otherConfig.ID)), strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
assert.Equal(t, http.StatusFound, resp.Code)
// Verify other user's config was updated
var updatedOtherConfig db.TransferConfig
database.First(&updatedOtherConfig, otherConfig.ID)
assert.Equal(t, "Updated Config", updatedOtherConfig.Name)
}
func TestHandleDeleteConfig(t *testing.T) {
handlers, router, database, user := setupConfigTest(t)
// Create test config
config := createTestConfig(t, database, user.ID)
// Create a config for another user
otherUser := testutils.CreateTestUser(t, database, "other@example.com", false)
otherConfig := createTestConfig(t, database, otherUser.ID)
// Create config with associated job
configWithJob := createTestConfig(t, database, user.ID)
job := &db.Job{
Name: "Test Job",
Schedule: "*/5 * * * *",
ConfigID: configWithJob.ID,
Enabled: BoolPtr(true),
CreatedBy: user.ID,
}
if err := database.Create(job).Error; err != nil {
t.Fatalf("Failed to create test job: %v", err)
}
// Set up route
router.DELETE("/configs/:id", handlers.HandleDeleteConfig)
// Test cases
testCases := []struct {
name string
configID uint
expectedCode int
errorMsg string
}{
{
name: "Delete own config",
configID: config.ID,
expectedCode: http.StatusOK,
errorMsg: "",
},
{
name: "Cannot delete other user's config",
configID: otherConfig.ID,
expectedCode: http.StatusForbidden,
errorMsg: "You do not have permission to delete this config",
},
{
name: "Cannot delete config with jobs",
configID: configWithJob.ID,
expectedCode: http.StatusBadRequest,
errorMsg: "Config is in use by jobs and cannot be deleted",
},
{
name: "Non-existent config",
configID: 9999,
expectedCode: http.StatusNotFound,
errorMsg: "Config not found",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create request
req, _ := http.NewRequest("DELETE", "/configs/"+strconv.Itoa(int(tc.configID)), nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
if tc.errorMsg != "" {
// Parse response body
var response map[string]string
err := json.Unmarshal(resp.Body.Bytes(), &response)
assert.NoError(t, err)
// Check error message
assert.Equal(t, tc.errorMsg, response["error"])
} else {
// Verify config was deleted - using a new DB query
var foundConfig db.TransferConfig
err := database.First(&foundConfig, tc.configID).Error
assert.Error(t, err, "Expected config to be deleted but it was found")
}
})
}
// Test admin access to delete other user's config
adminRouter := gin.New()
adminRouter.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("isAdmin", true) // Set as admin
c.Next()
})
adminRouter.DELETE("/configs/:id", handlers.HandleDeleteConfig)
// Admin should be able to delete other user's config
req, _ := http.NewRequest("DELETE", "/configs/"+strconv.Itoa(int(otherConfig.ID)), nil)
resp := httptest.NewRecorder()
adminRouter.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
// Verify config was deleted
var foundConfig db.TransferConfig
err := database.First(&foundConfig, otherConfig.ID).Error
assert.Error(t, err, "Expected config to be deleted but it was found")
}
@@ -1,286 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
)
func setupDashboardTest(t *testing.T) (*Handlers, *gin.Engine, *db.DB) {
// Set up test database
database := testutils.SetupTestDB(t)
// Create test user
user := testutils.CreateTestUser(t, database, "test@example.com", false)
// Create test config
config := &db.TransferConfig{
Name: "Test Config",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: user.ID,
}
if err := database.DB.Create(config).Error; err != nil {
t.Fatalf("Failed to create transfer config: %v", err)
}
// Create test job
job := &db.Job{
Name: "Test Job",
Schedule: "*/5 * * * *",
ConfigID: config.ID,
Enabled: BoolPtr(true),
CreatedBy: user.ID,
}
if err := database.DB.Create(job).Error; err != nil {
t.Fatalf("Failed to create job: %v", err)
}
// Create test job history entries
now := time.Now()
// Completed job
completedJob := &db.JobHistory{
JobID: job.ID,
StartTime: now.Add(-time.Hour),
EndTime: &now,
Status: "completed",
BytesTransferred: 1024,
FilesTransferred: 1,
}
if err := database.DB.Create(completedJob).Error; err != nil {
t.Fatalf("Failed to create completed job history: %v", err)
}
// Failed job
failedJob := &db.JobHistory{
JobID: job.ID,
StartTime: now.Add(-2 * time.Hour),
EndTime: &now,
Status: "failed",
ErrorMessage: "Test error",
}
if err := database.DB.Create(failedJob).Error; err != nil {
t.Fatalf("Failed to create failed job history: %v", err)
}
// Running job
runningJob := &db.JobHistory{
JobID: job.ID,
StartTime: now.Add(-30 * time.Minute),
Status: "running",
}
if err := database.DB.Create(runningJob).Error; err != nil {
t.Fatalf("Failed to create running job history: %v", err)
}
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers
handlers := &Handlers{
DB: database,
}
// Set up authentication middleware
router.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Set("isAdmin", false)
c.Next()
})
return handlers, router, database
}
func TestHandleDashboard(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/dashboard", handlers.HandleDashboard)
// Create request
req, _ := http.NewRequest("GET", "/dashboard", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Dashboard")
assert.Contains(t, resp.Body.String(), "Recent Jobs")
// Check that job statistics are included
assert.Contains(t, resp.Body.String(), "Active Transfers")
assert.Contains(t, resp.Body.String(), "Completed Today")
assert.Contains(t, resp.Body.String(), "Failed Transfers")
}
func TestHandleHistory(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/history", handlers.HandleHistory)
// Create request
req, _ := http.NewRequest("GET", "/history", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Transfer History")
// Check that job history is included
assert.Contains(t, resp.Body.String(), "Test Config")
assert.Contains(t, resp.Body.String(), "Completed")
assert.Contains(t, resp.Body.String(), "Failed")
}
func TestHandleHistoryWithPagination(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/history", handlers.HandleHistory)
testCases := []struct {
name string
url string
expectedStatus int
expectedContent string
}{
{
name: "Default pagination",
url: "/history",
expectedStatus: http.StatusOK,
expectedContent: "Test Config",
},
{
name: "Custom page size",
url: "/history?pageSize=25",
expectedStatus: http.StatusOK,
expectedContent: "Test Config",
},
{
name: "Invalid page size defaults to 10",
url: "/history?pageSize=invalid",
expectedStatus: http.StatusOK,
expectedContent: "Test Config",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", tc.url, nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, tc.expectedStatus, resp.Code)
assert.Contains(t, resp.Body.String(), tc.expectedContent)
})
}
}
func TestHandleHistoryWithSearch(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/history", handlers.HandleHistory)
// Test search
req, _ := http.NewRequest("GET", "/history?search=completed", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "completed")
assert.NotContains(t, resp.Body.String(), "failed") // Should filter out failed jobs
}
func TestHandleHistoryWithHtmx(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/history", handlers.HandleHistory)
// Test HTMX request
req, _ := http.NewRequest("GET", "/history", nil)
req.Header.Set("HX-Request", "true")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
// Should only contain the history content, not the full page
assert.Contains(t, resp.Body.String(), "Test Config")
assert.NotContains(t, resp.Body.String(), "<html")
}
func TestHandleDashboardData(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/dashboard/data", handlers.HandleDashboardData)
// Create request
req, _ := http.NewRequest("GET", "/dashboard/data", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "recent_runs")
}
func TestHandleDashboardJobsData(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/dashboard/jobs", handlers.HandleDashboardJobsData)
// Create request
req, _ := http.NewRequest("GET", "/dashboard/jobs", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "active_jobs")
}
func TestHandleDashboardHistoryData(t *testing.T) {
handlers, router, _ := setupDashboardTest(t)
// Set up route
router.GET("/dashboard/history", handlers.HandleDashboardHistoryData)
// Create request
req, _ := http.NewRequest("GET", "/dashboard/history", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "success_count")
assert.Contains(t, resp.Body.String(), "failure_count")
assert.Contains(t, resp.Body.String(), "pending_count")
}
@@ -1,401 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
)
func setupFileMetadataHandlers(t *testing.T) (*FileMetadataHandler, *gin.Engine, *db.User, *db.Job) {
// Get base handlers and router from the shared setup
handlers, router := setupTestHandlers(t)
// Create a test user with a unique email
testUser := &db.User{
Email: fmt.Sprintf("file-meta-test-%d@example.com", time.Now().UnixNano()),
PasswordHash: "hashed_password",
LastPasswordChange: time.Now(),
}
err := handlers.DB.CreateUser(testUser)
assert.NoError(t, err)
// Create a test config
testConfig := &db.TransferConfig{
Name: "Test Config for File Metadata",
SourceType: "local",
SourcePath: "/source/path",
DestinationType: "local",
DestinationPath: "/destination/path",
CreatedBy: testUser.ID,
}
err = handlers.DB.CreateTransferConfig(testConfig)
assert.NoError(t, err)
// Create a test job
testJob := &db.Job{
Name: "Test Job for File Metadata",
ConfigID: testConfig.ID,
Schedule: "0 * * * *", // Run hourly
Enabled: BoolPtr(true),
CreatedBy: testUser.ID,
}
err = handlers.DB.CreateJob(testJob)
assert.NoError(t, err)
// Create test file metadata entries
for i := 0; i < 5; i++ {
fileMetadata := &db.FileMetadata{
JobID: testJob.ID,
FileName: fmt.Sprintf("testfile%d.txt", i),
OriginalPath: fmt.Sprintf("/source/path/testfile%d.txt", i),
FileSize: int64(1024 * (i + 1)),
FileHash: fmt.Sprintf("hash%d", i),
CreationTime: time.Now().Add(-24 * time.Hour),
ModTime: time.Now().Add(-12 * time.Hour),
ProcessedTime: time.Now(),
DestinationPath: fmt.Sprintf("/destination/path/testfile%d.txt", i),
Status: "processed",
}
err = handlers.DB.CreateFileMetadata(fileMetadata)
assert.NoError(t, err)
}
// Create middleware to simulate authenticated user
router.Use(func(c *gin.Context) {
c.Set("userID", testUser.ID)
c.Next()
})
// Create the FileMetadataHandler that we'll test
fileMetadataHandler := &FileMetadataHandler{
DB: handlers.DB,
}
return fileMetadataHandler, router, testUser, testJob
}
// Helper function to set HTMX headers on request
func setHTMXHeaders(req *http.Request) {
req.Header.Set("HX-Request", "true")
}
func TestListFileMetadata(t *testing.T) {
// Setup
handler, router, testUser, testJob := setupFileMetadataHandlers(t)
// Ensure job is owned by test user
testJob.CreatedBy = testUser.ID
handler.DB.DB.Save(testJob)
// Recreate file metadata entries to ensure they're properly linked to the updated job
handler.DB.DB.Unscoped().Where("job_id = ?", testJob.ID).Delete(&db.FileMetadata{})
// Create new test file metadata entries for the job
var fileIDs []uint
for i := 0; i < 5; i++ {
fileMetadata := &db.FileMetadata{
JobID: testJob.ID,
FileName: fmt.Sprintf("testfile%d.txt", i),
OriginalPath: fmt.Sprintf("/source/path/testfile%d.txt", i),
FileSize: int64(1024 * (i + 1)),
FileHash: fmt.Sprintf("hash%d", i),
CreationTime: time.Now().Add(-24 * time.Hour),
ModTime: time.Now().Add(-12 * time.Hour),
ProcessedTime: time.Now(),
DestinationPath: fmt.Sprintf("/destination/path/testfile%d.txt", i),
Status: "processed",
}
err := handler.DB.CreateFileMetadata(fileMetadata)
assert.NoError(t, err)
fileIDs = append(fileIDs, fileMetadata.ID)
}
// Setup route
router.GET("/files", handler.ListFileMetadata)
// Test default pagination (page 1, limit 50)
req, _ := http.NewRequest(http.MethodGet, "/files", nil)
setHTMXHeaders(req) // Add HTMX header
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response status code
assert.Equal(t, http.StatusOK, resp.Code)
// Verify that the database contains the expected records
var count int64
handler.DB.DB.Model(&db.FileMetadata{}).Where("job_id = ?", testJob.ID).Count(&count)
assert.Equal(t, int64(5), count)
// Test with pagination params
req, _ = http.NewRequest(http.MethodGet, "/files?page=1&limit=2", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response status code
assert.Equal(t, http.StatusOK, resp.Code)
// Test with status filter
req, _ = http.NewRequest(http.MethodGet, "/files?status=processed", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response status code
assert.Equal(t, http.StatusOK, resp.Code)
// Verify that the database contains the expected records with the status filter
handler.DB.DB.Model(&db.FileMetadata{}).Where("job_id = ? AND status = ?", testJob.ID, "processed").Count(&count)
assert.Equal(t, int64(5), count)
}
func TestGetFileMetadataDetails(t *testing.T) {
// Setup
handler, router, testUser, _ := setupFileMetadataHandlers(t)
// Setup route
router.GET("/files/:id", handler.GetFileMetadataDetails)
// Get first file metadata ID
var firstMetadata db.FileMetadata
result := handler.DB.DB.First(&firstMetadata)
assert.NoError(t, result.Error)
// Update the job to make sure the test user owns it
var job db.Job
handler.DB.DB.First(&job, firstMetadata.JobID)
job.CreatedBy = testUser.ID
handler.DB.DB.Save(&job)
// Test getting details for valid ID
req, _ := http.NewRequest(http.MethodGet, "/files/"+strconv.Itoa(int(firstMetadata.ID)), nil)
setHTMXHeaders(req) // Add HTMX header
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), firstMetadata.FileName)
assert.Contains(t, resp.Body.String(), firstMetadata.Status)
// Test getting details for invalid ID
req, _ = http.NewRequest(http.MethodGet, "/files/999999", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code)
}
func TestDeleteFileMetadata(t *testing.T) {
// Setup
handler, router, testUser, _ := setupFileMetadataHandlers(t)
// Setup route
router.DELETE("/files/:id", handler.DeleteFileMetadata)
// Get first file metadata ID
var firstMetadata db.FileMetadata
result := handler.DB.DB.First(&firstMetadata)
assert.NoError(t, result.Error)
// Update the job to make sure the test user owns it
var job db.Job
handler.DB.DB.First(&job, firstMetadata.JobID)
job.CreatedBy = testUser.ID
handler.DB.DB.Save(&job)
// Test deleting with valid ID
req, _ := http.NewRequest(http.MethodDelete, "/files/"+strconv.Itoa(int(firstMetadata.ID)), nil)
setHTMXHeaders(req) // Add HTMX header
resp := httptest.NewRecorder()
fmt.Println("Deleting file metadata")
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Verify deletion
var deletedMetadata db.FileMetadata
result = handler.DB.DB.First(&deletedMetadata, firstMetadata.ID)
assert.Error(t, result.Error) // Should not find the deleted record
// Test deleting with invalid ID
req, _ = http.NewRequest(http.MethodDelete, "/files/999999", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
fmt.Println("Deleting file metadata")
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code)
}
func TestGetFileMetadataForJob(t *testing.T) {
// Setup
handler, router, testUser, testJob := setupFileMetadataHandlers(t)
// Ensure job is owned by test user
testJob.CreatedBy = testUser.ID
handler.DB.DB.Save(testJob)
// Setup route
router.GET("/files/job/:job_id", handler.GetFileMetadataForJob)
// Test getting files for valid job ID
req, _ := http.NewRequest(http.MethodGet, "/files/job/"+strconv.Itoa(int(testJob.ID)), nil)
setHTMXHeaders(req) // Add HTMX header
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "testfile0.txt")
// Test getting files for invalid job ID
req, _ = http.NewRequest(http.MethodGet, "/files/job/999999", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusNotFound, resp.Code) // Not found for invalid job ID
}
func TestSearchFileMetadata(t *testing.T) {
// Setup
handler, router, _, _ := setupFileMetadataHandlers(t)
// Setup route
router.GET("/files/search", handler.SearchFileMetadata)
// Test search by filename
req, _ := http.NewRequest(http.MethodGet, "/files/search?filename=testfile", nil)
setHTMXHeaders(req) // Add HTMX header
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "testfile0.txt")
assert.Contains(t, resp.Body.String(), "testfile4.txt")
// Test search by specific filename
req, _ = http.NewRequest(http.MethodGet, "/files/search?filename=testfile1", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "testfile1.txt")
assert.NotContains(t, resp.Body.String(), "testfile2.txt")
// Test search with no results
req, _ = http.NewRequest(http.MethodGet, "/files/search?filename=nonexistent", nil)
setHTMXHeaders(req) // Add HTMX header
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.NotContains(t, resp.Body.String(), "testfile")
}
func TestHandleFileMetadataPartial(t *testing.T) {
// Setup
handler, router, testUser, testJob := setupFileMetadataHandlers(t)
// Ensure job is owned by test user
testJob.CreatedBy = testUser.ID
handler.DB.DB.Save(testJob)
// Recreate file metadata entries to ensure they're properly linked to the updated job
handler.DB.DB.Unscoped().Where("job_id = ?", testJob.ID).Delete(&db.FileMetadata{})
// Create new test file metadata entries for the job
var fileIDs []uint
for i := 0; i < 5; i++ {
fileMetadata := &db.FileMetadata{
JobID: testJob.ID,
FileName: fmt.Sprintf("testfile%d.txt", i),
OriginalPath: fmt.Sprintf("/source/path/testfile%d.txt", i),
FileSize: int64(1024 * (i + 1)),
FileHash: fmt.Sprintf("hash%d", i),
CreationTime: time.Now().Add(-24 * time.Hour),
ModTime: time.Now().Add(-12 * time.Hour),
ProcessedTime: time.Now(),
DestinationPath: fmt.Sprintf("/destination/path/testfile%d.txt", i),
Status: "processed",
}
err := handler.DB.CreateFileMetadata(fileMetadata)
assert.NoError(t, err)
fileIDs = append(fileIDs, fileMetadata.ID)
}
// Setup route
router.GET("/files/partial", handler.HandleFileMetadataPartial)
// Test partial loading of file metadata (with HTMX header)
req, _ := http.NewRequest(http.MethodGet, "/files/partial?page=1&limit=2", nil)
setHTMXHeaders(req) // Add HTMX headers
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response status code
assert.Equal(t, http.StatusOK, resp.Code)
// Verify that the database contains the expected records
var count int64
handler.DB.DB.Model(&db.FileMetadata{}).Where("job_id = ?", testJob.ID).Count(&count)
assert.Equal(t, int64(5), count)
// Test with different page (with HTMX header)
req, _ = http.NewRequest(http.MethodGet, "/files/partial?page=2&limit=2", nil)
setHTMXHeaders(req) // Add HTMX headers
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response status code
assert.Equal(t, http.StatusOK, resp.Code)
}
func TestHandleFileMetadataSearchPartial(t *testing.T) {
// Setup
handler, router, _, _ := setupFileMetadataHandlers(t)
// Setup route
router.GET("/files/search/partial", handler.HandleFileMetadataSearchPartial)
// Test partial search results (with HTMX header)
req, _ := http.NewRequest(http.MethodGet, "/files/search/partial?filename=testfile&page=1&limit=2", nil)
setHTMXHeaders(req) // Add HTMX headers
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
responseBody := resp.Body.String()
// Verify the response contains test files
assert.Contains(t, responseBody, "testfile")
// Test search with no results (with HTMX header)
req, _ = http.NewRequest(http.MethodGet, "/files/search/partial?filename=nonexistent", nil)
setHTMXHeaders(req) // Add HTMX headers
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.NotContains(t, resp.Body.String(), "testfile")
}
@@ -1,429 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// DBInterface defines the methods we need to mock for our tests
type DBInterface interface {
GetTransferConfig(id uint) (*db.TransferConfig, error)
GetConfigRclonePath(config *db.TransferConfig) string
GenerateRcloneConfigWithToken(config *db.TransferConfig, token string) error
GetGDriveCredentialsFromConfig(config *db.TransferConfig) (string, string)
}
// MockDB is a mock implementation of the DB interface for testing
type MockDB struct {
mock.Mock
}
// Implement the necessary methods from the DB interface for our tests
func (m *MockDB) GetTransferConfig(id uint) (*db.TransferConfig, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*db.TransferConfig), args.Error(1)
}
func (m *MockDB) GetConfigRclonePath(config *db.TransferConfig) string {
args := m.Called(config)
return args.String(0)
}
func (m *MockDB) GenerateRcloneConfigWithToken(config *db.TransferConfig, token string) error {
args := m.Called(config, token)
return args.Error(0)
}
func (m *MockDB) GetGDriveCredentialsFromConfig(config *db.TransferConfig) (string, string) {
args := m.Called(config)
return args.String(0), args.String(1)
}
// MockHandlers is a modified version of Handlers that accepts our mock DB
type MockHandlers struct {
DB DBInterface
}
// HandleGDriveAuth is a copy of the original method but using our interface
func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
// Get the config ID from the query parameter
configIDStr := c.Param("id")
if configIDStr == "" {
RenderErrorPageTest(c, "Missing configuration ID", "")
return
}
configID, err := strconv.ParseUint(configIDStr, 10, 64)
if err != nil {
RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
return
}
// Get the configuration
config, err := h.DB.GetTransferConfig(uint(configID))
if err != nil {
RenderErrorPageTest(c, "Configuration not found", err.Error())
return
}
// Ensure it's a Google Drive or Google Photos configuration
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
RenderErrorPageTest(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
return
}
// Prepare for OAuth
dataDir := os.Getenv("DATA_DIR")
if dataDir == "" {
dataDir = "./data"
}
// Get Rclone Config Path
rcloneConfigPath := h.DB.GetConfigRclonePath(config)
if rcloneConfigPath == "" {
RenderErrorPageTest(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
return
}
// Create a temporary config file for authentication
tempConfigDir := filepath.Join(dataDir, "temp")
if err := os.MkdirAll(tempConfigDir, 0755); err != nil {
RenderErrorPageTest(c, "Failed to create temporary directory", err.Error())
return
}
tempConfigPath := filepath.Join(tempConfigDir, fmt.Sprintf("gdrive_auth_%d.conf", config.ID))
// Store the temporary config path in a cookie
c.SetCookie("gdrive_temp_config", tempConfigPath, 3600, "/", "", false, true)
// Get base URL for redirect URI
baseURL := os.Getenv("BASE_URL")
if baseURL == "" {
// Try to detect the base URL from the request
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
}
// Define the redirect URI for our callback
redirectURI := fmt.Sprintf("%s/configs/gdrive-callback", baseURL)
// Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV
clientID := os.Getenv("GOOGLE_CLIENT_ID")
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
// Check if we have client credentials in the existing config file
existingClientID, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config)
if existingClientID != "" && existingClientSecret != "" {
// Use credentials from existing config
clientID = existingClientID
clientSecret = existingClientSecret
} else {
// fallback to rclone client ID and secret
clientID = "202264815644.apps.googleusercontent.com"
clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
}
}
// Generate state parameter for security (to prevent CSRF)
state := fmt.Sprintf("gomft_%d_%d", config.ID, time.Now().Unix())
// Store state in cookie for validation during callback
c.SetCookie("gdrive_auth_state", state, 3600, "/", "", false, true)
// Store config ID in cookie for use during callback
c.SetCookie("gdrive_config_id", configIDStr, 3600, "/", "", false, true)
// Determine the appropriate scope based on destination type
var scope string
if config.DestinationType == "gphotos" {
// Read-only access is handled elsewhere in the config; here we need the full auth scope
scope = url.QueryEscape("https://www.googleapis.com/auth/photoslibrary")
} else {
// Default to Google Drive scope
scope = url.QueryEscape("https://www.googleapis.com/auth/drive")
}
// Direct Google OAuth URL with our redirect
authURL := fmt.Sprintf("https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&access_type=offline&state=%s",
url.QueryEscape(clientID),
url.QueryEscape(redirectURI),
scope,
url.QueryEscape(state))
// Redirect the user to Google's auth page directly
c.Redirect(http.StatusFound, authURL)
}
// HandleGDriveAuthCallback handles the callback from Google OAuth
func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
// Get auth code from query parameters
authCode := c.Query("code")
if authCode == "" {
RenderErrorPageTest(c, "Authentication failed", "No authorization code received from Google")
return
}
// Verify state parameter to prevent CSRF
state := c.Query("state")
storedState, err := c.Cookie("gdrive_auth_state")
if err != nil || state != storedState {
RenderErrorPageTest(c, "Authentication failed", "Invalid state parameter")
return
}
// Get config ID from cookie
configIDStr, err := c.Cookie("gdrive_config_id")
if err != nil {
RenderErrorPageTest(c, "Authentication failed", "Unable to retrieve configuration ID")
return
}
configID, err := strconv.ParseUint(configIDStr, 10, 64)
if err != nil {
RenderErrorPageTest(c, "Invalid configuration ID", err.Error())
return
}
// Get the configuration
config, err := h.DB.GetTransferConfig(uint(configID))
if err != nil {
RenderErrorPageTest(c, "Failed to get configuration", err.Error())
return
}
// For testing purposes, we'll simulate a successful token exchange
// In a real implementation, we would exchange the auth code for a token
mockToken := `{"access_token":"test_access_token","refresh_token":"test_refresh_token","expiry":"2023-12-31T23:59:59Z"}`
// Update the config with the token
err = h.DB.GenerateRcloneConfigWithToken(config, mockToken)
if err != nil {
RenderErrorPageTest(c, "Failed to update configuration", err.Error())
return
}
// Redirect to the config edit page
c.Redirect(http.StatusFound, fmt.Sprintf("/configs/edit/%d", config.ID))
}
func setupTestRouter() (*gin.Engine, *MockDB) {
gin.SetMode(gin.TestMode)
router := gin.New()
mockDB := new(MockDB)
handlers := &MockHandlers{
DB: mockDB,
}
router.GET("/configs/gdrive/:id", handlers.HandleGDriveAuth)
router.GET("/configs/gdrive-callback", handlers.HandleGDriveAuthCallback)
return router, mockDB
}
func TestHandleGDriveAuth_GoogleDrive(t *testing.T) {
// Setup
router, mockDB := setupTestRouter()
// Create a test config
testConfig := &db.TransferConfig{
ID: 1,
DestinationType: "gdrive",
}
// Set up mock expectations
mockDB.On("GetTransferConfig", uint(1)).Return(testConfig, nil)
mockDB.On("GetConfigRclonePath", testConfig).Return("/path/to/rclone.conf")
mockDB.On("GetGDriveCredentialsFromConfig", testConfig).Return("test_client_id", "test_client_secret")
// Create test request
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/configs/gdrive/1", nil)
router.ServeHTTP(w, req)
// Assertions
assert.Equal(t, http.StatusFound, w.Code)
// Verify the redirect URL
location := w.Header().Get("Location")
assert.Contains(t, location, "accounts.google.com/o/oauth2/auth")
assert.Contains(t, location, "drive")
assert.Contains(t, location, "test_client_id")
// Verify cookies were set
cookies := w.Result().Cookies()
assert.GreaterOrEqual(t, len(cookies), 3)
// Check if state cookie exists
stateFound := false
for _, cookie := range cookies {
if cookie.Name == "gdrive_auth_state" {
stateFound = true
break
}
}
assert.True(t, stateFound)
}
func TestHandleGDriveAuth_GooglePhotos(t *testing.T) {
// Setup
router, mockDB := setupTestRouter()
// Create a test config
testConfig := &db.TransferConfig{
ID: 2,
DestinationType: "gphotos",
}
// Set up mock expectations
mockDB.On("GetTransferConfig", uint(2)).Return(testConfig, nil)
mockDB.On("GetConfigRclonePath", testConfig).Return("/path/to/rclone.conf")
mockDB.On("GetGDriveCredentialsFromConfig", testConfig).Return("test_client_id", "test_client_secret")
// Create test request
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/configs/gdrive/2", nil)
router.ServeHTTP(w, req)
// Assertions
assert.Equal(t, http.StatusFound, w.Code)
// Verify the redirect URL
location := w.Header().Get("Location")
assert.Contains(t, location, "accounts.google.com/o/oauth2/auth")
assert.Contains(t, location, "photoslibrary")
assert.Contains(t, location, "test_client_id")
// Verify cookies were set
cookies := w.Result().Cookies()
assert.GreaterOrEqual(t, len(cookies), 3)
// Check if state cookie exists
stateFound := false
for _, cookie := range cookies {
if cookie.Name == "gdrive_auth_state" {
stateFound = true
break
}
}
assert.True(t, stateFound)
}
func TestHandleGDriveAuthCallback(t *testing.T) {
// Setup test environment
router, mockDB := setupTestRouter()
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "gdrive-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a temporary config file
tempConfigPath := filepath.Join(tempDir, "temp_config.conf")
if err := os.WriteFile(tempConfigPath, []byte("test config"), 0644); err != nil {
t.Fatal(err)
}
// Test state and config ID
testState := "gomft_1_12345"
testConfigID := "1"
// Create a test config
testConfig := &db.TransferConfig{
ID: 1,
DestinationType: "gphotos",
}
// Set up mock expectations
mockDB.On("GetTransferConfig", uint(1)).Return(testConfig, nil)
mockDB.On("GenerateRcloneConfigWithToken", testConfig, mock.Anything).Return(nil)
// Create test request with auth code and state
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/configs/gdrive-callback?code=test_auth_code&state="+testState, nil)
// Add required cookies to the request
req.AddCookie(&http.Cookie{Name: "gdrive_auth_state", Value: testState})
req.AddCookie(&http.Cookie{Name: "gdrive_config_id", Value: testConfigID})
req.AddCookie(&http.Cookie{Name: "gdrive_temp_config", Value: tempConfigPath})
// Send the request
router.ServeHTTP(w, req)
// We expect a redirect on successful auth
assert.Equal(t, http.StatusFound, w.Code)
// Should redirect to the config edit page
location := w.Header().Get("Location")
assert.Contains(t, location, "/configs/edit/1")
}
func TestHandleGDriveAuth_InvalidConfig(t *testing.T) {
// Setup
router, mockDB := setupTestRouter()
// Set up mock expectations for a non-existent config
mockDB.On("GetTransferConfig", uint(999)).Return(nil, fmt.Errorf("config not found"))
// Create test request
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/configs/gdrive/999", nil)
router.ServeHTTP(w, req)
// Assertions - should render error page
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Configuration not found")
}
func TestHandleGDriveAuth_NonGoogleConfig(t *testing.T) {
// Setup
router, mockDB := setupTestRouter()
// Create a non-Google test config
testConfig := &db.TransferConfig{
ID: 3,
DestinationType: "s3", // Not Google Drive or Photos
}
// Set up mock expectations
mockDB.On("GetTransferConfig", uint(3)).Return(testConfig, nil)
// Create test request
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/configs/gdrive/3", nil)
router.ServeHTTP(w, req)
// Assertions - should render error page
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Not a Google configuration")
}
// RenderErrorPageTest renders an error page with the given message
func RenderErrorPageTest(c *gin.Context, title string, details string) {
// Here we'd typically use a component for error display
// For now, we'll just render a simple HTML error page for testing
errorHTML := fmt.Sprintf("<html><body><h1>Error: %s</h1><p>%s</p></body></html>", title, details)
c.Data(http.StatusOK, "text/html", []byte(errorHTML))
}
-267
View File
@@ -1,267 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestJob is a struct for testing job imports
type TestJob struct {
Name string `json:"name"`
ConfigID uint `json:"config_id"`
ConfigIDs string `json:"config_ids"`
Schedule string `json:"schedule"`
Enabled bool `json:"enabled"`
CreatedBy uint `json:"created_by"`
}
// TestHandleImportJobsFixed tests the HandleImportJobs function
func TestHandleImportJobsFixed(t *testing.T) {
// Set up test environment
handlers, router := setupTestHandlers(t)
// Create a test user
testUser := &db.User{
ID: 1,
Email: "admin@example.com",
IsAdmin: BoolPtr(true),
}
// Set up middleware to add the user to the context
router.Use(func(c *gin.Context) {
c.Set("user", testUser)
c.Next()
})
// Create a test config first
config := &db.TransferConfig{
Name: "Test Config For Import Jobs",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: testUser.ID,
}
err := handlers.DB.DB.Create(config).Error
require.NoError(t, err)
configID := config.ID // Get the actual ID assigned by the database
t.Logf("Created config with ID: %d", configID)
// Verify the config exists
var foundConfig db.TransferConfig
err = handlers.DB.DB.First(&foundConfig, configID).Error
require.NoError(t, err, "Config should exist in database")
require.Equal(t, config.Name, foundConfig.Name, "Config name should match")
// Set up the route
router.POST("/admin/import/jobs", handlers.HandleImportJobs)
// Create test data with the correct config ID and config_ids
jobsData := fmt.Sprintf(`[
{
"name": "Imported Job",
"schedule": "0 */2 * * *",
"config_id": %d,
"config_ids": "%d",
"enabled": true,
"created_by": %d
}
]`, configID, configID, testUser.ID)
t.Logf("JSON payload: %s", jobsData)
// Create a test request
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/admin/import/jobs", strings.NewReader(jobsData))
req.Header.Set("Content-Type", "application/json")
// Test binding directly
var testJobs []TestJob
err = json.Unmarshal([]byte(jobsData), &testJobs)
require.NoError(t, err)
t.Logf("Unmarshaled job: ConfigID=%d, ConfigIDs=%s", testJobs[0].ConfigID, testJobs[0].ConfigIDs)
// Create a db.Job from the TestJob
dbJob := &db.Job{
Name: testJobs[0].Name,
ConfigID: testJobs[0].ConfigID,
ConfigIDs: testJobs[0].ConfigIDs,
Schedule: testJobs[0].Schedule,
Enabled: BoolPtr(testJobs[0].Enabled),
CreatedBy: testJobs[0].CreatedBy,
}
// Create the job directly in the database
err = handlers.DB.DB.Create(dbJob).Error
require.NoError(t, err)
t.Logf("Created job directly: ID=%d, ConfigID=%d, ConfigIDs=%s", dbJob.ID, dbJob.ConfigID, dbJob.ConfigIDs)
// Serve the request
router.ServeHTTP(w, req)
// Check response
t.Logf("Response body: %s", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify the success message
assert.Contains(t, response["message"], "jobs imported successfully")
// Verify the job was created
var count int64
err = handlers.DB.DB.Model(&db.Job{}).Where("name = ?", "Imported Job").Count(&count).Error
assert.NoError(t, err)
assert.Greater(t, count, int64(0), "Expected at least one job with the name 'Imported Job'")
}
// TestHandleImportJobsFromFileFixed tests the HandleImportJobsFromFile function
func TestHandleImportJobsFromFileFixed(t *testing.T) {
// Set up test environment
handlers, router := setupTestHandlers(t)
// Create a test user
testUser := &db.User{
ID: 1,
Email: "admin@example.com",
IsAdmin: BoolPtr(true),
}
// Set up middleware to add the user to the context - must be done BEFORE registering routes
router.Use(func(c *gin.Context) {
c.Set("user", testUser)
c.Next()
})
// Reset the database to ensure we're starting fresh
handlers.DB.DB.Exec("DELETE FROM jobs")
handlers.DB.DB.Exec("DELETE FROM transfer_configs")
// Create a test config
config := &db.TransferConfig{
Name: "Test Config For Import File",
SourceType: "local",
SourcePath: "/source",
DestinationType: "local",
DestinationPath: "/dest",
CreatedBy: testUser.ID,
}
// Create the config in the database
result := handlers.DB.DB.Create(config)
require.NoError(t, result.Error)
configID := config.ID // Get the actual ID assigned by the database
t.Logf("Created config with ID: %d", configID)
// Verify the config exists
var configCount int64
handlers.DB.DB.Model(&db.TransferConfig{}).Count(&configCount)
require.Equal(t, int64(1), configCount)
// Set up the route - AFTER middleware
router.POST("/admin/import/jobs/file", handlers.HandleImportJobsFromFile)
// Create test data with the correct config ID and config_ids
jobsData := fmt.Sprintf(`[
{
"name": "Imported Job From File",
"schedule": "0 */2 * * *",
"config_id": %d,
"config_ids": "%d",
"enabled": true,
"created_by": %d
}
]`, configID, configID, testUser.ID)
t.Logf("JSON payload: %s", jobsData)
// Create a multipart form buffer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add the file field
part, err := writer.CreateFormFile("jobs_file", "jobs.json")
require.NoError(t, err)
// Write the JSON data to the form file
_, err = part.Write([]byte(jobsData))
require.NoError(t, err)
// Close the writer
err = writer.Close()
require.NoError(t, err)
// Test binding directly
var testJobs []TestJob
err = json.Unmarshal([]byte(jobsData), &testJobs)
require.NoError(t, err)
t.Logf("Unmarshaled job: ConfigID=%d, ConfigIDs=%s", testJobs[0].ConfigID, testJobs[0].ConfigIDs)
// Create a db.Job from the TestJob
dbJob := &db.Job{
Name: testJobs[0].Name,
ConfigID: testJobs[0].ConfigID,
ConfigIDs: testJobs[0].ConfigIDs,
Schedule: testJobs[0].Schedule,
Enabled: BoolPtr(testJobs[0].Enabled),
CreatedBy: testJobs[0].CreatedBy,
}
// Create the job directly in the database
err = handlers.DB.DB.Create(dbJob).Error
require.NoError(t, err)
t.Logf("Created job directly: ID=%d, ConfigID=%d, ConfigIDs=%s", dbJob.ID, dbJob.ConfigID, dbJob.ConfigIDs)
// Create the request
req, err := http.NewRequest("POST", "/admin/import/jobs/file", body)
require.NoError(t, err)
// Set the content type
req.Header.Set("Content-Type", writer.FormDataContentType())
// Create recorder for the response
w := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(w, req)
// Check response
t.Logf("Response body: %s", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify the success message
assert.Contains(t, response["message"], "jobs imported successfully")
// Verify the job was created
var importedJobs []db.Job
err = handlers.DB.DB.Where("name = ?", "Imported Job From File").Find(&importedJobs).Error
assert.NoError(t, err)
assert.NotEmpty(t, importedJobs, "Expected at least one job with the name 'Imported Job From File'")
// Print all jobs for debugging
var allJobs []db.Job
handlers.DB.DB.Find(&allJobs)
t.Logf("Total jobs in database: %d", len(allJobs))
for i, job := range allJobs {
t.Logf("Job %d: ID=%d, Name='%s', ConfigID=%d", i+1, job.ID, job.Name, job.ConfigID)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,172 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
)
func setupProfileTest(t *testing.T) (*Handlers, *gin.Engine, *db.DB, *db.User) {
// Set up test database
database := testutils.SetupTestDB(t)
// Create test user
user := testutils.CreateTestUser(t, database, "test@example.com", false)
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers
handlers := &Handlers{
DB: database,
}
// Set up authentication middleware
router.Use(func(c *gin.Context) {
c.Set("userID", user.ID)
c.Next()
})
return handlers, router, database, user
}
func TestHandleProfile(t *testing.T) {
handlers, router, _, user := setupProfileTest(t)
// Set up route
router.GET("/profile", handlers.HandleProfile)
// Create request
req, _ := http.NewRequest("GET", "/profile", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Check profile content
assert.Contains(t, resp.Body.String(), user.Email)
// Test with non-existent user
invalidRouter := gin.New()
invalidRouter.Use(func(c *gin.Context) {
c.Set("userID", uint(9999)) // Non-existent user ID
c.Next()
})
invalidRouter.GET("/profile", handlers.HandleProfile)
req, _ = http.NewRequest("GET", "/profile", nil)
resp = httptest.NewRecorder()
invalidRouter.ServeHTTP(resp, req)
assert.Equal(t, http.StatusInternalServerError, resp.Code)
assert.Contains(t, resp.Body.String(), "Failed to retrieve user profile")
}
func TestHandleUpdateTheme(t *testing.T) {
handlers, router, database, user := setupProfileTest(t)
// Set up route
router.POST("/profile/theme", handlers.HandleUpdateTheme)
// Test cases
testCases := []struct {
name string
theme string
expectedCode int
}{
{
name: "Valid light theme",
theme: "light",
expectedCode: http.StatusOK,
},
{
name: "Valid dark theme",
theme: "dark",
expectedCode: http.StatusOK,
},
{
name: "Valid system theme",
theme: "system",
expectedCode: http.StatusOK,
},
{
name: "Invalid theme",
theme: "invalid",
expectedCode: http.StatusBadRequest,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Build form data
formData := url.Values{
"theme": {tc.theme},
}
// Create request
req, _ := http.NewRequest("POST", "/profile/theme", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
// If valid theme, check that user's theme was updated
if tc.expectedCode == http.StatusOK {
// Fetch the user from the database
var updatedUser db.User
err := database.First(&updatedUser, user.ID).Error
assert.NoError(t, err)
// Check that theme was updated
assert.Equal(t, tc.theme, updatedUser.Theme)
// Check that theme cookie was set
cookies := resp.Result().Cookies()
var themeCookie *http.Cookie
for _, cookie := range cookies {
if cookie.Name == "theme" {
themeCookie = cookie
break
}
}
assert.NotNil(t, themeCookie)
assert.Equal(t, tc.theme, themeCookie.Value)
assert.Equal(t, 60*60*24*365, themeCookie.MaxAge) // 1 year
}
})
}
// Test with non-existent user
invalidRouter := gin.New()
invalidRouter.Use(func(c *gin.Context) {
c.Set("userID", uint(9999)) // Non-existent user ID
c.Next()
})
invalidRouter.POST("/profile/theme", handlers.HandleUpdateTheme)
formData := url.Values{
"theme": {"light"},
}
req, _ := http.NewRequest("POST", "/profile/theme", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
invalidRouter.ServeHTTP(resp, req)
assert.Equal(t, http.StatusInternalServerError, resp.Code)
}
-337
View File
@@ -1,337 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/starfleetcptn/gomft/internal/testutils"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
func setupUserTest(t *testing.T) (*Handlers, *gin.Engine, *db.DB, uint) {
// Set up test database
database := testutils.SetupTestDB(t)
// Create admin user
admin := testutils.CreateTestUser(t, database, "admin@example.com", true)
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers with JWT configuration
config := testutils.SetupTestConfig(t)
handlers := &Handlers{
DB: database,
JWTSecret: config.JWTSecret,
}
// Set up authentication middleware for admins
router.Use(func(c *gin.Context) {
c.Set("userID", admin.ID)
c.Set("isAdmin", true)
c.Next()
})
return handlers, router, database, admin.ID
}
func TestHandleUsers(t *testing.T) {
handlers, router, database, _ := setupUserTest(t)
// Create additional test users
testutils.CreateTestUser(t, database, "user1@example.com", false)
testutils.CreateTestUser(t, database, "user2@example.com", false)
// Set up route
router.GET("/admin/users", handlers.HandleUsers)
// Create request
req, _ := http.NewRequest("GET", "/admin/users", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
// Should contain the admin and two test users
assert.Contains(t, resp.Body.String(), "admin@example.com")
assert.Contains(t, resp.Body.String(), "user1@example.com")
assert.Contains(t, resp.Body.String(), "user2@example.com")
}
func TestHandleNewUser(t *testing.T) {
handlers, router, _, _ := setupUserTest(t)
// Set up route
router.GET("/admin/users/new", handlers.HandleNewUser)
// Create request
req, _ := http.NewRequest("GET", "/admin/users/new", nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "New User")
assert.Contains(t, resp.Body.String(), "Email")
assert.Contains(t, resp.Body.String(), "Password")
assert.Contains(t, resp.Body.String(), "Admin")
}
func TestHandleCreateUser(t *testing.T) {
handlers, router, database, _ := setupUserTest(t)
// Set up route
router.POST("/admin/users/new", handlers.HandleCreateUser)
// Test cases
testCases := []struct {
name string
formData url.Values
expectedCode int
checkUser bool
}{
{
name: "Valid user creation",
formData: url.Values{
"email": {"newuser@example.com"},
"password": {"testpassword"},
"is_admin": {"on"},
},
expectedCode: http.StatusSeeOther,
checkUser: true,
},
{
name: "Duplicate email",
formData: url.Values{
"email": {"admin@example.com"}, // Already exists
"password": {"testpassword"},
},
expectedCode: http.StatusBadRequest,
checkUser: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create request with form data
req, _ := http.NewRequest("POST", "/admin/users/new", strings.NewReader(tc.formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
// If we expect user creation, verify the user exists in the database
if tc.checkUser {
var user db.User
err := database.Where("email = ?", tc.formData.Get("email")).First(&user).Error
assert.NoError(t, err)
assert.Equal(t, tc.formData.Get("email"), user.Email)
assert.Equal(t, tc.formData.Get("is_admin") == "on", user.IsAdmin)
}
})
}
}
func TestHandleDeleteUser(t *testing.T) {
handlers, router, database, adminID := setupUserTest(t)
// Create a user to delete
userToDelete := testutils.CreateTestUser(t, database, "delete-me@example.com", false)
// Set up route
router.POST("/admin/users/delete/:id", handlers.HandleDeleteUser)
// Test cases
testCases := []struct {
name string
userID uint
expectedCode int
expectedBody string
}{
{
name: "Delete valid user",
userID: userToDelete.ID,
expectedCode: http.StatusSeeOther,
expectedBody: "",
},
{
name: "Cannot delete own account",
userID: adminID,
expectedCode: http.StatusBadRequest,
expectedBody: "Cannot delete your own account",
},
{
name: "Invalid user ID",
userID: 9999,
expectedCode: http.StatusSeeOther,
expectedBody: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create request
req, _ := http.NewRequest("POST", "/admin/users/delete/"+strconv.Itoa(int(tc.userID)), nil)
resp := httptest.NewRecorder()
// Serve request
router.ServeHTTP(resp, req)
// Check response code
assert.Equal(t, tc.expectedCode, resp.Code)
// If we expect a specific body message, check it
if tc.expectedBody != "" {
assert.Contains(t, resp.Body.String(), tc.expectedBody)
}
// Verify database state after the action
if tc.name == "Delete valid user" {
// For the valid deletion case, verify user was deleted
var deletedUser db.User
// User should not be found with normal query after deletion
err := database.Where("id = ?", tc.userID).First(&deletedUser).Error
assert.Equal(t, gorm.ErrRecordNotFound, err, "User should be deleted and not found")
} else if tc.name == "Cannot delete own account" {
// For cannot delete own account, verify user still exists
var adminUser db.User
err := database.Where("id = ?", tc.userID).First(&adminUser).Error
assert.NoError(t, err, "Admin user should still exist")
} else if tc.name == "Invalid user ID" {
// For invalid user ID, just verify it doesn't exist
var nonExistentUser db.User
err := database.Where("id = ?", tc.userID).First(&nonExistentUser).Error
assert.Equal(t, gorm.ErrRecordNotFound, err, "Non-existent user should not be found")
}
})
}
}
func TestHandleRegisterPage(t *testing.T) {
// Set up clean database with no users
database := testutils.SetupTestDB(t)
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers
handlers := &Handlers{
DB: database,
}
// Set up route
router.GET("/register", handlers.HandleRegisterPage)
// Test with no existing users - should show registration page
req, _ := http.NewRequest("GET", "/register", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "Register")
// Create a user
testutils.CreateTestUser(t, database, "existing@example.com", true)
// Test with existing user - should redirect
req, _ = http.NewRequest("GET", "/register", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/", resp.Header().Get("Location"))
}
func TestHandleRegister(t *testing.T) {
// Set up clean database with no users
database := testutils.SetupTestDB(t)
// Set up Gin router
gin.SetMode(gin.TestMode)
router := gin.New()
// Create handlers with JWT configuration
config := testutils.SetupTestConfig(t)
handlers := &Handlers{
DB: database,
JWTSecret: config.JWTSecret,
}
// Set up route
router.POST("/register", handlers.HandleRegister)
// Test user registration
formData := url.Values{
"email": {"firstuser@example.com"},
"password": {"testpassword"},
}
req, _ := http.NewRequest("POST", "/register", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to dashboard
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/dashboard", resp.Header().Get("Location"))
// Verify user was created as admin
var user db.User
err := database.Where("email = ?", formData.Get("email")).First(&user).Error
assert.NoError(t, err)
assert.Equal(t, formData.Get("email"), user.Email)
assert.True(t, user.GetIsAdmin())
// Verify JWT cookie was set
cookies := resp.Result().Cookies()
assert.GreaterOrEqual(t, len(cookies), 1)
var jwtCookie *http.Cookie
for _, cookie := range cookies {
if cookie.Name == "jwt" {
jwtCookie = cookie
break
}
}
assert.NotNil(t, jwtCookie)
assert.NotEmpty(t, jwtCookie.Value)
// Try registering a second user - should be redirected
formData = url.Values{
"email": {"seconduser@example.com"},
"password": {"testpassword"},
}
req, _ = http.NewRequest("POST", "/register", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect to home
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/", resp.Header().Get("Location"))
// Second user should not exist
var count int64
database.Model(&db.User{}).Where("email = ?", formData.Get("email")).Count(&count)
assert.Equal(t, int64(0), count)
}
-247
View File
@@ -1,247 +0,0 @@
package handlers
import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/starfleetcptn/gomft/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestWebhookConfiguration tests the webhook configuration during job creation and editing
func TestWebhookConfiguration(t *testing.T) {
// Set up test environment
handlers, router, database, user, config := setupJobsTest(t)
// Add job create route
router.POST("/jobs/create", handlers.HandleCreateJob)
// Create job form data with webhook enabled
formData := url.Values{
"name": {"Webhook Test Job"},
"config_ids[]": {strconv.Itoa(int(config.ID))},
"schedule": {"*/15 * * * *"},
"enabled": {"true"},
"webhook_enabled": {"true"},
"webhook_url": {"https://example.com/webhook"},
"webhook_secret": {"test-secret"},
"webhook_headers": {`{"X-Test-Header": "test-value"}`},
"notify_on_success": {"true"},
"notify_on_failure": {"true"},
}
// Submit form
req, _ := http.NewRequest("POST", "/jobs/create", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect on success
assert.Equal(t, http.StatusFound, resp.Code)
// Check if job was created with webhook settings
var jobs []db.Job
err := database.DB.Where("created_by = ?", user.ID).Find(&jobs).Error
require.NoError(t, err)
require.GreaterOrEqual(t, len(jobs), 1)
// Get the most recently created job
var job db.Job
err = database.DB.Where("created_by = ?", user.ID).Order("created_at DESC").First(&job).Error
require.NoError(t, err)
// Verify webhook settings were saved correctly
assert.True(t, job.GetWebhookEnabled())
assert.Equal(t, "https://example.com/webhook", job.WebhookURL)
assert.Equal(t, "test-secret", job.WebhookSecret)
assert.Equal(t, `{"X-Test-Header": "test-value"}`, job.WebhookHeaders)
assert.True(t, job.GetNotifyOnSuccess())
assert.True(t, job.GetNotifyOnFailure())
}
// TestWebhookEditConfiguration tests editing webhook configuration
func TestWebhookEditConfiguration(t *testing.T) {
// Set up test environment
handlers, router, database, user, config := setupJobsTest(t)
// Create a job first
job := &db.Job{
Name: "Initial Job",
ConfigID: config.ID,
Schedule: "*/30 * * * *",
Enabled: BoolPtr(true),
WebhookEnabled: BoolPtr(false), // Initially disabled
CreatedBy: user.ID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := database.DB.Create(job).Error
require.NoError(t, err)
// Add job update route
router.PUT("/jobs/:id", handlers.HandleUpdateJob)
// Create edit form data to enable webhook
formData := url.Values{
"name": {"Updated Job"},
"config_ids[]": {strconv.Itoa(int(config.ID))},
"schedule": {"*/30 * * * *"},
"enabled": {"true"},
"webhook_enabled": {"true"}, // Enabling webhook
"webhook_url": {"https://example.com/webhook"}, // Adding URL
"webhook_secret": {"new-secret"}, // Adding secret
"webhook_headers": {`{"X-Api-Key": "12345"}`}, // Adding headers
"notify_on_success": {"true"}, // Configure notifications
"notify_on_failure": {"false"}, // Only notify on success
}
// Submit edit form
req, _ := http.NewRequest("PUT", "/jobs/"+strconv.Itoa(int(job.ID)), strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect on success
assert.Equal(t, http.StatusFound, resp.Code)
// Get the updated job
var updatedJob db.Job
err = database.DB.First(&updatedJob, job.ID).Error
require.NoError(t, err)
// Verify webhook settings were updated correctly
assert.True(t, updatedJob.GetWebhookEnabled())
assert.Equal(t, "https://example.com/webhook", updatedJob.WebhookURL)
assert.Equal(t, "new-secret", updatedJob.WebhookSecret)
assert.Equal(t, `{"X-Api-Key": "12345"}`, updatedJob.WebhookHeaders)
assert.True(t, updatedJob.GetNotifyOnSuccess())
assert.False(t, updatedJob.GetNotifyOnFailure())
}
// TestDisablingWebhook tests disabling a previously enabled webhook
func TestDisablingWebhook(t *testing.T) {
// Set up test environment
handlers, router, database, user, config := setupJobsTest(t)
// Create a job with webhook enabled
job := &db.Job{
Name: "Webhook Enabled Job",
ConfigID: config.ID,
Schedule: "*/30 * * * *",
Enabled: BoolPtr(true),
WebhookEnabled: BoolPtr(true),
WebhookURL: "https://example.com/webhook",
WebhookSecret: "secret",
WebhookHeaders: `{"X-Test": "test"}`,
NotifyOnSuccess: BoolPtr(true),
NotifyOnFailure: BoolPtr(true),
CreatedBy: user.ID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := database.DB.Create(job).Error
require.NoError(t, err)
// Add job update route
router.PUT("/jobs/:id", handlers.HandleUpdateJob)
// Create edit form data to disable webhook
formData := url.Values{
"name": {"Webhook Disabled Job"},
"config_ids[]": {strconv.Itoa(int(config.ID))},
"schedule": {"*/30 * * * *"},
"enabled": {"true"},
"webhook_enabled": {"false"}, // Explicitly set to false
"webhook_url": {"https://example.com/webhook"}, // URL remains the same
"webhook_secret": {"secret"}, // Secret remains the same
"webhook_headers": {`{"X-Test": "test"}`}, // Headers remain the same
}
// Submit edit form
req, _ := http.NewRequest("PUT", "/jobs/"+strconv.Itoa(int(job.ID)), strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should redirect on success
assert.Equal(t, http.StatusFound, resp.Code)
// Get the updated job
var updatedJob db.Job
err = database.DB.First(&updatedJob, job.ID).Error
require.NoError(t, err)
// Verify webhook was disabled
assert.False(t, updatedJob.GetWebhookEnabled())
// Other fields should remain unchanged
assert.Equal(t, "https://example.com/webhook", updatedJob.WebhookURL)
assert.Equal(t, "secret", updatedJob.WebhookSecret)
assert.Equal(t, `{"X-Test": "test"}`, updatedJob.WebhookHeaders)
}
// TestWebhookValidation tests validation of webhook URL
func TestWebhookValidation(t *testing.T) {
// Set up test environment
handlers, router, _, _, config := setupJobsTest(t)
// Add job create route
router.POST("/jobs/create", handlers.HandleCreateJob)
// Create job form data with invalid webhook URL
formData := url.Values{
"name": {"Invalid Webhook Job"},
"config_ids[]": {strconv.Itoa(int(config.ID))},
"schedule": {"*/15 * * * *"},
"enabled": {"true"},
"webhook_enabled": {"true"},
"webhook_url": {"invalid-url"}, // Invalid URL
"webhook_secret": {"test-secret"},
"notify_on_success": {"true"},
"notify_on_failure": {"true"},
}
// Submit form
req, _ := http.NewRequest("POST", "/jobs/create", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should not create job with invalid webhook URL
assert.NotEqual(t, http.StatusFound, resp.Code)
assert.Contains(t, resp.Body.String(), "valid URL")
// Test invalid headers JSON
formData = url.Values{
"name": {"Invalid Headers Job"},
"config_ids[]": {strconv.Itoa(int(config.ID))},
"schedule": {"*/15 * * * *"},
"enabled": {"true"},
"webhook_enabled": {"true"},
"webhook_url": {"https://example.com/webhook"},
"webhook_secret": {"test-secret"},
"webhook_headers": {`{"invalid json`}, // Invalid JSON
"notify_on_success": {"true"},
"notify_on_failure": {"true"},
}
// Submit form
req, _ = http.NewRequest("POST", "/jobs/create", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
// Should not create job with invalid headers JSON
assert.NotEqual(t, http.StatusFound, resp.Code)
assert.Contains(t, resp.Body.String(), "valid JSON")
}
func BoolPtr(b bool) *bool {
return &b
}