From 60c815086d7fa23f27a00f1614382d81818cbde2 Mon Sep 17 00:00:00 2001 From: theijhay Date: Sun, 25 Aug 2024 08:22:05 +0100 Subject: [PATCH] Added send email template endpoint --- api/v1/routes/email_template.py | 23 +++- api/v1/schemas/email_template.py | 8 +- api/v1/services/email_template.py | 31 +++++- seed.py | 8 +- .../test_send_email_template.py | 105 ++++++++++++++++++ 5 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 tests/v1/email_template/test_send_email_template.py diff --git a/api/v1/routes/email_template.py b/api/v1/routes/email_template.py index 35346da26..91eb1c873 100644 --- a/api/v1/routes/email_template.py +++ b/api/v1/routes/email_template.py @@ -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 @@ -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, + ) diff --git a/api/v1/schemas/email_template.py b/api/v1/schemas/email_template.py index 1862866db..75f6621b3 100644 --- a/api/v1/schemas/email_template.py +++ b/api/v1/schemas/email_template.py @@ -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', @@ -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 diff --git a/api/v1/services/email_template.py b/api/v1/services/email_template.py index 5ca01a840..97d5f6bad 100644 --- a/api/v1/services/email_template.py +++ b/api/v1/services/email_template.py @@ -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]): @@ -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() diff --git a/seed.py b/seed.py index b4423180c..1a29666b1 100644 --- a/seed.py +++ b/seed.py @@ -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, diff --git a/tests/v1/email_template/test_send_email_template.py b/tests/v1/email_template/test_send_email_template.py new file mode 100644 index 000000000..b524b3979 --- /dev/null +++ b/tests/v1/email_template/test_send_email_template.py @@ -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="admin@gmail.com", + 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="

Hello

", + 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 recipient@example.com"} + + # recipient_email is passed as a query parameter now + response = client.post( + f'/api/v1/email-templates/{mock_email_template().id}/send?recipient_email=recipient@example.com', + headers={'Authorization': 'Bearer token'}, + ) + + assert response.status_code == 200 + assert response.json()["message"] == "Email sent successfully to recipient@example.com" + + +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}/send?recipient_email=recipient@example.com', + 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": "recipient@example.com"}, + ) + + assert response.status_code == 401