chore: add unit tests for certificate handler, logs websocket upgrader, config loading, and mail service
This commit is contained in:
@@ -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{})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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, "<script>")
|
||||
}
|
||||
|
||||
func newTestTLSConfig(t *testing.T) (*tls.Config, []byte) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user