- Removed redundant `gin.SetMode(gin.TestMode)` calls from individual test files. - Introduced a centralized `TestMain` function in `testmain_test.go` to set the Gin mode for all tests. - Ensured consistent test environment setup across various handler test files.
890 lines
29 KiB
Go
890 lines
29 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/Wikid82/charon/backend/internal/services"
|
|
)
|
|
|
|
// mockAuthMiddleware adds a mock user to the context for testing
|
|
func mockAuthMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Set("user", map[string]any{"id": 1, "username": "testuser"})
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func setupCertTestRouter(t *testing.T, db *gorm.DB) *gin.Engine {
|
|
t.Helper()
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
return r
|
|
}
|
|
|
|
func TestDeleteCertificate_InUse(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)
|
|
}
|
|
|
|
// Migrate minimal models
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert", Name: "example-cert", Provider: "custom", Domains: "example.com"}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
// Create proxy host referencing the certificate
|
|
ph := models.ProxyHost{UUID: "ph-1", Name: "ph", DomainNames: "example.com", ForwardHost: "localhost", ForwardPort: 8080, CertificateID: &cert.ID}
|
|
if err := db.Create(&ph).Error; err != nil {
|
|
t.Fatalf("failed to create proxy host: %v", err)
|
|
}
|
|
|
|
r := setupCertTestRouter(t, db)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409 Conflict, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func toStr(id uint) string {
|
|
return fmt.Sprintf("%d", id)
|
|
}
|
|
|
|
// Test that deleting a certificate NOT in use creates a backup and deletes successfully
|
|
func TestDeleteCertificate_CreatesBackup(t *testing.T) {
|
|
// Use a file-backed DB with busy timeout and single connection to avoid
|
|
// lock contention with CertificateService background sync.
|
|
dbPath := t.TempDir() + "/cert_create_backup.db"
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("failed to access sql db: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-backup-success", Name: "deletable-cert", Provider: "custom", Domains: "delete.example.com"}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
// Mock BackupService
|
|
backupCalled := false
|
|
mockBackupService := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
backupCalled = true
|
|
return "backup-test.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
if !backupCalled {
|
|
t.Fatal("expected backup to be created before deletion")
|
|
}
|
|
|
|
// Verify certificate was deleted
|
|
var found models.SSLCertificate
|
|
err = db.First(&found, cert.ID).Error
|
|
if err == nil {
|
|
t.Fatal("expected certificate to be deleted")
|
|
}
|
|
}
|
|
|
|
// Test that backup failure prevents deletion
|
|
func TestDeleteCertificate_BackupFailure(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 certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-backup-fails", Name: "deletable-cert", Provider: "custom", Domains: "delete-fail.example.com"}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
// Mock BackupService that fails
|
|
mockBackupService := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
return "", fmt.Errorf("backup creation failed")
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500 Internal Server Error, got %d", w.Code)
|
|
}
|
|
|
|
// Verify certificate was NOT deleted
|
|
var found models.SSLCertificate
|
|
err = db.First(&found, cert.ID).Error
|
|
if err != nil {
|
|
t.Fatal("expected certificate to still exist after backup failure")
|
|
}
|
|
}
|
|
|
|
// Test that in-use check does not create a backup
|
|
func TestDeleteCertificate_InUse_NoBackup(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 certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-in-use-no-backup", Name: "in-use-cert", Provider: "custom", Domains: "inuse.example.com"}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
// Create proxy host referencing the certificate
|
|
ph := models.ProxyHost{UUID: "ph-no-backup-test", Name: "ph", DomainNames: "inuse.example.com", ForwardHost: "localhost", ForwardPort: 8080, CertificateID: &cert.ID}
|
|
if err := db.Create(&ph).Error; err != nil {
|
|
t.Fatalf("failed to create proxy host: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
// Mock BackupService
|
|
backupCalled := false
|
|
mockBackupService := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
backupCalled = true
|
|
return "backup-test.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409 Conflict, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
if backupCalled {
|
|
t.Fatal("expected backup NOT to be created when certificate is in use")
|
|
}
|
|
}
|
|
|
|
// Mock BackupService for testing
|
|
type mockBackupService struct {
|
|
createFunc func() (string, error)
|
|
availableSpaceFunc func() (int64, error)
|
|
}
|
|
|
|
func (m *mockBackupService) CreateBackup() (string, error) {
|
|
if m.createFunc != nil {
|
|
return m.createFunc()
|
|
}
|
|
return "", fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *mockBackupService) ListBackups() ([]services.BackupFile, error) {
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *mockBackupService) DeleteBackup(filename string) error {
|
|
return fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *mockBackupService) GetBackupPath(filename string) (string, error) {
|
|
return "", fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *mockBackupService) RestoreBackup(filename string) error {
|
|
return fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (m *mockBackupService) GetAvailableSpace() (int64, error) {
|
|
if m.availableSpaceFunc != nil {
|
|
return m.availableSpaceFunc()
|
|
}
|
|
// Default: return 1GB available
|
|
return 1024 * 1024 * 1024, nil
|
|
}
|
|
|
|
// Test List handler
|
|
func TestCertificateHandler_List(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.GET("/api/certificates", h.List)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/certificates", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test Upload handler with missing name
|
|
func TestCertificateHandler_Upload_MissingName(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
// Empty body - no form fields
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", strings.NewReader(""))
|
|
req.Header.Set("Content-Type", "multipart/form-data")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Test Upload handler missing certificate_file
|
|
func TestCertificateHandler_Upload_MissingCertFile(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
body := strings.NewReader("name=testcert")
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d", w.Code)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "certificate_file") {
|
|
t.Fatalf("expected error message about certificate_file, got: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test Upload handler missing key_file
|
|
func TestCertificateHandler_Upload_MissingKeyFile(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
body := strings.NewReader("name=testcert")
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
_ = writer.WriteField("name", "testcert")
|
|
part, createErr := writer.CreateFormFile("certificate_file", "cert.pem")
|
|
if createErr != nil {
|
|
t.Fatalf("failed to create form file: %v", createErr)
|
|
}
|
|
_, _ = part.Write([]byte("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----"))
|
|
_ = writer.Close()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), "key_file") {
|
|
t.Fatalf("expected error message about key_file, got: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test Upload handler success path using a mock CertificateService
|
|
func TestCertificateHandler_Upload_Success(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
|
|
// Create a mock CertificateService that returns a created certificate
|
|
// Create a temporary services.CertificateService with a temp dir and DB
|
|
tmpDir := t.TempDir()
|
|
svc := services.NewCertificateService(tmpDir, db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
// Prepare multipart form data
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
_ = writer.WriteField("name", "uploaded-cert")
|
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate cert: %v", err)
|
|
}
|
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
|
_, _ = part.Write([]byte(certPEM))
|
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
|
_, _ = part2.Write([]byte(keyPEM))
|
|
_ = writer.Close()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 Created, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func generateSelfSignedCertPEM() (certPEM, keyPEM string, err error) {
|
|
// generate RSA key
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
// create a simple self-signed cert
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"Test Org"},
|
|
},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
certBuf := new(bytes.Buffer)
|
|
_ = pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
keyBuf := new(bytes.Buffer)
|
|
_ = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
|
certPEM = certBuf.String()
|
|
keyPEM = keyBuf.String()
|
|
return certPEM, keyPEM, nil
|
|
}
|
|
|
|
// Note: mockCertificateService removed — helper tests now use real service instances or testify mocks inlined where required.
|
|
|
|
// TestCertificateHandler_Upload_WithNotificationService verifies that the notification
|
|
// path is exercised when a non-nil NotificationService is provided.
|
|
func TestCertificateHandler_Upload_WithNotificationService(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.Setting{}, &models.NotificationProvider{}))
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
|
|
tmpDir := t.TempDir()
|
|
svc := services.NewCertificateService(tmpDir, db)
|
|
ns := services.NewNotificationService(db, nil)
|
|
h := NewCertificateHandler(svc, nil, ns)
|
|
r.POST("/api/certificates", h.Upload)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
_ = writer.WriteField("name", "cert-with-ns")
|
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
|
require.NoError(t, err)
|
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
|
_, _ = part.Write([]byte(certPEM))
|
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
|
_, _ = part2.Write([]byte(keyPEM))
|
|
_ = writer.Close()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
}
|
|
|
|
// Test Delete with invalid ID format
|
|
func TestDeleteCertificate_InvalidID(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/invalid", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Test Delete with ID = 0
|
|
func TestDeleteCertificate_ZeroID(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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/0", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 Bad Request, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Test Delete with low disk space
|
|
func TestDeleteCertificate_LowDiskSpace(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 certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-low-space", Name: "low-space-cert", Provider: "custom", Domains: "lowspace.example.com"}
|
|
if err := db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
// Mock BackupService with low disk space
|
|
mockBackupService := &mockBackupService{
|
|
availableSpaceFunc: func() (int64, error) {
|
|
return 50 * 1024 * 1024, nil // Only 50MB available
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInsufficientStorage {
|
|
t.Fatalf("expected 507 Insufficient Storage, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test Delete with disk space check failure (warning but continue)
|
|
func TestDeleteCertificate_DiskSpaceCheckError(t *testing.T) {
|
|
// Use isolated file-backed DB to avoid lock flakiness from shared in-memory
|
|
// connections and background sync.
|
|
dbPath := t.TempDir() + "/cert_disk_space_error.db"
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("failed to access sql db: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-space-err", Name: "space-err-cert", Provider: "custom", Domains: "spaceerr.example.com"}
|
|
if err := db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
// Mock BackupService with space check error but backup succeeds
|
|
mockBackupService := &mockBackupService{
|
|
availableSpaceFunc: func() (int64, error) {
|
|
return 0, fmt.Errorf("failed to check disk space")
|
|
},
|
|
createFunc: func() (string, error) {
|
|
return "backup.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Should succeed even if space check fails (with warning)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test that an expired Let's Encrypt certificate not in use can be deleted.
|
|
// The backend has no provider-based restrictions; deletion policy is frontend-only.
|
|
func TestDeleteCertificate_ExpiredLetsEncrypt_NotInUse(t *testing.T) {
|
|
dbPath := t.TempDir() + "/cert_expired_le.db"
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("failed to access sql db: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
expired := time.Now().Add(-24 * time.Hour)
|
|
cert := models.SSLCertificate{
|
|
UUID: "expired-le-cert",
|
|
Name: "expired-le",
|
|
Provider: "letsencrypt",
|
|
Domains: "expired.example.com",
|
|
ExpiresAt: &expired,
|
|
}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
mockBS := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
return "backup-expired-le.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBS, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var found models.SSLCertificate
|
|
if err = db.First(&found, cert.ID).Error; err == nil {
|
|
t.Fatal("expected expired LE certificate to be deleted")
|
|
}
|
|
}
|
|
|
|
// Test that a valid (non-expired) Let's Encrypt certificate not in use can be deleted.
|
|
// Confirms the backend imposes no provider-based restrictions on deletion.
|
|
func TestDeleteCertificate_ValidLetsEncrypt_NotInUse(t *testing.T) {
|
|
dbPath := t.TempDir() + "/cert_valid_le.db"
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("failed to access sql db: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
future := time.Now().Add(30 * 24 * time.Hour)
|
|
cert := models.SSLCertificate{
|
|
UUID: "valid-le-cert",
|
|
Name: "valid-le",
|
|
Provider: "letsencrypt",
|
|
Domains: "valid.example.com",
|
|
ExpiresAt: &future,
|
|
}
|
|
if err = db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
|
|
mockBS := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
return "backup-valid-le.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBS, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var found models.SSLCertificate
|
|
if err = db.First(&found, cert.ID).Error; err == nil {
|
|
t.Fatal("expected valid LE certificate to be deleted")
|
|
}
|
|
}
|
|
|
|
// Test Delete when IsCertificateInUse fails
|
|
func TestDeleteCertificate_UsageCheckError(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)
|
|
}
|
|
|
|
// Only migrate SSLCertificate, not ProxyHost - this will cause usage check to fail
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create certificate
|
|
cert := models.SSLCertificate{UUID: "test-cert-usage-err", Name: "usage-err-cert", Provider: "custom", Domains: "usageerr.example.com"}
|
|
if err := db.Create(&cert).Error; err != nil {
|
|
t.Fatalf("failed to create cert: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
h := NewCertificateHandler(svc, nil, nil)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500 Internal Server Error, got %d, body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Test notification rate limiting
|
|
func TestDeleteCertificate_NotificationRateLimit(t *testing.T) {
|
|
// Use unique file-based temp db to avoid shared memory locking issues
|
|
tmpFile := t.TempDir() + "/rate_limit_test.db"
|
|
db, err := gorm.Open(sqlite.Open(tmpFile), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.NotificationProvider{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
// Create two certificates
|
|
cert1 := models.SSLCertificate{UUID: "test-cert-rate-1", Name: "rate-cert-1", Provider: "custom", Domains: "rate1.example.com"}
|
|
cert2 := models.SSLCertificate{UUID: "test-cert-rate-2", Name: "rate-cert-2", Provider: "custom", Domains: "rate2.example.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)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.Use(mockAuthMiddleware())
|
|
svc := services.NewCertificateService("/tmp", db)
|
|
ns := services.NewNotificationService(db, nil)
|
|
|
|
mockBackupService := &mockBackupService{
|
|
createFunc: func() (string, error) {
|
|
return "backup.tar.gz", nil
|
|
},
|
|
}
|
|
|
|
h := NewCertificateHandler(svc, mockBackupService, ns)
|
|
r.DELETE("/api/certificates/:id", h.Delete)
|
|
|
|
// Delete first certificate
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert1.ID), http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK for first delete, got %d", w.Code)
|
|
}
|
|
|
|
// Delete second certificate immediately (different ID, so should not be rate limited)
|
|
req = httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert2.ID), http.NoBody)
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK for second delete, got %d", w.Code)
|
|
}
|
|
}
|