Skip to content

Commit

Permalink
Endpoint for sending email (#883)
Browse files Browse the repository at this point in the history
* fix formatting

* created tests for send_email endpoint

* Fix code scanning alert no. 45: Information exposure through an exception

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fixed errors in send_email and in tests

* lint

* added tests for empty lists and for sending mail to multiple users

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 3b9004e commit 527d0c2
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 0 deletions.
6 changes: 6 additions & 0 deletions app/communication/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ class UserNotificationSettingType(models.TextChoices):
RESERVATION_NEW = "RESERVATION NEW", "Ny reservasjon"
RESERVATION_APPROVED = "RESERVATION APPROVED", "Godkjent reservasjon"
RESERVATION_CANCELLED = "RESERVATION CANCELLED", "Avslått reservasjon"
KONTRES = "KONTRES", "Kontres"
BLITZED = "BLITZED", "Blitzed"

@classmethod
def get_kontres_and_blitzed(cls):
return [cls.KONTRES, cls.BLITZED]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.5 on 2024-09-23 18:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("communication", "0010_alter_usernotificationsetting_notification_type"),
]

operations = [
migrations.AlterField(
model_name="usernotificationsetting",
name="notification_type",
field=models.CharField(
choices=[
("REGISTRATION", "Påmeldingsoppdateringer"),
("UNREGISTRATION", "Avmeldingsoppdateringer"),
("STRIKE", "Prikkoppdateringer"),
("EVENT_SIGN_UP_START", "Arrangementer - påmeldingsstart"),
("EVENT_SIGN_OFF_DEADLINE", "Arrangementer - avmeldingsfrist"),
("EVENT_EVALUATION", "Arrangementer - evaluering"),
("EVENT_INFO", "Arrangementer - info fra arrangør"),
("FINE", "Grupper - bot"),
("GROUP_MEMBERSHIP", "Grupper - medlemsskap"),
("OTHER", "Andre"),
("RESERVATION NEW", "Ny reservasjon"),
("RESERVATION APPROVED", "Godkjent reservasjon"),
("RESERVATION CANCELLED", "Avslått reservasjon"),
("KONTRES", "Kontres"),
("BLITZED", "Blitzed"),
],
max_length=30,
),
),
]
216 changes: 216 additions & 0 deletions app/communication/tests/test_send_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import os
from unittest.mock import patch

from rest_framework import status

import pytest

from app.communication.enums import UserNotificationSettingType
from app.communication.notifier import Notify
from app.content.factories import UserFactory
from app.util.test_utils import get_api_client

EMAIL_URL = "/send-email/"
EMAIL_API_KEY = os.environ.get("EMAIL_API_KEY")


def _get_email_url():
return f"{EMAIL_URL}"


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_success(mock_send):
"""
Test that the send_email endpoint sends an email successfully.
"""
test_user = UserFactory()

