Skip to content

Commit

Permalink
Add notifications tests, that test the various permutations of plaint…
Browse files Browse the repository at this point in the history
…ext/encryption
  • Loading branch information
micahflee committed Feb 21, 2025
1 parent 8c4bb2e commit dec7992
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 11 deletions.
6 changes: 6 additions & 0 deletions hushline/routes/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,23 @@ def submit_message(username: str) -> Response | str:
if uname.user.email_encrypt_entire_body:
if form.encrypted_email_body.data.startswith("-----BEGIN PGP MESSAGE-----"):
email_body = form.encrypted_email_body.data
current_app.logger.debug("Sending email with encrypted body")
else:
# If the body is not encrypted, we should not send it
email_body = plaintext_new_message_body
current_app.logger.debug(
"Email body is not encrypted, sending email with generic body"
)
else:
# If we don't want to encrypt the entire body, or if client-side encryption
# of the body failed
email_body = ""
for name, value in extracted_fields:
email_body += f"\n\n{name}\n\n{value}\n\n=============="
current_app.logger.debug("Sending email with unencrypted body")
else:
email_body = plaintext_new_message_body
current_app.logger.debug("Sending email with generic body")

do_send_email(uname.user, email_body.strip())

Expand Down
13 changes: 13 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import string
from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, TypeVar

from flask import url_for
from flask.testing import FlaskClient
from flask_wtf import FlaskForm

T = TypeVar("T")
Expand Down Expand Up @@ -57,3 +59,14 @@ def __ne__(self, other: object) -> bool:
# ridiculous formatting because `ruff` won't allow `not (x == y)`
assert (Missing() == Missing()) ^ bool("x")
assert Missing() != Missing()


def get_captcha_from_session(client: FlaskClient, username: str) -> str:
# Simulate loading the profile page to generate and retrieve the CAPTCHA from the session
response = client.get(url_for("profile", username=username))
assert response.status_code == 200

with client.session_transaction() as session:
captcha_answer = session.get("math_answer")
assert captcha_answer
return captcha_answer
237 changes: 237 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
from unittest.mock import MagicMock, patch

import pytest
from flask import url_for
from flask.testing import FlaskClient
from helpers import get_captcha_from_session

from hushline.db import db
from hushline.model import Message, User

msg_contact_method = "I prefer Signal."
msg_content = "This is a test message."

pgp_message_sig = "-----BEGIN PGP MESSAGE-----\n\n"
plaintext_new_message_body = "You have a new Hush Line message! Please log in to read it."


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
@patch("hushline.routes.profile.do_send_email")
def test_notifications_disabled(
mock_do_send_email: MagicMock, client: FlaskClient, user: User
) -> None:
# Disable email notifications
user.enable_email_notifications = False
db.session.commit()

response = client.post(
url_for("profile", username=user.primary_username.username),
data={
"field_0": msg_contact_method,
"field_1": msg_content,
"captcha_answer": get_captcha_from_session(client, user.primary_username.username),
},
follow_redirects=True,
)
assert response.status_code == 200, response.text
assert "Message submitted successfully." in response.text

message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert len(message.field_values) == 2
for field_value in message.field_values:
assert pgp_message_sig in field_value.value

# Check if do_send_email was not called
mock_do_send_email.assert_not_called()

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
@patch("hushline.routes.profile.do_send_email")
def test_notifications_enabled_no_content(
mock_do_send_email: MagicMock, client: FlaskClient, user: User
) -> None:
# Enable email notifications, with no message content
user.enable_email_notifications = True
user.email_include_message_content = False
db.session.commit()

response = client.post(
url_for("profile", username=user.primary_username.username),
data={
"field_0": msg_contact_method,
"field_1": msg_content,
"captcha_answer": get_captcha_from_session(client, user.primary_username.username),
},
follow_redirects=True,
)
assert response.status_code == 200, response.text
assert "Message submitted successfully." in response.text

message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert len(message.field_values) == 2
for field_value in message.field_values:
assert pgp_message_sig in field_value.value

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text

# Check if do_send_email was called
mock_do_send_email.assert_called_once_with(user, plaintext_new_message_body)

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
@patch("hushline.routes.profile.do_send_email")
def test_notifications_enabled_yes_content_no_encrypted_body(
mock_do_send_email: MagicMock, client: FlaskClient, user: User
) -> None:
# Enable email notifications, with no message content
user.enable_email_notifications = True
user.email_include_message_content = True
user.email_encrypt_entire_body = False
db.session.commit()

