mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
715 lines
21 KiB
Go
715 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/starfleetcptn/gomft/internal/db"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Define a DBInterface that has just the methods we need for these tests
|
|
type DBInterface interface {
|
|
GetStorageProviders(userID uint) ([]*db.StorageProvider, error)
|
|
GetStorageProvider(id uint) (*db.StorageProvider, error)
|
|
GetStorageProviderWithOwnerCheck(id, userID uint) (*db.StorageProvider, error)
|
|
CreateStorageProvider(provider *db.StorageProvider) error
|
|
UpdateStorageProvider(provider *db.StorageProvider) error
|
|
DeleteStorageProvider(id uint) error
|
|
}
|
|
|
|
// MockDB implements the necessary DB methods for testing
|
|
type MockDB struct {
|
|
mock.Mock
|
|
*gorm.DB
|
|
}
|
|
|
|
func (m *MockDB) GetStorageProviders(userID uint) ([]*db.StorageProvider, error) {
|
|
args := m.Called(userID)
|
|
return args.Get(0).([]*db.StorageProvider), args.Error(1)
|
|
}
|
|
|
|
func (m *MockDB) GetStorageProvider(id uint) (*db.StorageProvider, error) {
|
|
args := m.Called(id)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*db.StorageProvider), args.Error(1)
|
|
}
|
|
|
|
func (m *MockDB) GetStorageProviderWithOwnerCheck(id, userID uint) (*db.StorageProvider, error) {
|
|
args := m.Called(id, userID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*db.StorageProvider), args.Error(1)
|
|
}
|
|
|
|
func (m *MockDB) CreateStorageProvider(provider *db.StorageProvider) error {
|
|
args := m.Called(provider)
|
|
// Set ID to simulate DB auto-increment
|
|
provider.ID = 1
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockDB) UpdateStorageProvider(provider *db.StorageProvider) error {
|
|
args := m.Called(provider)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockDB) DeleteStorageProvider(id uint) error {
|
|
args := m.Called(id)
|
|
return args.Error(0)
|
|
}
|
|
|
|
// Use a wrapper struct for the handlers tests
|
|
type TestHandlers struct {
|
|
DB DBInterface
|
|
}
|
|
|
|
// Create a new test handlers instance with our mock DB
|
|
func NewTestHandlers(mockDB DBInterface) *TestHandlers {
|
|
return &TestHandlers{
|
|
DB: mockDB,
|
|
}
|
|
}
|
|
|
|
func setupHandlerTest() (*gin.Engine, *MockDB, *httptest.ResponseRecorder) {
|
|
gin.SetMode(gin.TestMode)
|
|
mockDB := new(MockDB)
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
|
|
// Skip template loading for tests
|
|
// r.LoadHTMLGlob("test_templates/*")
|
|
|
|
w := httptest.NewRecorder()
|
|
return r, mockDB, w
|
|
}
|
|
|
|
// Helper to set user ID in context for protected endpoints
|
|
func setUserContext(c *gin.Context) {
|
|
c.Set("userID", uint(1))
|
|
c.Set("email", "test@example.com")
|
|
}
|
|
|
|
func TestHandleListStorageProviders(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
testHandlers := NewTestHandlers(mockDB)
|
|
_ = testHandlers // Use variable to avoid unused warning
|
|
|
|
providers := []*db.StorageProvider{
|
|
{
|
|
ID: 1,
|
|
Name: "Test S3",
|
|
Type: db.ProviderTypeS3,
|
|
AccessKey: "test-access-key",
|
|
CreatedBy: 1,
|
|
},
|
|
{
|
|
ID: 2,
|
|
Name: "Test SFTP",
|
|
Type: db.ProviderTypeSFTP,
|
|
Username: "testuser",
|
|
CreatedBy: 1,
|
|
},
|
|
}
|
|
|
|
mockDB.On("GetStorageProviders", uint(1)).Return(providers, nil)
|
|
|
|
// For testing, simply skip actual template rendering and check status code
|
|
// since we don't have actual template files in test environment
|
|
r.GET("/storage-providers", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
// Actually call the mocked method
|
|
providers, err := mockDB.GetStorageProviders(c.GetUint("userID"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch storage providers"})
|
|
return
|
|
}
|
|
|
|
// Check we got the expected results
|
|
if len(providers) != 2 || providers[0].Name != "Test S3" || providers[1].Name != "Test SFTP" {
|
|
c.String(http.StatusInternalServerError, "Unexpected provider data")
|
|
return
|
|
}
|
|
|
|
// Mock success response instead of actual template rendering
|
|
c.String(http.StatusOK, "Mock response containing Test S3 and Test SFTP")
|
|
})
|
|
|
|
// Make the request
|
|
req, _ := http.NewRequest("GET", "/storage-providers", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Check results
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
// Since we're mocking the response, just check for the expected content
|
|
assert.Contains(t, w.Body.String(), "Test S3")
|
|
assert.Contains(t, w.Body.String(), "Test SFTP")
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHandleNewStorageProvider(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
testHandlers := NewTestHandlers(mockDB)
|
|
_ = testHandlers // Use variable to avoid unused warning
|
|
|
|
// For testing, simply skip actual template rendering and check status code
|
|
r.GET("/storage-providers/new", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
// Mock success response instead of actual template rendering
|
|
c.String(http.StatusOK, "Mock form containing New Storage Provider")
|
|
})
|
|
|
|
// Make the request
|
|
req, _ := http.NewRequest("GET", "/storage-providers/new", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Check results - we're just checking the status and mock content
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "New Storage Provider")
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
func TestHandleCreateStorageProvider(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
testHandlers := NewTestHandlers(mockDB)
|
|
_ = testHandlers // Use variable to avoid unused warning
|
|
|
|
// Set up mock expectations
|
|
mockDB.On("CreateStorageProvider", mock.AnythingOfType("*db.StorageProvider")).Return(nil)
|
|
|
|
// Replace actual handler with test mock
|
|
r.POST("/storage-providers", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
// Parse form
|
|
if err := c.Request.ParseForm(); err != nil {
|
|
c.String(http.StatusBadRequest, "Error parsing form")
|
|
return
|
|
}
|
|
|
|
// Create a new provider from form data
|
|
provider := &db.StorageProvider{
|
|
Name: c.PostForm("name"),
|
|
Type: db.StorageProviderType(c.PostForm("type")),
|
|
AccessKey: c.PostForm("access_key"),
|
|
SecretKey: c.PostForm("secret_key"),
|
|
Region: c.PostForm("region"),
|
|
Bucket: c.PostForm("bucket"),
|
|
CreatedBy: c.GetUint("userID"),
|
|
}
|
|
|
|
// Save it
|
|
err := mockDB.CreateStorageProvider(provider)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, "Failed to create provider")
|
|
return
|
|
}
|
|
|
|
// Redirect on success
|
|
c.Redirect(http.StatusFound, "/storage-providers?status=created")
|
|
})
|
|
|
|
// Create form data
|
|
form := url.Values{}
|
|
form.Add("name", "Test S3 Provider")
|
|
form.Add("type", string(db.ProviderTypeS3))
|
|
form.Add("access_key", "test-access-key")
|
|
form.Add("secret_key", "test-secret-key")
|
|
form.Add("region", "us-west-1")
|
|
form.Add("bucket", "test-bucket")
|
|
|
|
// Make the request
|
|
req, _ := http.NewRequest("POST", "/storage-providers", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Check results - should redirect on success
|
|
assert.Equal(t, http.StatusFound, w.Code)
|
|
// Check for redirect to list page with status
|
|
redirectURL, err := w.Result().Location()
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "/storage-providers?status=created", redirectURL.String())
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
// ---- SECURITY TESTS ----
|
|
|
|
// TestUnauthorizedAccess tests that handlers require authentication
|
|
func TestUnauthorizedAccess(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
_ = mockDB // Use variable to avoid unused warning
|
|
|
|
// Define routes without setting userContext
|
|
r.GET("/storage-providers", func(c *gin.Context) {
|
|
// No setUserContext() call - simulate missing authentication
|
|
if _, exists := c.Get("userID"); !exists {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
c.String(http.StatusOK, "Authenticated response")
|
|
})
|
|
|
|
// Test GET request
|
|
req, _ := http.NewRequest("GET", "/storage-providers", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should return unauthorized
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
// Test another route
|
|
w = httptest.NewRecorder()
|
|
r.POST("/storage-providers", func(c *gin.Context) {
|
|
// No setUserContext() call - simulate missing authentication
|
|
if _, exists := c.Get("userID"); !exists {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
c.String(http.StatusOK, "Authenticated response")
|
|
})
|
|
|
|
req, _ = http.NewRequest("POST", "/storage-providers", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should return unauthorized
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
// TestCrossSiteRequestForgery tests CSRF protection
|
|
func TestCrossSiteRequestForgery(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
_ = mockDB // Use variable to avoid unused warning
|
|
|
|
// Add CSRF check middleware
|
|
r.Use(func(c *gin.Context) {
|
|
// For this test, we simulate a CSRF check that validates a token
|
|
// In a real app, this would be a more complex check
|
|
if c.Request.Method != "GET" && c.GetHeader("X-CSRF-Token") != "valid-token" {
|
|
c.AbortWithStatus(http.StatusForbidden)
|
|
return
|
|
}
|
|
c.Next()
|
|
})
|
|
|
|
// Set up a POST route with CSRF protection
|
|
r.POST("/storage-providers", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
c.String(http.StatusOK, "Success")
|
|
})
|
|
|
|
// Test without CSRF token
|
|
form := url.Values{}
|
|
form.Add("name", "CSRF Test Provider")
|
|
form.Add("type", "s3")
|
|
|
|
req, _ := http.NewRequest("POST", "/storage-providers", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should be forbidden due to missing CSRF token
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
|
|
// Test with valid CSRF token
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("POST", "/storage-providers", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("X-CSRF-Token", "valid-token")
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should succeed with valid token
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// TestCredentialStorage tests that credentials are not returned in responses
|
|
func TestCredentialStorage(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
// Create a provider with sensitive fields
|
|
provider := &db.StorageProvider{
|
|
ID: 1,
|
|
Name: "Security Test Provider",
|
|
Type: db.ProviderTypeS3,
|
|
AccessKey: "test-access-key",
|
|
// This should be encrypted in the DB
|
|
EncryptedSecretKey: "ENC:encrypted-secret-key",
|
|
CreatedBy: 1,
|
|
}
|
|
|
|
// Mock DB to return our provider
|
|
mockDB.On("GetStorageProviderWithOwnerCheck", uint(1), uint(1)).Return(provider, nil)
|
|
|
|
// Add a route to get provider details
|
|
r.GET("/storage-providers/:id", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
provider, err := mockDB.GetStorageProviderWithOwnerCheck(uint(id), c.GetUint("userID"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
|
|
return
|
|
}
|
|
|
|
// Return provider as JSON
|
|
c.JSON(http.StatusOK, provider)
|
|
})
|
|
|
|
// Make the request
|
|
req, _ := http.NewRequest("GET", "/storage-providers/1", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Check response status
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Check that response doesn't contain sensitive fields
|
|
responseBody := w.Body.String()
|
|
assert.NotContains(t, responseBody, "SecretKey")
|
|
assert.NotContains(t, responseBody, "Password")
|
|
assert.NotContains(t, responseBody, "ClientSecret")
|
|
assert.NotContains(t, responseBody, "RefreshToken")
|
|
|
|
// The encrypted values should also not be included in JSON response
|
|
assert.NotContains(t, responseBody, "EncryptedSecretKey")
|
|
assert.NotContains(t, responseBody, "EncryptedPassword")
|
|
assert.NotContains(t, responseBody, "EncryptedClientSecret")
|
|
assert.NotContains(t, responseBody, "EncryptedRefreshToken")
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
// TestInputValidation tests validation of user input
|
|
func TestInputValidation(t *testing.T) {
|
|
r, mockDB, _ := setupHandlerTest() // Changed w to _ since it's not used
|
|
_ = mockDB // Use variable to avoid unused warning
|
|
|
|
// Add a route with input validation for creating a storage provider
|
|
r.POST("/storage-providers", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
// Validate required fields
|
|
name := c.PostForm("name")
|
|
providerType := c.PostForm("type")
|
|
|
|
if name == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Name is required"})
|
|
return
|
|
}
|
|
|
|
if providerType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Type is required"})
|
|
return
|
|
}
|
|
|
|
// Validate type is one of the allowed values
|
|
validTypes := map[string]bool{
|
|
"s3": true,
|
|
"sftp": true,
|
|
"ftp": true,
|
|
"smb": true,
|
|
"onedrive": true,
|
|
"gdrive": true,
|
|
"gphotos": true,
|
|
"hetzner": true,
|
|
"local": true,
|
|
}
|
|
|
|
if !validTypes[providerType] {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider type"})
|
|
return
|
|
}
|
|
|
|
// Test XSS protection by checking for HTML in name
|
|
if strings.Contains(name, "<script>") || strings.Contains(name, "<") {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid characters in name"})
|
|
return
|
|
}
|
|
|
|
// Validate S3-specific fields
|
|
if providerType == "s3" {
|
|
bucket := c.PostForm("bucket")
|
|
if bucket == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket is required for S3 providers"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.String(http.StatusOK, "Validation passed")
|
|
})
|
|
|
|
testCases := []struct {
|
|
name string
|
|
formValues url.Values
|
|
expectedCode int
|
|
expectedBody string
|
|
}{
|
|
{
|
|
name: "Missing name",
|
|
formValues: url.Values{"type": {"s3"}, "bucket": {"test-bucket"}},
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedBody: "Name is required",
|
|
},
|
|
{
|
|
name: "Missing type",
|
|
formValues: url.Values{"name": {"Test Provider"}, "bucket": {"test-bucket"}},
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedBody: "Type is required",
|
|
},
|
|
{
|
|
name: "Invalid type",
|
|
formValues: url.Values{"name": {"Test Provider"}, "type": {"invalid-type"}},
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedBody: "Invalid provider type",
|
|
},
|
|
{
|
|
name: "XSS attempt",
|
|
formValues: url.Values{"name": {"<script>alert('xss')</script>"}, "type": {"s3"}, "bucket": {"test-bucket"}},
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedBody: "Invalid characters in name",
|
|
},
|
|
{
|
|
name: "Missing S3 bucket",
|
|
formValues: url.Values{"name": {"Test S3"}, "type": {"s3"}},
|
|
expectedCode: http.StatusBadRequest,
|
|
expectedBody: "Bucket is required for S3 providers",
|
|
},
|
|
{
|
|
name: "Valid input",
|
|
formValues: url.Values{"name": {"Test S3"}, "type": {"s3"}, "bucket": {"test-bucket"}},
|
|
expectedCode: http.StatusOK,
|
|
expectedBody: "Validation passed",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
req, _ := http.NewRequest("POST", "/storage-providers", strings.NewReader(tc.formValues.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.expectedCode, w.Code)
|
|
assert.Contains(t, w.Body.String(), tc.expectedBody)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAccessControl tests authorization for storage provider access
|
|
func TestAccessControl(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
// Create two providers with different owners
|
|
userProvider := &db.StorageProvider{
|
|
ID: 1,
|
|
Name: "User's Provider",
|
|
Type: db.ProviderTypeS3,
|
|
CreatedBy: 1,
|
|
}
|
|
|
|
otherUserProvider := &db.StorageProvider{
|
|
ID: 2,
|
|
Name: "Other User's Provider",
|
|
Type: db.ProviderTypeS3,
|
|
CreatedBy: 2,
|
|
}
|
|
|
|
// Mock DB to handle owner checks
|
|
mockDB.On("GetStorageProviderWithOwnerCheck", uint(1), uint(1)).Return(userProvider, nil)
|
|
mockDB.On("GetStorageProviderWithOwnerCheck", uint(2), uint(1)).Return(nil, fmt.Errorf("provider not found"))
|
|
|
|
// Add a route to access a provider
|
|
r.GET("/storage-providers/:id/edit", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
|
|
// Try to get provider with owner check
|
|
provider, err := mockDB.GetStorageProviderWithOwnerCheck(uint(id), c.GetUint("userID"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"name": provider.Name})
|
|
})
|
|
|
|
// Test access to user's own provider
|
|
req, _ := http.NewRequest("GET", "/storage-providers/1/edit", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should succeed
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "User's Provider")
|
|
|
|
// Test access to another user's provider
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/storage-providers/2/edit", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should fail with not found
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
// Verify the otherUserProvider exists (to avoid unused variable warning)
|
|
assert.Equal(t, uint(2), otherUserProvider.ID)
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
// TestSensitiveOperationProtection tests protection for sensitive operations
|
|
func TestSensitiveOperationProtection(t *testing.T) {
|
|
r, mockDB, w := setupHandlerTest()
|
|
|
|
// Mock provider for deletion tests
|
|
provider := &db.StorageProvider{
|
|
ID: 1,
|
|
Name: "Test Provider",
|
|
Type: db.ProviderTypeS3,
|
|
CreatedBy: 1,
|
|
}
|
|
|
|
mockDB.On("GetStorageProviderWithOwnerCheck", uint(1), uint(1)).Return(provider, nil)
|
|
mockDB.On("DeleteStorageProvider", uint(1)).Return(nil)
|
|
|
|
// Add a route with confirmation requirement for deletion
|
|
r.POST("/storage-providers/:id/delete", func(c *gin.Context) {
|
|
setUserContext(c)
|
|
|
|
// Get ID from path
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
|
|
// First check ownership
|
|
_, err := mockDB.GetStorageProviderWithOwnerCheck(uint(id), c.GetUint("userID"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"})
|
|
return
|
|
}
|
|
|
|
// Check for confirmation
|
|
confirmed := c.PostForm("confirm")
|
|
if confirmed != "true" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Confirmation required to delete"})
|
|
return
|
|
}
|
|
|
|
// Delete the provider
|
|
err = mockDB.DeleteStorageProvider(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"})
|
|
return
|
|
}
|
|
|
|
c.Redirect(http.StatusFound, "/storage-providers?status=deleted")
|
|
})
|
|
|
|
// Test without confirmation
|
|
form := url.Values{}
|
|
req, _ := http.NewRequest("POST", "/storage-providers/1/delete", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should require confirmation
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Confirmation required")
|
|
|
|
// Test with confirmation
|
|
w = httptest.NewRecorder()
|
|
form = url.Values{"confirm": {"true"}}
|
|
req, _ = http.NewRequest("POST", "/storage-providers/1/delete", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should redirect after successful deletion
|
|
assert.Equal(t, http.StatusFound, w.Code)
|
|
redirectURL, _ := w.Result().Location()
|
|
assert.Equal(t, "/storage-providers?status=deleted", redirectURL.String())
|
|
|
|
mockDB.AssertExpectations(t)
|
|
}
|
|
|
|
// TestBruteForceProtection tests for rate limiting and brute force protection
|
|
func TestBruteForceProtection(t *testing.T) {
|
|
r, mockDB, _ := setupHandlerTest()
|
|
_ = mockDB // Use variable to avoid unused warning
|
|
|
|
// Create a simple rate limiter for testing
|
|
// In a real app, this would be more sophisticated
|
|
failedAttempts := make(map[string]int)
|
|
|
|
r.POST("/test-login", func(c *gin.Context) {
|
|
ipAddress := c.ClientIP()
|
|
|
|
// Check if IP is already blocked
|
|
if failedAttempts[ipAddress] >= 3 {
|
|
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many failed attempts"})
|
|
return
|
|
}
|
|
|
|
// Check credentials (simulated)
|
|
username := c.PostForm("username")
|
|
password := c.PostForm("password")
|
|
|
|
if username == "admin" && password == "correct-password" {
|
|
// Reset counter on success
|
|
failedAttempts[ipAddress] = 0
|
|
c.JSON(http.StatusOK, gin.H{"status": "logged in"})
|
|
return
|
|
}
|
|
|
|
// Increment failed counter
|
|
failedAttempts[ipAddress]++
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
|
})
|
|
|
|
// First attempt with wrong password
|
|
w := httptest.NewRecorder()
|
|
form := url.Values{"username": {"admin"}, "password": {"wrong-password"}}
|
|
req, _ := http.NewRequest("POST", "/test-login", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
// Second attempt with wrong password
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
// Third attempt with wrong password
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
// Fourth attempt should be blocked
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusTooManyRequests, w.Code)
|
|
|
|
// Right password should also be blocked now
|
|
w = httptest.NewRecorder()
|
|
form = url.Values{"username": {"admin"}, "password": {"correct-password"}}
|
|
req, _ = http.NewRequest("POST", "/test-login", strings.NewReader(form.Encode()))
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusTooManyRequests, w.Code)
|
|
}
|