mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-13 17:48:15 +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 parent
8424e97cf8
commit
a7cd021ff2
27 files changed
-11348
No files matched your search
@@ -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