chore: add unit tests for certificate handler, logs websocket upgrader, config loading, and mail service

This commit is contained in:
GitHub Actions
2026-03-08 05:44:53 +00:00
parent 698ad86d17
commit c4e8d6c8ae
4 changed files with 131 additions and 0 deletions

View File

@@ -17,6 +17,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -516,6 +518,42 @@ func generateSelfSignedCertPEM() (certPEM, keyPEM string, err error) {
// 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{}))
gin.SetMode(gin.TestMode)
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{})

View File

@@ -33,6 +33,43 @@ func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {
t.Fatalf("condition not met within %s", timeout)
}
func TestUpgraderCheckOrigin(t *testing.T) {
t.Parallel()
tests := []struct {
name string
origin string
host string
xForwardedHost string
want bool
}{
{"empty origin allows request", "", "example.com", "", true},
{"invalid URL origin rejects", "://bad-url", "example.com", "", false},
{"matching host allows", "http://example.com", "example.com", "", true},
{"non-matching host rejects", "http://evil.com", "example.com", "", false},
{"X-Forwarded-Host matching allows", "http://proxy.example.com", "backend.internal", "proxy.example.com", true},
{"X-Forwarded-Host non-matching rejects", "http://evil.com", "backend.internal", "proxy.example.com", false},
{"origin with port matching", "http://example.com:8080", "example.com:8080", "", true},
{"origin with port non-matching", "http://example.com:9090", "example.com:8080", "", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/ws", http.NoBody)
if tc.origin != "" {
req.Header.Set("Origin", tc.origin)
}
req.Host = tc.host
if tc.xForwardedHost != "" {
req.Header.Set("X-Forwarded-Host", tc.xForwardedHost)
}
got := upgrader.CheckOrigin(req)
assert.Equal(t, tc.want, got, "origin=%q host=%q xfh=%q", tc.origin, tc.host, tc.xForwardedHost)
})
}
}
func TestLogsWebSocketHandler_DeprecatedWrapperUpgradeFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
charonlogger.Init(false, io.Discard)

View File

@@ -152,6 +152,24 @@ func TestGetEnvIntAny(t *testing.T) {
})
}
func TestLoad_JWTSecretFallbackGeneration(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db"))
t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy"))
t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports"))
// Clear both JWT secret env vars to trigger fallback generation
t.Setenv("CHARON_JWT_SECRET", "")
t.Setenv("CPM_JWT_SECRET", "")
cfg, err := Load()
require.NoError(t, err)
// Fallback generates 32 random bytes → 64-char hex string
assert.NotEmpty(t, cfg.JWTSecret)
assert.Len(t, cfg.JWTSecret, 64)
}
func TestLoad_SecurityConfig(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db"))

View File

@@ -1092,6 +1092,44 @@ func TestMailService_SendEmail_SSLSuccess(t *testing.T) {
require.NoError(t, err)
}
func TestMailService_SendEmail_ContextCancelled(t *testing.T) {
t.Parallel()
db := setupMailTestDB(t)
svc := NewMailService(db)
require.NoError(t, svc.SaveSMTPConfig(&SMTPConfig{
Host: "127.0.0.1",
Port: 2525,
FromAddress: "sender@example.com",
Encryption: "none",
}))
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
err := svc.SendEmail(ctx, []string{"recipient@example.com"}, "Test Subject", "<p>Body</p>")
require.Error(t, err)
assert.Contains(t, err.Error(), "context cancelled")
}
func TestSanitizeAndNormalizeHTMLBody_EmptyInput(t *testing.T) {
t.Parallel()
assert.Equal(t, "", sanitizeAndNormalizeHTMLBody(""))
}
func TestSanitizeAndNormalizeHTMLBody_SingleLine(t *testing.T) {
t.Parallel()
result := sanitizeAndNormalizeHTMLBody("Hello World")
assert.Equal(t, "<p>Hello World</p>", result)
}
func TestSanitizeAndNormalizeHTMLBody_HTMLEscaping(t *testing.T) {
t.Parallel()
result := sanitizeAndNormalizeHTMLBody("<script>alert('xss')</script>")
assert.NotContains(t, result, "<script>")
assert.Contains(t, result, "&lt;script&gt;")
}
func newTestTLSConfig(t *testing.T) (*tls.Config, []byte) {
t.Helper()