test_email_service.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. """Unit tests for email service.
  2. These tests verify email template rendering and HTML formatting.
  3. """
  4. from unittest.mock import AsyncMock, patch
  5. import pytest
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.models.notification_template import NotificationTemplate
  8. from backend.app.services.email_service import (
  9. create_password_reset_email_from_template,
  10. create_welcome_email_from_template,
  11. )
  12. class TestEmailTemplateFormatting:
  13. """Tests for email template formatting."""
  14. @pytest.mark.asyncio
  15. async def test_welcome_email_newlines_converted_to_br(self):
  16. """Verify that newlines in welcome email body are converted to <br> tags."""
  17. # Mock database session
  18. db = AsyncMock(spec=AsyncSession)
  19. # Mock template with newlines
  20. template = NotificationTemplate(
  21. event_type="user_created",
  22. name="Welcome Email",
  23. title_template="Welcome to {app_name}",
  24. body_template="Hello {username}!\n\nYour password is: {password}\n\nPlease login at: {login_url}",
  25. is_default=True,
  26. )
  27. # Patch get_notification_template to return our template
  28. with patch("backend.app.services.email_service.get_notification_template", return_value=template):
  29. # Generate email
  30. subject, text_body, html_body = await create_welcome_email_from_template(
  31. db=db,
  32. username="testuser",
  33. password="testpass123",
  34. login_url="http://example.com/login",
  35. app_name="TestApp",
  36. )
  37. # Verify subject
  38. assert subject == "Welcome to TestApp"
  39. # Verify text body has newlines
  40. assert "\n\n" in text_body
  41. assert "Hello testuser!" in text_body
  42. assert "Your password is: testpass123" in text_body
  43. # Verify HTML body has <br> tags instead of relying on CSS
  44. assert "<br>" in html_body
  45. # Should not use white-space: pre-wrap
  46. assert "white-space: pre-wrap" not in html_body
  47. # Should have proper structure
  48. assert "<!DOCTYPE html>" in html_body
  49. assert '<div style="font-size: 16px;">' in html_body
  50. # Verify that escaped content is present (XSS protection)
  51. assert "Hello testuser!<br>" in html_body
  52. assert "Your password is: testpass123<br>" in html_body
  53. @pytest.mark.asyncio
  54. async def test_password_reset_email_newlines_converted_to_br(self):
  55. """Verify that newlines in password reset email body are converted to <br> tags."""
  56. # Mock database session
  57. db = AsyncMock(spec=AsyncSession)
  58. # Mock template with newlines
  59. template = NotificationTemplate(
  60. event_type="password_reset",
  61. name="Password Reset",
  62. title_template="{app_name} - Password Reset",
  63. body_template="Hello {username},\n\nYour password has been reset.\nNew password: {password}\n\nLogin at: {login_url}",
  64. is_default=True,
  65. )
  66. # Patch get_notification_template to return our template
  67. with patch("backend.app.services.email_service.get_notification_template", return_value=template):
  68. # Generate email
  69. subject, text_body, html_body = await create_password_reset_email_from_template(
  70. db=db,
  71. username="testuser",
  72. password="newpass456",
  73. login_url="http://example.com/login",
  74. app_name="TestApp",
  75. )
  76. # Verify subject
  77. assert subject == "TestApp - Password Reset"
  78. # Verify text body has newlines
  79. assert "\n\n" in text_body
  80. assert "Hello testuser," in text_body
  81. # Verify HTML body has <br> tags
  82. assert "<br>" in html_body
  83. # Should not use white-space: pre-wrap
  84. assert "white-space: pre-wrap" not in html_body
  85. # Should have security alert
  86. assert "Security Alert" in html_body
  87. @pytest.mark.asyncio
  88. async def test_email_header_padding(self):
  89. """Verify that email header has proper padding to prevent cutoff."""
  90. # Mock database session
  91. db = AsyncMock(spec=AsyncSession)
  92. # Mock template
  93. template = NotificationTemplate(
  94. event_type="user_created",
  95. name="Welcome Email",
  96. title_template="Welcome",
  97. body_template="Test body",
  98. is_default=True,
  99. )
  100. # Patch get_notification_template to return our template
  101. with patch("backend.app.services.email_service.get_notification_template", return_value=template):
  102. # Generate email
  103. subject, text_body, html_body = await create_welcome_email_from_template(
  104. db=db,
  105. username="testuser",
  106. password="testpass123",
  107. login_url="http://example.com/login",
  108. )
  109. # Verify header has 30px padding (not 20px which was cutting off)
  110. assert "padding: 30px; border-radius: 8px 8px 0 0;" in html_body
  111. @pytest.mark.asyncio
  112. async def test_email_xss_protection(self):
  113. """Verify that HTML escaping is applied to prevent XSS attacks."""
  114. # Mock database session
  115. db = AsyncMock(spec=AsyncSession)
  116. # Mock template with potential XSS content
  117. template = NotificationTemplate(
  118. event_type="user_created",
  119. name="Welcome Email",
  120. title_template="Welcome <script>alert('xss')</script>",
  121. body_template="Hello <script>alert('xss')</script>\nTest",
  122. is_default=True,
  123. )
  124. # Patch get_notification_template to return our template
  125. with patch("backend.app.services.email_service.get_notification_template", return_value=template):
  126. # Generate email
  127. subject, text_body, html_body = await create_welcome_email_from_template(
  128. db=db,
  129. username="testuser",
  130. password="testpass123",
  131. login_url="http://example.com/login",
  132. )
  133. # Verify that script tags are escaped
  134. assert "&lt;script&gt;" in html_body
  135. # Verify no unescaped script tags
  136. assert "<script>" not in html_body