Files
Charon/backend/internal/services/certificate_service_sync_coverage_test.go
GitHub Actions 8239a94938 chore: Add tests for CertificateList and CertificateUploadDialog components
- Implement test to deselect a row checkbox in CertificateList by clicking it a second time.
- Add test to close detail dialog via the close button in CertificateList.
- Add test to close export dialog via the cancel button in CertificateList.
- Add test to show KEY format badge when a .key file is uploaded in CertificateUploadDialog.
- Add test to ensure no format badge is shown for unknown file extensions in CertificateUploadDialog.
2026-04-15 11:35:10 +00:00

237 lines
7.5 KiB
Go

package services
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/models"
)
func TestSyncFromDisk_StagingToProductionUpgrade(t *testing.T) {
tmpDir := t.TempDir()
certRoot := filepath.Join(tmpDir, "certificates")
require.NoError(t, os.MkdirAll(certRoot, 0755))
domain := "staging-upgrade.example.com"
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
certFile := filepath.Join(certRoot, domain+".crt")
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
existing := models.SSLCertificate{
UUID: uuid.New().String(),
Name: domain,
Provider: "letsencrypt-staging",
Domains: domain,
Certificate: "old-content",
}
require.NoError(t, db.Create(&existing).Error)
svc := newTestCertificateService(tmpDir, db)
require.NoError(t, svc.SyncFromDisk())
var updated models.SSLCertificate
require.NoError(t, db.Where("uuid = ?", existing.UUID).First(&updated).Error)
assert.Equal(t, "letsencrypt", updated.Provider)
}
func TestSyncFromDisk_ExpiryOnlyUpdate(t *testing.T) {
tmpDir := t.TempDir()
certRoot := filepath.Join(tmpDir, "certificates")
require.NoError(t, os.MkdirAll(certRoot, 0755))
domain := "expiry-only.example.com"
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
certFile := filepath.Join(certRoot, domain+".crt")
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
existing := models.SSLCertificate{
UUID: uuid.New().String(),
Name: domain,
Provider: "letsencrypt",
Domains: domain,
Certificate: string(certPEM), // identical content
}
require.NoError(t, db.Create(&existing).Error)
svc := newTestCertificateService(tmpDir, db)
require.NoError(t, svc.SyncFromDisk())
var updated models.SSLCertificate
require.NoError(t, db.Where("uuid = ?", existing.UUID).First(&updated).Error)
assert.Equal(t, "letsencrypt", updated.Provider)
assert.Equal(t, string(certPEM), updated.Certificate)
}
func TestSyncFromDisk_CertRootStatPermissionError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("cannot test permission error as root")
}
tmpDir := t.TempDir()
certRoot := filepath.Join(tmpDir, "certificates")
require.NoError(t, os.MkdirAll(certRoot, 0755))
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
// Restrict parent dir so os.Stat(certRoot) fails with permission error
require.NoError(t, os.Chmod(tmpDir, 0))
defer func() { _ = os.Chmod(tmpDir, 0755) }()
svc := newTestCertificateService(tmpDir, db)
err = svc.SyncFromDisk()
require.NoError(t, err)
}
func TestListCertificates_StaleCache_TriggersBackgroundSync(t *testing.T) {
tmpDir := t.TempDir()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
svc := newTestCertificateService(tmpDir, db)
// Simulate stale cache
svc.cacheMu.Lock()
svc.initialized = true
svc.lastScan = time.Now().Add(-10 * time.Minute)
before := svc.lastScan
svc.cacheMu.Unlock()
_, err = svc.ListCertificates()
require.NoError(t, err)
// Background goroutine should update lastScan via SyncFromDisk
require.Eventually(t, func() bool {
svc.cacheMu.RLock()
defer svc.cacheMu.RUnlock()
return svc.lastScan.After(before)
}, 2*time.Second, 10*time.Millisecond)
}
func TestGetDecryptedPrivateKey_DecryptFails(t *testing.T) {
tmpDir := t.TempDir()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
svc := newTestCertServiceWithEnc(t, tmpDir, db)
cert := models.SSLCertificate{
UUID: uuid.New().String(),
Name: "enc-fail",
Domains: "encfail.example.com",
Provider: "custom",
PrivateKeyEncrypted: "corrupted-ciphertext",
}
require.NoError(t, db.Create(&cert).Error)
_, err = svc.GetDecryptedPrivateKey(&cert)
assert.Error(t, err)
}
func TestDeleteCertificate_LetsEncryptProvider_FileCleanup(t *testing.T) {
tmpDir := t.TempDir()
certRoot := filepath.Join(tmpDir, "certificates")
require.NoError(t, os.MkdirAll(certRoot, 0755))
domain := "le-cleanup.example.com"
certFile := filepath.Join(certRoot, domain+".crt")
keyFile := filepath.Join(certRoot, domain+".key")
jsonFile := filepath.Join(certRoot, domain+".json")
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
require.NoError(t, os.WriteFile(keyFile, []byte("key"), 0600))
require.NoError(t, os.WriteFile(jsonFile, []byte("{}"), 0600))
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
certUUID := uuid.New().String()
cert := models.SSLCertificate{
UUID: certUUID,
Name: domain,
Provider: "letsencrypt",
Domains: domain,
}
require.NoError(t, db.Create(&cert).Error)
svc := newTestCertificateService(tmpDir, db)
require.NoError(t, svc.DeleteCertificate(certUUID))
assert.NoFileExists(t, certFile)
assert.NoFileExists(t, keyFile)
assert.NoFileExists(t, jsonFile)
}
func TestDeleteCertificate_StagingProvider_FileCleanup(t *testing.T) {
tmpDir := t.TempDir()
certRoot := filepath.Join(tmpDir, "certificates")
require.NoError(t, os.MkdirAll(certRoot, 0755))
domain := "le-staging-cleanup.example.com"
certFile := filepath.Join(certRoot, domain+".crt")
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
certUUID := uuid.New().String()
cert := models.SSLCertificate{
UUID: certUUID,
Name: domain,
Provider: "letsencrypt-staging",
Domains: domain,
}
require.NoError(t, db.Create(&cert).Error)
svc := newTestCertificateService(tmpDir, db)
require.NoError(t, svc.DeleteCertificate(certUUID))
assert.NoFileExists(t, certFile)
}
func TestCheckExpiringCertificates_DBError(t *testing.T) {
tmpDir := t.TempDir()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
// deliberately do NOT AutoMigrate SSLCertificate
svc := newTestCertificateService(tmpDir, db)
_, err = svc.CheckExpiringCertificates(30)
assert.Error(t, err)
}