diff --git a/api/core/dependencies/email/templates/admin-base.html b/api/core/dependencies/email/templates/admin-base.html new file mode 100644 index 000000000..8f73452ef --- /dev/null +++ b/api/core/dependencies/email/templates/admin-base.html @@ -0,0 +1,32 @@ + + + + + + {% block title %}{% endblock %} + + + + + + + + +
+

HNG Boilerplate

+
+ + {% block content %} {% endblock %} + + + + + + +
+
+

Please address this inquiry at your earliest convenience.

+

You are receiving this email because a user submitted a contact form on our website.

+
+ + \ No newline at end of file diff --git a/api/core/dependencies/email/templates/contact_us.html b/api/core/dependencies/email/templates/contact_us.html new file mode 100644 index 000000000..626fe1137 --- /dev/null +++ b/api/core/dependencies/email/templates/contact_us.html @@ -0,0 +1,57 @@ +{% extends 'admin-base.html' %} + +{% block title %} + Magic Link +{% endblock %} + +{% block content %} +
+
+

Dear Admin,

+

You have received a new contact form submission with the following details:

+
+
+

User Details

+ +

Message

+

+ {{ message }} +

+
+
+{% endblock %} + diff --git a/api/utils/send_mail.py b/api/utils/send_mail.py index f85415a51..e4d2c2fec 100644 --- a/api/utils/send_mail.py +++ b/api/utils/send_mail.py @@ -25,3 +25,32 @@ async def send_magic_link(context: dict): with smtplib.SMTP_SSL(settings.MAIL_SERVER, settings.MAIL_PORT) as server: server.login(sender_email, password) server.sendmail(sender_email, receiver_email, message.as_string()) + + +async def send_contact_mail(context: dict): + """Sends user contact to admin mail + + Args: + context (dict): Holds data for sending email, such as 'name', 'email', and 'message'. + """ + from main import email_templates + sender_email = settings.MAIL_FROM + reciever_email = settings.MAIL_USERNAME + password = settings.MAIL_PASSWORD + + html = email_templates.get_template("contact_us.html").render(context) + + message = MIMEMultipart("alternative") + message["Subject"] = "New Contact Request" + message["From"] = sender_email + message["To"] = reciever_email + + part = MIMEText(html, "html") + + message.attach(part) + + with smtplib.SMTP_SSL(settings.MAIL_SERVER, settings.MAIL_PORT) as server: + server.login(settings.MAIL_USERNAME, password) + server.sendmail(sender_email, reciever_email, message.as_string()) + + diff --git a/api/v1/routes/contact_us.py b/api/v1/routes/contact_us.py index f9312b861..6620c9c51 100644 --- a/api/v1/routes/contact_us.py +++ b/api/v1/routes/contact_us.py @@ -1,6 +1,7 @@ -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, status, BackgroundTasks from sqlalchemy.orm import Session from api.db.database import get_db +from api.utils.send_mail import send_contact_mail from typing import Annotated from api.core.responses import SUCCESS from api.utils.success_response import success_response @@ -25,10 +26,23 @@ }, ) async def create_contact_us( - data: CreateContactUs, db: Annotated[Session, Depends(get_db)] + data: CreateContactUs, db: Annotated[Session, Depends(get_db)], + background_tasks: BackgroundTasks, ): """Add a new contact us message.""" new_contact_us_message = contact_us_service.create(db, data) + + # Send email to admin + background_tasks.add_task( + send_contact_mail, + context={ + "full_name": new_contact_us_message.full_name, + "email": new_contact_us_message.email, + "phone": new_contact_us_message.title, + "message": new_contact_us_message.message, + } + ) + response = success_response( message=SUCCESS, data={"id": new_contact_us_message.id}, diff --git a/api/v1/schemas/contact_us.py b/api/v1/schemas/contact_us.py index f07969f22..2123b6b02 100644 --- a/api/v1/schemas/contact_us.py +++ b/api/v1/schemas/contact_us.py @@ -18,3 +18,4 @@ class CreateContactUs(BaseModel): email: EmailStr phone_number: str message: str + org_id: str diff --git a/api/v1/services/contact_us.py b/api/v1/services/contact_us.py index 936ea267c..b95740a19 100644 --- a/api/v1/services/contact_us.py +++ b/api/v1/services/contact_us.py @@ -16,6 +16,7 @@ def __init__(self) -> None: "email": "email", "title": "phone_number", # Adapting the schema to the model "message": "message", + "org_id": "org_id", } super().__init__() @@ -28,6 +29,7 @@ def create(self, db: Annotated[Session, Depends(get_db)], data: CreateContactUs) email=getattr(data, self.adabtingMapper["email"]), title=getattr(data, self.adabtingMapper["title"]), message=getattr(data, self.adabtingMapper["message"]), + org_id=getattr(data, self.adabtingMapper["org_id"]) ) db.add(contact_message) db.commit() diff --git a/tests/v1/contact_us/test_post_contact_us.py b/tests/v1/contact_us/test_post_contact_us.py new file mode 100644 index 000000000..485af38a3 --- /dev/null +++ b/tests/v1/contact_us/test_post_contact_us.py @@ -0,0 +1,107 @@ +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest +from fastapi.testclient import TestClient +from fastapi import status +from sqlalchemy.orm import Session +from uuid_extensions import uuid7 + +from api.db.database import get_db +from api.utils.send_mail import send_contact_mail +from api.v1.models.contact_us import ContactUs +from api.v1.models.organisation import Organisation +from api.v1.services.user import user_service +from api.v1.models.user import User +from main import app + + + +@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 mock_org(): + return Organisation( + id=str(uuid7()), + name="Test Organisation", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) + ) + +def mock_contact_us(): + return ContactUs( + id=str(uuid7()), + full_name="Jane Doe", + email="jane.doe@example.com", + title="08058878456", + message="Hello, I would like more information about your services and pricing.", + org_id=mock_org().id + ) + +@patch('fastapi.BackgroundTasks.add_task') +@patch("api.v1.services.contact_us.contact_us_service.create") +def test_post_contact_us(mock_create, mock_add_task, db_session_mock, client): + '''Test to successfully create a new contact request''' + + db_session_mock.add.return_value = None + db_session_mock.commit.return_value = None + db_session_mock.refresh.return_value = None + + mock_create.return_value = mock_contact_us() + # mock_email_send.return_value = None + + response = client.post('/api/v1/contact', json={ + "full_name": "Jane Doe", + "email": "jane.doe@example.com", + "phone_number": "08058878456", + "message": "Hello, I would like more information about your services and pricing.", + "org_id": mock_org().id + }) + + print(response.json()) + assert response.status_code == 201 + + # Assert that the contact_us_service.create was called with the expected arguments + mock_create.assert_called_once() + + mock_add_task.assert_called_once() + mock_add_task.assert_called_with( + send_contact_mail, + context={ + "full_name": "Jane Doe", + "email": "jane.doe@example.com", + "phone": "08058878456", + "message": "Hello, I would like more information about your services and pricing.", + } + ) + + +@patch('fastapi.BackgroundTasks.add_task') +@patch("api.v1.services.contact_us.contact_us_service.create") +def test_post_contact_missing_fields(mock_create, mock_add_task, db_session_mock, client): + '''Test to unsuccessfully create a new contact request withz category''' + + db_session_mock.add.return_value = None + db_session_mock.commit.return_value = None + db_session_mock.refresh.return_value = None + + mock_create.return_value = mock_contact_us() + + response = client.post('/api/v1/contact', json={ + "email": "jane.doe@example.com", + "message": "Hello, I would like more information about your services and pricing.", + }) + + print(response.json()) + assert response.status_code == 422 + +