mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
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:
@@ -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 === 'password'"`)
|
||||
assert.Contains(html, `<input type="password" name="source_password"`)
|
||||
|
||||
// Should have key file field that's conditionally shown
|
||||
assert.Contains(html, `x-show="sourceAuthType === 'key_file'"`)
|
||||
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"`)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user