response = client.post(
url_for("profile", username=user.primary_username.username),
data={
"field_0": msg_contact_method,
"field_1": msg_content,
"captcha_answer": get_captcha_from_session(client, user.primary_username.username),
},
follow_redirects=True,
)
assert response.status_code == 200, response.text
assert "Message submitted successfully." in response.text

message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert len(message.field_values) == 2
for field_value in message.field_values:
assert pgp_message_sig in field_value.value

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text

# Check if do_send_email was called
mock_do_send_email.assert_called_once()

# Check if the body contains the message content, mix of plaintext and ciphertext
args, _ = mock_do_send_email.call_args
assert "Contact Method" in args[1]
assert "Message" in args[1]
assert pgp_message_sig in args[1]

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
@patch("hushline.routes.profile.do_send_email")
def test_notifications_enabled_yes_content_yes_encrypted_body(
mock_do_send_email: MagicMock, client: FlaskClient, user: User
) -> None:
# Enable email notifications, with no message content
user.enable_email_notifications = True
user.email_include_message_content = True
user.email_encrypt_entire_body = True
db.session.commit()

encrypted_email_body = (
"-----BEGIN PGP MESSAGE-----\n\nfake encrypted body\n\n-----END PGP MESSAGE-----"
)

response = client.post(
url_for("profile", username=user.primary_username.username),
data={
"encrypted_email_body": encrypted_email_body,
"field_0": msg_contact_method,
"field_1": msg_content,
"captcha_answer": get_captcha_from_session(client, user.primary_username.username),
},
follow_redirects=True,
)
assert response.status_code == 200, response.text
assert "Message submitted successfully." in response.text

message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert len(message.field_values) == 2
for field_value in message.field_values:
assert pgp_message_sig in field_value.value

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text

# Check if do_send_email was called with encrypted email body
mock_do_send_email.assert_called_once_with(user, encrypted_email_body)

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
@patch("hushline.routes.profile.do_send_email")
def test_notifications_enabled_yes_content_yes_encrypted_body_failed_client_encryption(
mock_do_send_email: MagicMock, client: FlaskClient, user: User
) -> None:
# Enable email notifications, with no message content
user.enable_email_notifications = True
user.email_include_message_content = True
user.email_encrypt_entire_body = True
db.session.commit()

encrypted_email_body = ""

response = client.post(
url_for("profile", username=user.primary_username.username),
data={
"encrypted_email_body": encrypted_email_body,
"field_0": msg_contact_method,
"field_1": msg_content,
"captcha_answer": get_captcha_from_session(client, user.primary_username.username),
},
follow_redirects=True,
)
assert response.status_code == 200, response.text
assert "Message submitted successfully." in response.text

message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert len(message.field_values) == 2
for field_value in message.field_values:
assert pgp_message_sig in field_value.value

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text

# Check if do_send_email was called with plaintext message
mock_do_send_email.assert_called_once_with(user, plaintext_new_message_body)

response = client.get(url_for("message", id=message.id), follow_redirects=True)
assert response.status_code == 200
assert pgp_message_sig in response.text, response.text
12 changes: 1 addition & 11 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from bs4 import BeautifulSoup
from flask import Flask, url_for
from flask.testing import FlaskClient
from helpers import get_captcha_from_session

from hushline.db import db
from hushline.model import Message, OrganizationSetting, User, Username
Expand All @@ -14,17 +15,6 @@
pgp_message_sig = "-----BEGIN PGP MESSAGE-----\n\n"


def get_captcha_from_session(client: FlaskClient, username: str) -> str:
# Simulate loading the profile page to generate and retrieve the CAPTCHA from the session
response = client.get(url_for("profile", username=username))
assert response.status_code == 200

with client.session_transaction() as session:
captcha_answer = session.get("math_answer")
assert captcha_answer
return captcha_answer


@pytest.mark.usefixtures("_pgp_user")
def test_profile_header(client: FlaskClient, user: User) -> None:
assert (
Expand Down

0 comments on commit dec7992

Please sign in to comment.