package services import ( "bufio" "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "net" "net/mail" "os" "path/filepath" "strconv" "strings" "sync" "testing" "time" "github.com/Wikid82/charon/backend/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // TestMain sets up the SSL_CERT_FILE environment variable globally BEFORE any tests run. // This ensures x509.SystemCertPool() initializes with our test CA, which is critical for // parallel test execution with -race flag where cert pool initialization timing matters. func TestMain(m *testing.M) { // Initialize shared test CA and write stable cert file initializeTestCAForSuite() // Set SSL_CERT_FILE globally so cert pool initialization uses our CA if err := os.Setenv("SSL_CERT_FILE", testCAFile); err != nil { panic("failed to set SSL_CERT_FILE: " + err.Error()) } // Run tests exitCode := m.Run() // Cleanup (optional, OS will clean /tmp on reboot) _ = os.Remove(testCAFile) os.Exit(exitCode) } // initializeTestCAForSuite is called once by TestMain to set up the shared CA infrastructure. func initializeTestCAForSuite() { testCAOnce.Do(func() { var err error testCAKey, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic("GenerateKey failed: " + err.Error()) } testCATemplate = &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ CommonName: "charon-test-ca", }, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * 365 * time.Hour), // 24 years KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, } caDER, err := x509.CreateCertificate(rand.Reader, testCATemplate, testCATemplate, &testCAKey.PublicKey, testCAKey) if err != nil { panic("CreateCertificate failed: " + err.Error()) } testCAPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}) testCAFile = filepath.Join(os.TempDir(), "charon-test-ca-mail-service.pem") if err := os.WriteFile(testCAFile, testCAPEM, 0o600); err != nil { panic("WriteFile failed: " + err.Error()) } }) } func setupMailTestDB(t *testing.T) *gorm.DB { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) err = db.AutoMigrate(&models.Setting{}) require.NoError(t, err) return db } func TestMailService_SaveAndGetSMTPConfig(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, Username: "user@example.com", Password: "secret123", FromAddress: "noreply@example.com", Encryption: "starttls", } err := svc.SaveSMTPConfig(config) require.NoError(t, err) retrieved, err := svc.GetSMTPConfig() require.NoError(t, err) assert.Equal(t, config.Host, retrieved.Host) assert.Equal(t, config.Port, retrieved.Port) assert.Equal(t, config.Username, retrieved.Username) assert.Equal(t, config.Password, retrieved.Password) assert.Equal(t, config.FromAddress, retrieved.FromAddress) assert.Equal(t, config.Encryption, retrieved.Encryption) } func TestMailService_UpdateSMTPConfig(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, Username: "user@example.com", Password: "secret123", FromAddress: "noreply@example.com", Encryption: "starttls", } err := svc.SaveSMTPConfig(config) require.NoError(t, err) config.Host = "smtp.newhost.com" config.Port = 465 config.Encryption = "ssl" err = svc.SaveSMTPConfig(config) require.NoError(t, err) retrieved, err := svc.GetSMTPConfig() require.NoError(t, err) assert.Equal(t, "smtp.newhost.com", retrieved.Host) assert.Equal(t, 465, retrieved.Port) assert.Equal(t, "ssl", retrieved.Encryption) } func TestMailService_IsConfigured(t *testing.T) { tests := []struct { name string config *SMTPConfig expected bool }{ { name: "configured with all fields", config: &SMTPConfig{ Host: "smtp.example.com", Port: 587, FromAddress: "noreply@example.com", Encryption: "starttls", }, expected: true, }, { name: "not configured - missing host", config: &SMTPConfig{ Port: 587, FromAddress: "noreply@example.com", }, expected: false, }, { name: "not configured - missing from address", config: &SMTPConfig{ Host: "smtp.example.com", Port: 587, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) err := svc.SaveSMTPConfig(tt.config) require.NoError(t, err) result := svc.IsConfigured() assert.Equal(t, tt.expected, result) }) } } func TestMailService_GetSMTPConfig_Defaults(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config, err := svc.GetSMTPConfig() require.NoError(t, err) assert.Equal(t, 587, config.Port) assert.Equal(t, "starttls", config.Encryption) assert.Empty(t, config.Host) } func TestMailService_BuildEmail(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) fromAddr, err := mail.ParseAddress("sender@example.com") require.NoError(t, err) toAddr, err := mail.ParseAddress("recipient@example.com") require.NoError(t, err) // Encode subject as SendEmail would encodedSubject, err := encodeSubject("Test Subject") require.NoError(t, err) msg, err := svc.buildEmail(fromAddr, toAddr, nil, encodedSubject, "
Test Body") require.NoError(t, err) msgStr := string(msg) // Addresses are RFC-formatted and may include angle brackets assert.Contains(t, msgStr, "From:") assert.Contains(t, msgStr, "sender@example.com") assert.Contains(t, msgStr, "To:") // After CodeQL remediation, To: header uses undisclosed recipients assert.Contains(t, msgStr, "undisclosed-recipients:;") assert.Contains(t, msgStr, "Subject:") assert.Contains(t, msgStr, "Content-Type: text/html") assert.Contains(t, msgStr, "Test Body") } func TestParseEmailAddressForHeader(t *testing.T) { tests := []struct { name string email string wantErr bool }{ {"valid email", "user@example.com", false}, {"valid email with name", "User NameBody
") assert.Error(t, err) } func TestMailService_BuildEmail_RejectsCRLFInReplyTo(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) fromAddr, err := mail.ParseAddress("sender@example.com") require.NoError(t, err) toAddr, err := mail.ParseAddress("recipient@example.com") require.NoError(t, err) // Create reply-to address with CRLF injection attempt in the name field replyToAddr := &mail.Address{ Name: "Attacker\r\nBcc: evil@example.com", Address: "attacker@example.com", } _, err = svc.buildEmail(fromAddr, toAddr, replyToAddr, "Test Subject", "Body
") assert.Error(t, err, "Should reject CRLF in reply-to name") } func TestMailService_SMTPDotStuffing(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) fromAddr, err := mail.ParseAddress("from@example.com") require.NoError(t, err) toAddr, err := mail.ParseAddress("to@example.com") require.NoError(t, err) tests := []struct { name string htmlBody string shouldContain string }{ { name: "body with leading period on line", htmlBody: "Line 1\n.Line 2 starts with period\nLine 3", shouldContain: "Line 1\n..Line 2 starts with period\nLine 3", }, { name: "body with SMTP terminator sequence", htmlBody: "Some text\n.\nMore text", shouldContain: "Some text\n..\nMore text", }, { name: "body with multiple leading periods", htmlBody: ".First\n..Second\nNormal", shouldContain: "..First\n...Second\nNormal", }, { name: "body without leading periods", htmlBody: "Normal line\nAnother normal line", shouldContain: "Normal line\nAnother normal line", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { msg, err := svc.buildEmail(fromAddr, toAddr, nil, "Test", tc.htmlBody) require.NoError(t, err) msgStr := string(msg) parts := strings.Split(msgStr, "\r\n\r\n") require.Len(t, parts, 2, "Email should have headers and body") body := parts[1] assert.Contains(t, body, tc.shouldContain, "Body should contain dot-stuffed content") }) } } func TestSanitizeEmailBody(t *testing.T) { tests := []struct { name string input string expected string }{ {"single leading period", ".test", "..test"}, {"period in middle", "test.com", "test.com"}, {"multiple lines with periods", "line1\n.line2\nline3", "line1\n..line2\nline3"}, {"SMTP terminator", "text\n.\nmore", "text\n..\nmore"}, {"no periods", "clean text", "clean text"}, {"empty string", "", ""}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := sanitizeEmailBody(tc.input) assert.Equal(t, tc.expected, result) }) } } func TestMailService_TestConnection_NotConfigured(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) err := svc.TestConnection() assert.Error(t, err) assert.Contains(t, err.Error(), "not configured") } func TestMailService_SendEmail_NotConfigured(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) err := svc.SendEmail(context.Background(), []string{"test@example.com"}, "Subject", "Body
") assert.Error(t, err) assert.Contains(t, err.Error(), "not configured") } func TestMailService_SendEmail_RejectsCRLFInSubject(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, FromAddress: "noreply@example.com", } require.NoError(t, svc.SaveSMTPConfig(config)) err := svc.SendEmail(context.Background(), []string{"recipient@example.com"}, "Hello\r\nBcc: evil@example.com", "Body") assert.Error(t, err) assert.Contains(t, err.Error(), "invalid subject") } func TestSMTPConfigSerialization(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, Username: "user@example.com", Password: "p@$$w0rd!#$%", FromAddress: "CharonNormal body
") assert.Error(t, err, tc.description) assert.Contains(t, err.Error(), "invalid", "Error should indicate invalid input") }) } } // TestMailService_SendInvite_CRLFInjection tests CRLF injection prevention in invite emails // TestMailService_BuildEmail_UndisclosedRecipients verifies that buildEmail uses // "undisclosed-recipients:;" in the To: header instead of the actual recipient address. // This prevents request-derived data from appearing in message headers (CodeQL go/email-injection). func TestMailService_BuildEmail_UndisclosedRecipients(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) fromAddr, err := mail.ParseAddress("sender@example.com") require.NoError(t, err) toAddr, err := mail.ParseAddress("recipient@example.com") require.NoError(t, err) // Encode subject as SendEmail would encodedSubject, err := encodeSubject("Test Subject") require.NoError(t, err) msg, err := svc.buildEmail(fromAddr, toAddr, nil, encodedSubject, "Test Body") require.NoError(t, err) msgStr := string(msg) // MUST contain undisclosed recipients header assert.Contains(t, msgStr, "To: undisclosed-recipients:;", "To header must use undisclosed recipients") // MUST NOT contain the actual recipient email address in headers // Split message into headers and body parts := strings.Split(msgStr, "\r\n\r\n") require.Len(t, parts, 2, "Email should have headers and body sections") headers := parts[0] assert.NotContains(t, headers, "recipient@example.com", "Recipient email must not appear in message headers") } // TestMailService_SendInvite_HTMLTemplateEscaping verifies that the HTML template // properly escapes special characters in user-controlled fields like appName. func TestMailService_SendInvite_HTMLTemplateEscaping(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, FromAddress: "noreply@example.com", Encryption: "starttls", } require.NoError(t, svc.SaveSMTPConfig(config)) // Use appName with HTML tags that should be escaped maliciousAppName := "XSS" baseURL := "https://example.com" token := "testtoken123" // SendInvite will fail because SMTP isn't actually configured, // but we can test the template rendering by calling the internal logic // directly through a helper or by examining the error path. // For now, we'll test that the appName validation rejects CRLF // (HTML injection in templates is handled by html/template auto-escaping) err := svc.SendInvite("test@example.com", token, maliciousAppName, baseURL) assert.Error(t, err) // Will fail due to SMTP not actually running // The key security property is that html/template auto-escapes {{.AppName}} // This is verified by the template engine itself, not by our code } func TestMailService_SendInvite_CRLFInjection(t *testing.T) { db := setupMailTestDB(t) svc := NewMailService(db) config := &SMTPConfig{ Host: "smtp.example.com", Port: 587, FromAddress: "noreply@example.com", Encryption: "starttls", } require.NoError(t, svc.SaveSMTPConfig(config)) tests := []struct { name string email string token string appName string baseURL string description string }{ { name: "CRLF in email address", email: "victim@example.com\r\nBcc: attacker@evil.com", token: "token123", appName: "TestApp", baseURL: "https://example.com", description: "Reject CRLF in invite email address", }, { name: "CRLF in baseURL", email: "user@example.com", token: "token123", appName: "TestApp", baseURL: "https://example.com\r\nBcc: attacker@evil.com", description: "Reject CRLF in invite baseURL", }, { name: "CRLF in app name (subject)", email: "user@example.com", token: "token123", appName: "TestApp\r\nBcc: attacker@evil.com", baseURL: "https://example.com", description: "Reject CRLF in app name used in subject", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := svc.SendInvite(tc.email, tc.token, tc.appName, tc.baseURL) assert.Error(t, err, tc.description) }) } } func TestRejectCRLF(t *testing.T) { t.Parallel() require.NoError(t, rejectCRLF("normal-value")) require.ErrorIs(t, rejectCRLF("bad\r\nvalue"), errEmailHeaderInjection) } func TestNormalizeBaseURLForInvite(t *testing.T) { t.Parallel() tests := []struct { name string raw string want string wantErr bool }{ {name: "valid https", raw: "https://example.com", want: "https://example.com", wantErr: false}, {name: "valid http with slash path", raw: "https://discord.com/api/webhooks/123/abc/", want: "https://discord.com/api/webhooks/123/abc", wantErr: false}, {name: "empty", raw: "", wantErr: true}, {name: "invalid scheme", raw: "ftp://example.com", wantErr: true}, {name: "with path", raw: "https://example.com/path", wantErr: true}, {name: "with query", raw: "https://example.com?x=1", wantErr: true}, {name: "with fragment", raw: "https://example.com#frag", wantErr: true}, {name: "with user info", raw: "https://user@example.com", wantErr: true}, {name: "with header injection", raw: "https://example.com\r\nX-Test: 1", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := normalizeBaseURLForInvite(tt.raw) if tt.wantErr { require.Error(t, err) require.ErrorIs(t, err, errInvalidBaseURLForInvite) return } require.NoError(t, err) require.Equal(t, tt.want, got) }) } } func TestEncodeSubject_RejectsCRLF(t *testing.T) { t.Parallel() _, err := encodeSubject("Hello\r\nWorld") require.Error(t, err) require.ErrorIs(t, err, errEmailHeaderInjection) } // Shared test CA infrastructure to work around Go's cert pool caching. // When tests run with -count=N, Go caches x509.SystemCertPool() after the first run. // Generating a new CA per test causes failures because cached pool references old CA. // Solution: Generate CA once, reuse across runs, and use stable cert file path. var ( testCAOnce sync.Once testCAPEM []byte testCAKey *rsa.PrivateKey testCATemplate *x509.Certificate testCAFile string ) func initTestCA(t *testing.T) { t.Helper() // Delegate to the suite-level initialization (already called by TestMain) initializeTestCAForSuite() } func newTestTLSConfigShared(t *testing.T) (*tls.Config, []byte) { t.Helper() // Ensure shared CA is initialized initTestCA(t) // Generate leaf certificate signed by shared CA leafKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) leafTemplate := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().UnixNano()), // Unique serial per leaf Subject: pkix.Name{ CommonName: "127.0.0.1", }, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, testCATemplate, &leafKey.PublicKey, testCAKey) require.NoError(t, err) leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}) leafKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)}) cert, err := tls.X509KeyPair(leafCertPEM, leafKeyPEM) require.NoError(t, err) return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}, testCAPEM } func TestMailService_GetSMTPConfig_DBError(t *testing.T) { t.Parallel() db := setupMailTestDB(t) svc := NewMailService(db) sqlDB, err := db.DB() require.NoError(t, err) require.NoError(t, sqlDB.Close()) _, err = svc.GetSMTPConfig() assert.Error(t, err) assert.Contains(t, err.Error(), "failed to load SMTP settings") } func TestMailService_GetSMTPConfig_InvalidPortFallback(t *testing.T) { t.Parallel() db := setupMailTestDB(t) svc := NewMailService(db) require.NoError(t, db.Create(&models.Setting{Key: "smtp_host", Value: "smtp.example.com", Type: "string", Category: "smtp"}).Error) require.NoError(t, db.Create(&models.Setting{Key: "smtp_port", Value: "invalid", Type: "string", Category: "smtp"}).Error) require.NoError(t, db.Create(&models.Setting{Key: "smtp_from_address", Value: "noreply@example.com", Type: "string", Category: "smtp"}).Error) config, err := svc.GetSMTPConfig() require.NoError(t, err) assert.Equal(t, 587, config.Port) } func TestMailService_BuildEmail_NilAddressValidation(t *testing.T) { t.Parallel() db := setupMailTestDB(t) svc := NewMailService(db) toAddr, err := mail.ParseAddress("recipient@example.com") require.NoError(t, err) _, err = svc.buildEmail(nil, toAddr, nil, "Subject", "Body") assert.Error(t, err) assert.Contains(t, err.Error(), "from address is required") fromAddr, err := mail.ParseAddress("sender@example.com") require.NoError(t, err) _, err = svc.buildEmail(fromAddr, nil, nil, "Subject", "Body") assert.Error(t, err) assert.Contains(t, err.Error(), "to address is required") } func TestWriteEmailHeader_RejectsCRLFValue(t *testing.T) { t.Parallel() var buf bytes.Buffer err := writeEmailHeader(&buf, headerSubject, "bad\r\nvalue") assert.Error(t, err) } func TestMailService_sendSSL_DialFailure(t *testing.T) { t.Parallel() db := setupMailTestDB(t) svc := NewMailService(db) err := svc.sendSSL( "127.0.0.1:1", &SMTPConfig{Host: "127.0.0.1"}, nil, "from@example.com", "to@example.com", []byte("test"), ) assert.Error(t, err) assert.Contains(t, err.Error(), "SSL connection failed") } func TestMailService_sendSTARTTLS_DialFailure(t *testing.T) { t.Parallel() db := setupMailTestDB(t) svc := NewMailService(db) err := svc.sendSTARTTLS( "127.0.0.1:1", &SMTPConfig{Host: "127.0.0.1"}, nil, "from@example.com", "to@example.com", []byte("test"), ) assert.Error(t, err) assert.Contains(t, err.Error(), "SMTP connection failed") } func TestMailService_TestConnection_StartTLSSuccessWithAuth(t *testing.T) { t.Parallel() tlsConf, _ := newTestTLSConfigShared(t) trustTestCertificate(t, nil) addr, cleanup := startMockSMTPServer(t, tlsConf, true, true) defer cleanup() host, portStr, err := net.SplitHostPort(addr) require.NoError(t, err) port, err := strconv.Atoi(portStr) require.NoError(t, err) db := setupMailTestDB(t) svc := NewMailService(db) require.NoError(t, svc.SaveSMTPConfig(&SMTPConfig{ Host: host, Port: port, Username: "user", Password: "pass", FromAddress: "sender@example.com", Encryption: "starttls", })) require.NoError(t, svc.TestConnection()) } func TestMailService_TestConnection_NoneSuccess(t *testing.T) { t.Parallel() tlsConf, _ := newTestTLSConfig(t) addr, cleanup := startMockSMTPServer(t, tlsConf, false, false) defer cleanup() host, portStr, err := net.SplitHostPort(addr) require.NoError(t, err) port, err := strconv.Atoi(portStr) require.NoError(t, err) db := setupMailTestDB(t) svc := NewMailService(db) require.NoError(t, svc.SaveSMTPConfig(&SMTPConfig{ Host: host, Port: port, FromAddress: "sender@example.com", Encryption: "none", })) require.NoError(t, svc.TestConnection()) } func TestMailService_SendEmail_STARTTLSSuccess(t *testing.T) { t.Parallel() tlsConf, _ := newTestTLSConfigShared(t) trustTestCertificate(t, nil) addr, cleanup := startMockSMTPServer(t, tlsConf, true, true) defer cleanup() host, portStr, err := net.SplitHostPort(addr) require.NoError(t, err) port, err := strconv.Atoi(portStr) require.NoError(t, err) db := setupMailTestDB(t) svc := NewMailService(db) require.NoError(t, svc.SaveSMTPConfig(&SMTPConfig{ Host: host, Port: port, Username: "user", Password: "pass", FromAddress: "sender@example.com", Encryption: "starttls", })) // With fixed cert trust, STARTTLS connection and email send succeed err = svc.SendEmail(context.Background(), []string{"recipient@example.com"}, "Subject", "Body") require.NoError(t, err) } func TestMailService_SendEmail_SSLSuccess(t *testing.T) { t.Parallel() tlsConf, _ := newTestTLSConfigShared(t) trustTestCertificate(t, nil) addr, cleanup := startMockSSLSMTPServer(t, tlsConf, true) defer cleanup() host, portStr, err := net.SplitHostPort(addr) require.NoError(t, err) port, err := strconv.Atoi(portStr) require.NoError(t, err) db := setupMailTestDB(t) svc := NewMailService(db) require.NoError(t, svc.SaveSMTPConfig(&SMTPConfig{ Host: host, Port: port, Username: "user", Password: "pass", FromAddress: "sender@example.com", Encryption: "ssl", })) // With fixed cert trust, SSL connection and email send succeed err = svc.SendEmail(context.Background(), []string{"recipient@example.com"}, "Subject", "Body") 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", "Body
") 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, "Hello World
", result) } func TestSanitizeAndNormalizeHTMLBody_HTMLEscaping(t *testing.T) { t.Parallel() result := sanitizeAndNormalizeHTMLBody("") assert.NotContains(t, result, "