Skip to content

Commit

Permalink
Merge pull request #979 from theijhay/feat/send-email-template
Browse files Browse the repository at this point in the history
feat: Added send email template endpoint
  • Loading branch information
joboy-dev authored Aug 25, 2024
2 parents ef004ea + 60c8150 commit d2ce65f
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 10 deletions.
23 changes: 22 additions & 1 deletion api/v1/routes/email_template.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Depends, status, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session

Expand Down Expand Up @@ -92,3 +92,24 @@ async def delete_email_template(
"""Endpoint to delete a single template"""

email_template_service.delete(db, template_id=template_id)


@email_template.post("/{template_id}/send", response_model=success_response, status_code=200)
async def send_email_template(
template_id: str,
recipient_email: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_super_admin),
):
"""Endpoint to send an email template to a recipient"""

send_result = email_template_service.send(db=db, template_id=template_id, recipient_email=recipient_email)

if send_result["status"] == "failure":
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=send_result["message"])

return success_response(
data=send_result,
message=f"Email sent successfully to {recipient_email}",
status_code=status.HTTP_200_OK,
)
8 changes: 6 additions & 2 deletions api/v1/schemas/email_template.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pydantic import BaseModel, field_validator
import bleach
from enum import Enum

ALLOWED_TAGS = [
'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li',
Expand Down Expand Up @@ -30,12 +31,15 @@ def sanitize_html(template: str) -> str:
)
return cleaned_html

class EmailTemplateSchema(BaseModel):
class TemplateStatusEnum(str, Enum):
online = 'online'
offline = 'offline'

class EmailTemplateSchema(BaseModel):
title: str
template: str
type: str
status: str = 'online'
template_status: TemplateStatusEnum | None = TemplateStatusEnum.online # Default to 'online'

@field_validator("template")
@classmethod
Expand Down
31 changes: 28 additions & 3 deletions api/v1/services/email_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
from api.v1.models.email_template import EmailTemplate
from api.v1.schemas.email_template import EmailTemplateSchema
from api.utils.db_validators import check_model_existence
import logging
import time



class EmailTemplateService(Service):
'''Email template service functionality'''

def create(self, db: Session, schema: EmailTemplateSchema):
"""Create a new FAQ"""

"""Create a new Email Template"""
new_template = EmailTemplate(**schema.model_dump())
db.add(new_template)
db.commit()
db.refresh(new_template)

return new_template

def fetch_all(self, db: Session, **query_params: Optional[Any]):
Expand Down Expand Up @@ -58,6 +59,30 @@ def delete(self, db: Session, template_id: str):
template = self.fetch(db=db, template_id=template_id)
db.delete(template)
db.commit()


def send(self, db: Session, template_id: str, recipient_email: str, max_retries: int = 3):
"""Send an email template to a recipient with error handling and retries"""

template = self.fetch(db=db, template_id=template_id)

for attempt in range(max_retries):
try:
self._send_email(recipient_email, template)
logging.info(f"Template {template_id} sent successfully to {recipient_email}")
return {"status": "success", "message": f"Email sent to {recipient_email}"}

except Exception as e:
logging.error(f"Attempt {attempt + 1} failed to send template {template_id}: {e}")
time.sleep(2 ** attempt)

logging.error(f"All attempts to send template {template_id} to {recipient_email} failed.")
return {"status": "failure", "message": "Failed to send email after multiple attempts"}

def _send_email(self, recipient_email: str, template: EmailTemplate):
"""Mock email sending function"""
if not recipient_email or not template:
raise ValueError("Invalid recipient email or template.")
pass

email_template_service = EmailTemplateService()
8 changes: 4 additions & 4 deletions seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@


admin_user = User(
email="freeman@example.com",
password=user_service.hash_password("supersecret"),
first_name="Habeeb",
last_name="Habeeb",
email="Isaacj@gmail.com",
password=user_service.hash_password("45@&tuTU"),
first_name="Isaac",
last_name="John",
is_active=True,
is_superadmin=True,
is_deleted=False,
Expand Down
105 changes: 105 additions & 0 deletions tests/v1/email_template/test_send_email_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from datetime import datetime, timezone
from uuid_extensions import uuid7
import pytest

from api.db.database import get_db
from api.v1.services.user import user_service
from api.v1.models import User
from api.v1.models.email_template import EmailTemplate
from api.v1.services.email_template import email_template_service
from main import app


# Mocked data and services
def mock_get_current_admin():
return User(
id=str(uuid7()),
email="[email protected]",
password=user_service.hash_password("Testadmin@123"),
first_name='Admin',
last_name='User',
is_active=True,
is_superadmin=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)


def mock_email_template():
return EmailTemplate(
id=str(uuid7()),
title="Test title",
type="Test type",
template="<h1>Hello</h1>",
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)


@pytest.fixture
def db_session_mock():
db_session = MagicMock(spec=Session)
return db_session


@pytest.fixture
def client(db_session_mock):
app.dependency_overrides[get_db] = lambda: db_session_mock
client = TestClient(app)
yield client
app.dependency_overrides = {}


def test_send_email_template_success(client, db_session_mock):
"""Test successfully sending an email template"""

# Mock the current admin user and email template
app.dependency_overrides[user_service.get_current_super_admin] = lambda: mock_get_current_admin()
app.dependency_overrides[email_template_service.fetch] = lambda db, template_id: mock_email_template()

with patch("api.v1.services.email_template.email_template_service.send") as mock_send:
mock_send.return_value = {"status": "success", "message": "Email sent to [email protected]"}

# recipient_email is passed as a query parameter now
response = client.post(
f'/api/v1/email-templates/{mock_email_template().id}/[email protected]',
headers={'Authorization': 'Bearer token'},
)

assert response.status_code == 200
assert response.json()["message"] == "Email sent successfully to [email protected]"


def test_send_email_template_failure(client, db_session_mock):
"""Test failing to send an email template with retry mechanism"""

# Mock the current admin user and email template
app.dependency_overrides[user_service.get_current_super_admin] = lambda: mock_get_current_admin()
app.dependency_overrides[email_template_service.fetch] = lambda db, template_id: mock_email_template()

with patch("api.v1.services.email_template.email_template_service.send") as mock_send:
mock_send.return_value = {"status": "failure", "message": "Failed to send email after multiple attempts"}

response = client.post(
f'/api/v1/email-templates/{mock_email_template().id}/[email protected]',
headers={'Authorization': 'Bearer token'},
)

assert response.status_code == 500
assert response.json()["message"] == "Failed to send email after multiple attempts"




def test_send_email_template_unauthorized(client, db_session_mock):
"""Test sending email template by unauthorized user"""

response = client.post(
f'/api/v1/email-templates/{mock_email_template().id}/send',
json={"recipient_email": "[email protected]"},
)

assert response.status_code == 401

0 comments on commit d2ce65f

Please sign in to comment.