data = {
"user_id_list": [test_user.user_id],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_201_CREATED
mock_send.assert_called_once()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_fails_when_field_missing(mock_send):
"""
Test that the send_email endpoint returns 400 when one of the fields is missing.
"""
test_user = UserFactory()

data = {
"user_id_list": [test_user.user_id],
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_send.assert_not_called()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_fails_when_wrong_api_key(mock_send):
"""
Test that the send_email endpoint returns 403 when the API key is invalid.
"""
test_user = UserFactory()

data = {
"user_id_list": [test_user.user_id],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": "wrong_key"}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_403_FORBIDDEN
mock_send.assert_not_called()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_fails_when_user_id_invalid(mock_send):
"""
Test that the send_email endpoint returns 404 when the user id is invalid.
"""
test_user = UserFactory()

data = {
"user_id_list": [999],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_404_NOT_FOUND
mock_send.assert_not_called()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
@pytest.mark.parametrize(
"notification_type", UserNotificationSettingType.get_kontres_and_blitzed()
)
def test_email_success_with_kontres_and_blitzed(mock_send, notification_type):
"""
Tests that the send_email endpoint works with both KONTRES and BLITZED notification types.
"""
test_user = UserFactory()

data = {
"user_id_list": [test_user.user_id],
"notification_type": notification_type,
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_201_CREATED
mock_send.assert_called_once()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_success_with_user_id_list(mock_send):
"""
Test that the send_email endpoint sends an email successfully to a list of users.
"""
test_user1 = UserFactory()
test_user2 = UserFactory()
test_user3 = UserFactory()

data = {
"user_id_list": [
test_user.user_id for test_user in [test_user1, test_user2, test_user3]
],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user1)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_201_CREATED
mock_send.assert_called_once()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_fails_when_user_id_list_is_empty(mock_send):
"""
Test that the send_email endpoint returns 400 when the user id list is empty.
"""
test_user = UserFactory()

data = {
"user_id_list": [],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": ["This is a test paragraph.", "This is another paragraph."],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_send.assert_not_called()


@pytest.mark.django_db
@patch.object(Notify, "send", return_value=None)
def test_send_email_fails_when_user_paragraph_list_is_empty(mock_send):
"""
Test that the send_email endpoint returns 400 when the paragraph list is empty.
"""
test_user = UserFactory()

data = {
"user_id_list": [test_user.user_id],
"notification_type": "KONTRES",
"title": "Test Notification",
"paragraphs": [],
}

client = get_api_client(user=test_user)
url = _get_email_url()
headers = {"api_key": EMAIL_API_KEY}
response = client.post(url, data, format="json", **headers)

assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_send.assert_not_called()
2 changes: 2 additions & 0 deletions app/content/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
accept_form,
delete,
register_with_feide,
send_email,
upload,
)

Expand Down Expand Up @@ -53,6 +54,7 @@
re_path(r"", include(router.urls)),
path("accept-form/", accept_form),
path("upload/", upload),
path("send-email/", send_email),
path("delete-file/<str:container_name>/<str:blob_name>/", delete),
path("feide/", register_with_feide),
re_path(r"users/(?P<user_id>[^/.]+)/events.ics", UserCalendarEvents()),
Expand Down
1 change: 1 addition & 0 deletions app/content/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
from app.content.views.logentry import LogEntryViewSet
from app.content.views.minute import MinuteViewSet
from app.content.views.feide import register_with_feide
from app.content.views.send_email import send_email
84 changes: 84 additions & 0 deletions app/content/views/send_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from app.communication.enums import UserNotificationSettingType
from app.communication.notifier import Notify
from app.content.models import User


@api_view(["POST"])
def send_email(request):
"""
Endpoint for sending a notification and email to a user.
Body should contain:
- 'user_id_list': A list of user ids to send the email to.
- 'notification_type': KONTRES or BLITZED.
- 'title': The title of the notification.
- 'paragraphs': A list of paragraphs to include in the notification.
The header should contain:
- 'api_key': A key for validating access.
"""
try:
EMAIL_API_KEY = os.environ.get("EMAIL_API_KEY")
api_key = request.META.get("api_key")
if api_key != EMAIL_API_KEY:
return Response(
{"detail": "Feil API nøkkel"},
status=status.HTTP_403_FORBIDDEN,
)

user_id_list = request.data.get("user_id_list")
paragraphs = request.data.get("paragraphs")
title = request.data.get("title")
notification_type = request.data.get("notification_type")

if not isinstance(user_id_list, list) or not user_id_list:
return Response(
{"detail": "En ikke-tom liste med bruker id-er må inkluderes"},
status=status.HTTP_400_BAD_REQUEST,
)

if not isinstance(paragraphs, list) or not paragraphs:
return Response(
{"detail": "En ikke-tom liste med paragrafer må inkluderes"},
status=status.HTTP_400_BAD_REQUEST,
)

if not notification_type or not title:
return Response(
{
"detail": "Notifikasjonstype (KONTRES/BLITZED) og tittel må være satt"
},
status=status.HTTP_400_BAD_REQUEST,
)

users = list(User.objects.filter(user_id__in=user_id_list))
if not users or len(users) != len(user_id_list):
return Response(
{"detail": "En eller flere brukere ble ikke funnet"},
status=status.HTTP_404_NOT_FOUND,
)

email = Notify(
users,
f"{title}",
UserNotificationSettingType(notification_type),
)

for paragraph in paragraphs:
email.add_paragraph(paragraph)

email.send()
return Response({"detail": "Emailen ble sendt"}, status=status.HTTP_201_CREATED)
except Exception as e:
print(e)
return Response(
{"detail": "Det oppsto en feil under sending av email"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

0 comments on commit 527d0c2

Please sign in to comment.