Files
Charon/backend/internal/api/handlers/certificate_handler_security_test.go
GitHub Actions 8e2ba14ae5 feat: add certificate management security and cleanup dialog
- Documented certificate management security features in security.md, including backup and recovery processes.
- Implemented CertificateCleanupDialog component for confirming deletion of orphaned certificates when deleting proxy hosts.
- Enhanced ProxyHosts page to check for orphaned certificates and prompt users accordingly during deletion.
- Added tests for certificate cleanup prompts and behaviors in ProxyHosts, ensuring correct handling of unique, shared, and production certificates.
2025-12-06 01:43:46 +00:00

200 lines
6.3 KiB
Go

package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/services"
)
// TestCertificateHandler_Delete_RequiresAuth tests that delete requires authentication
func TestCertificateHandler_Delete_RequiresAuth(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
if err := db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
// Note: NOT adding mockAuthMiddleware here to test auth requirement
svc := services.NewCertificateService("/tmp", db)
h := NewCertificateHandler(svc, nil, nil)
r.DELETE("/api/certificates/:id", h.Delete)
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 Unauthorized without auth, got %d", w.Code)
}
}
// TestCertificateHandler_List_RequiresAuth tests that list requires authentication
func TestCertificateHandler_List_RequiresAuth(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
if err := db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
// Note: NOT adding mockAuthMiddleware here to test auth requirement
svc := services.NewCertificateService("/tmp", db)
h := NewCertificateHandler(svc, nil, nil)
r.GET("/api/certificates", h.List)
req := httptest.NewRequest(http.MethodGet, "/api/certificates", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 Unauthorized without auth, got %d", w.Code)
}
}
// TestCertificateHandler_Upload_RequiresAuth tests that upload requires authentication
func TestCertificateHandler_Upload_RequiresAuth(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
if err := db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
// Note: NOT adding mockAuthMiddleware here to test auth requirement
svc := services.NewCertificateService("/tmp", db)
h := NewCertificateHandler(svc, nil, nil)
r.POST("/api/certificates", h.Upload)
req := httptest.NewRequest(http.MethodPost, "/api/certificates", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 Unauthorized without auth, got %d", w.Code)
}
}
// TestCertificateHandler_Delete_DiskSpaceCheck tests the disk space check before backup
func TestCertificateHandler_Delete_DiskSpaceCheck(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
if err := db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
// Create a certificate
cert := models.SSLCertificate{
UUID: "test-cert",
Name: "test",
Provider: "custom",
Domains: "test.com",
}
if err := db.Create(&cert).Error; err != nil {
t.Fatalf("failed to create cert: %v", err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(mockAuthMiddleware())
svc := services.NewCertificateService("/tmp", db)
// Mock backup service that reports low disk space
mockBackup := &mockBackupService{
availableSpaceFunc: func() (int64, error) {
return 50 * 1024 * 1024, nil // 50MB (less than 100MB required)
},
}
h := NewCertificateHandler(svc, mockBackup, nil)
r.DELETE("/api/certificates/:id", h.Delete)
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusInsufficientStorage {
t.Fatalf("expected 507 Insufficient Storage with low disk space, got %d", w.Code)
}
}
// TestCertificateHandler_Delete_NotificationRateLimiting tests rate limiting
func TestCertificateHandler_Delete_NotificationRateLimiting(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
if err := db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
// Create certificates
cert1 := models.SSLCertificate{UUID: "test-1", Name: "test1", Provider: "custom", Domains: "test1.com"}
cert2 := models.SSLCertificate{UUID: "test-2", Name: "test2", Provider: "custom", Domains: "test2.com"}
if err := db.Create(&cert1).Error; err != nil {
t.Fatalf("failed to create cert1: %v", err)
}
if err := db.Create(&cert2).Error; err != nil {
t.Fatalf("failed to create cert2: %v", err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(mockAuthMiddleware())
svc := services.NewCertificateService("/tmp", db)
mockBackup := &mockBackupService{
createFunc: func() (string, error) {
return "backup.zip", nil
},
}
h := NewCertificateHandler(svc, mockBackup, nil)
r.DELETE("/api/certificates/:id", h.Delete)
// Delete first cert
req1 := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert1.ID), nil)
w1 := httptest.NewRecorder()
r.ServeHTTP(w1, req1)
if w1.Code != http.StatusOK {
t.Fatalf("first delete failed: got %d", w1.Code)
}
// Delete second cert (different ID, should not be rate limited)
req2 := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert2.ID), nil)
w2 := httptest.NewRecorder()
r.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Fatalf("second delete failed: got %d", w2.Code)
}
// The test passes if both deletions succeed
// Rate limiting is per-certificate ID, so different certs should not interfere
}