- Implemented `useManualChallenge`, `useChallengePoll`, and `useManualChallengeMutations` hooks for managing manual DNS challenges. - Created tests for the `useManualChallenge` hooks to ensure correct fetching and mutation behavior. - Added `ManualDNSChallenge` component for displaying challenge details and actions. - Developed end-to-end tests for the Manual DNS Provider feature, covering provider selection, challenge UI, and accessibility compliance. - Included error handling tests for verification failures and network errors.
843 lines
26 KiB
Go
843 lines
26 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/crypto"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Additional Plugin Handler Tests for Coverage
|
|
// =============================================================================
|
|
|
|
func TestPluginHandler_EnablePlugin_DatabaseUpdateError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := OpenTestDBWithMigrations(t)
|
|
pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil)
|
|
|
|
// Create plugin
|
|
plugin := models.Plugin{
|
|
UUID: "plugin-db-error-uuid",
|
|
Name: "Test Plugin",
|
|
Type: "test-type",
|
|
Enabled: false,
|
|
Status: models.PluginStatusError,
|
|
FilePath: "/nonexistent/path.so",
|
|
}
|
|
db.Create(&plugin)
|
|
|
|
handler := NewPluginHandler(db, pluginLoader)
|
|
|
|
// Close DB to trigger error during update
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.POST("/plugins/:id/enable", handler.EnablePlugin)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/plugins/1/enable", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
func TestPluginHandler_DisablePlugin_DatabaseUpdateError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := OpenTestDBWithMigrations(t)
|
|
pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil)
|
|
|
|
// Create plugin
|
|
plugin := models.Plugin{
|
|
UUID: "plugin-disable-error-uuid",
|
|
Name: "Test Plugin",
|
|
Type: "test-type-disable",
|
|
Enabled: true,
|
|
Status: models.PluginStatusLoaded,
|
|
FilePath: "/path/to/plugin.so",
|
|
}
|
|
db.Create(&plugin)
|
|
|
|
handler := NewPluginHandler(db, pluginLoader)
|
|
|
|
// Close DB to trigger error during update
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.POST("/plugins/:id/disable", handler.DisablePlugin)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/plugins/1/disable", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
func TestPluginHandler_GetPlugin_DatabaseError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := OpenTestDBWithMigrations(t)
|
|
pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil)
|
|
|
|
// Create plugin first
|
|
plugin := models.Plugin{
|
|
UUID: "get-error-uuid",
|
|
Name: "Get Error",
|
|
Type: "get-error-type",
|
|
Enabled: true,
|
|
FilePath: "/path/to/get.so",
|
|
}
|
|
db.Create(&plugin)
|
|
|
|
handler := NewPluginHandler(db, pluginLoader)
|
|
|
|
// Close DB to trigger database error
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.GET("/plugins/:id", handler.GetPlugin)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/plugins/1", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Failed to get plugin")
|
|
}
|
|
|
|
func TestPluginHandler_EnablePlugin_DatabaseFirstError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := OpenTestDBWithMigrations(t)
|
|
pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil)
|
|
|
|
handler := NewPluginHandler(db, pluginLoader)
|
|
|
|
// Close DB to trigger error when fetching plugin
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.POST("/plugins/:id/enable", handler.EnablePlugin)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/plugins/1/enable", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Failed to get plugin")
|
|
}
|
|
|
|
func TestPluginHandler_DisablePlugin_DatabaseFirstError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := OpenTestDBWithMigrations(t)
|
|
pluginLoader := services.NewPluginLoaderService(db, "/tmp/plugins", nil)
|
|
|
|
handler := NewPluginHandler(db, pluginLoader)
|
|
|
|
// Close DB to trigger error when fetching plugin
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.POST("/plugins/:id/disable", handler.DisablePlugin)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/plugins/1/disable", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Failed to get plugin")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Encryption Handler - Additional Coverage Tests
|
|
// =============================================================================
|
|
|
|
func TestEncryptionHandler_Validate_NonAdminAccess(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
currentKey, _ := crypto.GenerateNewKey()
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
|
|
db := setupEncryptionTestDB(t)
|
|
rotationService, _ := crypto.NewRotationService(db)
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, false)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestEncryptionHandler_GetHistory_PaginationBoundary(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
currentKey, _ := crypto.GenerateNewKey()
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
|
|
db := setupEncryptionTestDB(t)
|
|
rotationService, _ := crypto.NewRotationService(db)
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
// Test invalid page number (negative)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/history?page=-1&limit=10", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Test limit exceeding max (should clamp)
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/api/v1/admin/encryption/history?page=1&limit=200", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &response)
|
|
// limit should not exceed 100
|
|
assert.LessOrEqual(t, response["limit"].(float64), float64(100))
|
|
}
|
|
|
|
func TestEncryptionHandler_GetStatus_VersionInfo(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
currentKey, _ := crypto.GenerateNewKey()
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer func() {
|
|
os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
}()
|
|
|
|
db := setupEncryptionTestDB(t)
|
|
rotationService, _ := crypto.NewRotationService(db)
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/admin/encryption/status", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var status crypto.RotationStatus
|
|
err := json.Unmarshal(w.Body.Bytes(), &status)
|
|
assert.NoError(t, err)
|
|
// Verify the status response has expected fields
|
|
assert.True(t, status.CurrentVersion >= 1)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Settings Handler - Additional Unique Coverage Tests
|
|
// =============================================================================
|
|
|
|
func TestSettingsHandler_TestPublicURL_RoleNotExists(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
router := gin.New()
|
|
// Don't set any role
|
|
router.POST("/test-url", handler.TestPublicURL)
|
|
|
|
body := `{"url": "https://example.com"}`
|
|
req, _ := http.NewRequest("POST", "/test-url", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestSettingsHandler_TestPublicURL_InvalidURLFormat(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Next()
|
|
})
|
|
router.POST("/test-url", handler.TestPublicURL)
|
|
|
|
body := `{"url": "not-a-valid-url"}`
|
|
req, _ := http.NewRequest("POST", "/test-url", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestSettingsHandler_TestPublicURL_PrivateIPBlocked_Coverage(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Next()
|
|
})
|
|
router.POST("/test-url", handler.TestPublicURL)
|
|
|
|
// SSRF attempt with private IP
|
|
body := `{"url": "http://192.168.1.1"}`
|
|
req, _ := http.NewRequest("POST", "/test-url", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should return 200 but with reachable=false due to SSRF protection
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var response map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.False(t, response["reachable"].(bool))
|
|
}
|
|
|
|
func TestSettingsHandler_ValidatePublicURL_WithTrailingSlash(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Next()
|
|
})
|
|
router.POST("/validate-url", handler.ValidatePublicURL)
|
|
|
|
// URL with trailing slash (should normalize and may produce warning)
|
|
body := `{"url": "https://example.com/"}`
|
|
req, _ := http.NewRequest("POST", "/validate-url", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var response map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.True(t, response["valid"].(bool))
|
|
}
|
|
|
|
func TestSettingsHandler_ValidatePublicURL_MissingScheme(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("role", "admin")
|
|
c.Next()
|
|
})
|
|
router.POST("/validate-url", handler.ValidatePublicURL)
|
|
|
|
// Invalid URL (missing scheme)
|
|
body := `{"url": "example.com"}`
|
|
req, _ := http.NewRequest("POST", "/validate-url", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var response map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.False(t, response["valid"].(bool))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Audit Log Handler - Additional Coverage Tests
|
|
// =============================================================================
|
|
|
|
func TestAuditLogHandler_List_PaginationEdgeCases(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dbPath := fmt.Sprintf("/tmp/test_audit_pagination_%d.db", time.Now().UnixNano())
|
|
t.Cleanup(func() { _ = os.Remove(dbPath) })
|
|
|
|
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
_ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{})
|
|
|
|
// Create test audits
|
|
for i := 0; i < 10; i++ {
|
|
db.Create(&models.SecurityAudit{
|
|
Actor: "user1",
|
|
Action: fmt.Sprintf("action_%d", i),
|
|
EventCategory: "test",
|
|
Details: "{}",
|
|
})
|
|
}
|
|
|
|
secService := services.NewSecurityService(db)
|
|
defer secService.Close()
|
|
handler := NewAuditLogHandler(secService)
|
|
|
|
router := gin.New()
|
|
router.GET("/audit", handler.List)
|
|
|
|
// Test with pagination
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/audit?page=2&limit=3", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestAuditLogHandler_List_CategoryFilter(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dbPath := fmt.Sprintf("/tmp/test_audit_category_%d.db", time.Now().UnixNano())
|
|
t.Cleanup(func() { _ = os.Remove(dbPath) })
|
|
|
|
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
_ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{})
|
|
|
|
// Create test audits with different categories
|
|
db.Create(&models.SecurityAudit{
|
|
Actor: "user1",
|
|
Action: "action1",
|
|
EventCategory: "encryption",
|
|
Details: "{}",
|
|
})
|
|
db.Create(&models.SecurityAudit{
|
|
Actor: "user2",
|
|
Action: "action2",
|
|
EventCategory: "security",
|
|
Details: "{}",
|
|
})
|
|
|
|
secService := services.NewSecurityService(db)
|
|
defer secService.Close()
|
|
handler := NewAuditLogHandler(secService)
|
|
|
|
router := gin.New()
|
|
router.GET("/audit", handler.List)
|
|
|
|
// Test with category filter
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/audit?category=encryption", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
func TestAuditLogHandler_ListByProvider_DatabaseError(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dbPath := fmt.Sprintf("/tmp/test_audit_db_error_%d.db", time.Now().UnixNano())
|
|
t.Cleanup(func() { _ = os.Remove(dbPath) })
|
|
|
|
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
_ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{})
|
|
|
|
secService := services.NewSecurityService(db)
|
|
defer secService.Close()
|
|
handler := NewAuditLogHandler(secService)
|
|
|
|
// Close DB to trigger error
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
router := gin.New()
|
|
router.GET("/audit/provider/:id", handler.ListByProvider)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/audit/provider/1", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
func TestAuditLogHandler_ListByProvider_InvalidProviderID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
dbPath := fmt.Sprintf("/tmp/test_audit_invalid_id_%d.db", time.Now().UnixNano())
|
|
t.Cleanup(func() { _ = os.Remove(dbPath) })
|
|
|
|
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
|
_ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{})
|
|
|
|
secService := services.NewSecurityService(db)
|
|
defer secService.Close()
|
|
handler := NewAuditLogHandler(secService)
|
|
|
|
router := gin.New()
|
|
router.GET("/audit/provider/:id", handler.ListByProvider)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/audit/provider/invalid", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// getActorFromGinContext Additional Coverage
|
|
// =============================================================================
|
|
|
|
func TestGetActorFromGinContext_InvalidUserIDType(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
var capturedActor string
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_id", 123.45) // float - invalid type
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
capturedActor = getActorFromGinContext(c)
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should fall back to "system" for invalid type
|
|
assert.Equal(t, "system", capturedActor)
|
|
}
|
|
|
|
// =============================================================================
|
|
// isAdmin Additional Coverage
|
|
// =============================================================================
|
|
|
|
func TestIsAdmin_NonAdminRole(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
router := gin.New()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("user_role", "user") // Not admin
|
|
c.Next()
|
|
})
|
|
router.GET("/test", func(c *gin.Context) {
|
|
if isAdmin(c) {
|
|
c.JSON(http.StatusOK, gin.H{"admin": true})
|
|
} else {
|
|
c.JSON(http.StatusForbidden, gin.H{"admin": false})
|
|
}
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Credential Handler - Additional Coverage Tests
|
|
// =============================================================================
|
|
|
|
func setupCredentialHandlerTestWithCtx(t *testing.T) (*gin.Engine, *gorm.DB, *models.DNSProvider, context.Context) {
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
|
|
t.Cleanup(func() { os.Unsetenv("CHARON_ENCRYPTION_KEY") })
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared&_journal_mode=WAL", t.Name())
|
|
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
})
|
|
|
|
err = db.AutoMigrate(
|
|
&models.DNSProvider{},
|
|
&models.DNSProviderCredential{},
|
|
&models.SecurityAudit{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
testKey := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
|
|
encryptor, _ := crypto.NewEncryptionService(testKey)
|
|
|
|
creds := map[string]string{"api_token": "test-token"}
|
|
credsJSON, _ := json.Marshal(creds)
|
|
encrypted, _ := encryptor.Encrypt(credsJSON)
|
|
|
|
provider := &models.DNSProvider{
|
|
UUID: "test-uuid",
|
|
Name: "Test Provider",
|
|
ProviderType: "cloudflare",
|
|
Enabled: true,
|
|
UseMultiCredentials: true,
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
db.Create(provider)
|
|
|
|
credService := services.NewCredentialService(db, encryptor)
|
|
credHandler := NewCredentialHandler(credService)
|
|
|
|
router.GET("/api/v1/dns-providers/:id/credentials", credHandler.List)
|
|
router.POST("/api/v1/dns-providers/:id/credentials", credHandler.Create)
|
|
router.GET("/api/v1/dns-providers/:id/credentials/:cred_id", credHandler.Get)
|
|
router.PUT("/api/v1/dns-providers/:id/credentials/:cred_id", credHandler.Update)
|
|
router.DELETE("/api/v1/dns-providers/:id/credentials/:cred_id", credHandler.Delete)
|
|
router.POST("/api/v1/dns-providers/:id/credentials/:cred_id/test", credHandler.Test)
|
|
router.POST("/api/v1/dns-providers/:id/enable-multi-credentials", credHandler.EnableMultiCredentials)
|
|
|
|
return router, db, provider, context.Background()
|
|
}
|
|
|
|
func TestCredentialHandler_Update_InvalidProviderType(t *testing.T) {
|
|
router, db, _, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
testKey := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
|
|
encryptor, _ := crypto.NewEncryptionService(testKey)
|
|
|
|
// Create provider with invalid type
|
|
creds := map[string]string{"api_token": "test-token"}
|
|
credsJSON, _ := json.Marshal(creds)
|
|
encrypted, _ := encryptor.Encrypt(credsJSON)
|
|
|
|
provider := &models.DNSProvider{
|
|
UUID: "invalid-type-uuid",
|
|
Name: "Invalid Type Provider",
|
|
ProviderType: "nonexistent-provider",
|
|
Enabled: true,
|
|
UseMultiCredentials: true,
|
|
CredentialsEncrypted: encrypted,
|
|
KeyVersion: 1,
|
|
}
|
|
db.Create(provider)
|
|
|
|
// Create credential
|
|
credService := services.NewCredentialService(db, encryptor)
|
|
createReq := services.CreateCredentialRequest{
|
|
Label: "Original",
|
|
Credentials: map[string]string{"api_token": "token"},
|
|
}
|
|
|
|
// This should fail because provider type doesn't exist
|
|
_, err := credService.Create(context.Background(), provider.ID, createReq)
|
|
if err != nil {
|
|
// Expected - provider type validation fails
|
|
return
|
|
}
|
|
|
|
// If it didn't fail, try update with bad credentials
|
|
updateBody := `{"label":"Updated","credentials":{"invalid_field":"value"}}`
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/1", provider.ID)
|
|
req, _ := http.NewRequest("PUT", url, strings.NewReader(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestCredentialHandler_List_DatabaseClosed(t *testing.T) {
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
|
|
dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
|
db, _ := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
|
_ = db.AutoMigrate(&models.DNSProvider{}, &models.DNSProviderCredential{})
|
|
|
|
testKey := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
|
|
encryptor, _ := crypto.NewEncryptionService(testKey)
|
|
|
|
credService := services.NewCredentialService(db, encryptor)
|
|
credHandler := NewCredentialHandler(credService)
|
|
|
|
router.GET("/api/v1/dns-providers/:id/credentials", credHandler.List)
|
|
|
|
// Close DB to trigger error
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
req, _ := http.NewRequest("GET", "/api/v1/dns-providers/1/credentials", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Settings Handler - MaskPasswordForTest Coverage (unique test name)
|
|
// =============================================================================
|
|
|
|
func TestSettingsHandler_MaskPasswordForTestFunction(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
password string
|
|
expected string
|
|
}{
|
|
{"empty string", "", ""},
|
|
{"non-empty password", "secret123", "********"},
|
|
{"already masked", "********", "********"},
|
|
{"single char", "x", "********"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := MaskPasswordForTest(tt.password)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Credential Handler - Additional Update/Delete Error Paths (unique names)
|
|
// =============================================================================
|
|
|
|
func TestCredentialHandler_Update_NotFoundError(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
updateBody := `{"label":"Updated","credentials":{"api_token":"new-token"}}`
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/9999", provider.ID)
|
|
req, _ := http.NewRequest("PUT", url, strings.NewReader(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "not found")
|
|
}
|
|
|
|
func TestCredentialHandler_Update_MalformedJSON(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/1", provider.ID)
|
|
req, _ := http.NewRequest("PUT", url, strings.NewReader("invalid json"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestCredentialHandler_Update_BadCredentialID(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/invalid", provider.ID)
|
|
req, _ := http.NewRequest("PUT", url, strings.NewReader(`{}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Invalid credential ID")
|
|
}
|
|
|
|
func TestCredentialHandler_Delete_NotFoundError(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/9999", provider.ID)
|
|
req, _ := http.NewRequest("DELETE", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestCredentialHandler_Delete_BadCredentialID(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/invalid", provider.ID)
|
|
req, _ := http.NewRequest("DELETE", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestCredentialHandler_Test_BadCredentialID(t *testing.T) {
|
|
router, _, provider, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
url := fmt.Sprintf("/api/v1/dns-providers/%d/credentials/invalid/test", provider.ID)
|
|
req, _ := http.NewRequest("POST", url, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestCredentialHandler_EnableMultiCredentials_BadProviderID(t *testing.T) {
|
|
router, _, _, _ := setupCredentialHandlerTestWithCtx(t)
|
|
|
|
req, _ := http.NewRequest("POST", "/api/v1/dns-providers/invalid/enable-multi-credentials", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Encryption Handler - Additional Validate Success Test
|
|
// =============================================================================
|
|
|
|
func TestEncryptionHandler_Validate_AdminSuccess(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
currentKey, _ := crypto.GenerateNewKey()
|
|
os.Setenv("CHARON_ENCRYPTION_KEY", currentKey)
|
|
defer os.Unsetenv("CHARON_ENCRYPTION_KEY")
|
|
|
|
db := setupEncryptionTestDB(t)
|
|
rotationService, _ := crypto.NewRotationService(db)
|
|
securityService := services.NewSecurityService(db)
|
|
defer securityService.Close()
|
|
|
|
handler := NewEncryptionHandler(rotationService, securityService)
|
|
router := setupEncryptionTestRouter(handler, true)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/admin/encryption/validate", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|