mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
feat(testing): add email testing utilities
Changes: - util/sendmail now populates email files in the 'test-emails' directory. - util/sendmail does this in a serialized fashion based off of the test suite and name retrieved from PYTEST_CURRENT_TEST in the format: `<test_suite>_<test_function>.n.txt` where n is increased by one every time sendmail is run. - pytest conftest fixtures have been added for test email setup; it wipes out old emails for the particular test function being run. - New aurweb.testing.email.Email class allows developers to test against emails stored by util/sendmail. Simple pass the serial you want to test against, starting at serial = 1; e.g. Email(serial). Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
parent
b72bd38f76
commit
9fb1fbe32c
3 changed files with 162 additions and 1 deletions
120
aurweb/testing/email.py
Normal file
120
aurweb/testing/email.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import base64
|
||||||
|
import copy
|
||||||
|
import email
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Email:
|
||||||
|
"""
|
||||||
|
An email class used for testing.
|
||||||
|
|
||||||
|
This class targets a specific serial of emails for PYTEST_CURRENT_TEST.
|
||||||
|
As emails are sent out with util/sendmail, the serial number increases,
|
||||||
|
starting at 1.
|
||||||
|
|
||||||
|
Email content sent out by aurweb is always base64-encoded. Email.parse()
|
||||||
|
decodes that for us and puts it into Email.body.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
# Get the {test_suite}_{test_function}.1.txt email.
|
||||||
|
email = Email(1).parse()
|
||||||
|
print(email.body)
|
||||||
|
print(email.headers)
|
||||||
|
|
||||||
|
"""
|
||||||
|
TEST_DIR = "test-emails"
|
||||||
|
|
||||||
|
def __init__(self, serial: int = 1):
|
||||||
|
self.serial = serial
|
||||||
|
self.content = self._get()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def email_prefix(suite: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Get the email prefix.
|
||||||
|
|
||||||
|
We find the email prefix by reducing PYTEST_CURRENT_TEST to
|
||||||
|
either {test_suite}_{test_function}. If `suite` is set, we
|
||||||
|
reduce it to {test_suite} only.
|
||||||
|
|
||||||
|
:param suite: Reduce PYTEST_CURRENT_TEST to {test_suite}
|
||||||
|
:return: Email prefix with '/', '.', ',', and ':' chars replaced by '_'
|
||||||
|
"""
|
||||||
|
value = os.environ.get("PYTEST_CURRENT_TEST", "email").split(" ")[0]
|
||||||
|
if suite:
|
||||||
|
value = value.split(":")[0]
|
||||||
|
return re.sub(r'(\/|\.|,|:)', "_", value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def count() -> int:
|
||||||
|
"""
|
||||||
|
Count the current number of emails sent from the test.
|
||||||
|
|
||||||
|
This function is **only** supported inside of pytest functions.
|
||||||
|
Do not use it elsewhere as data races will occur.
|
||||||
|
|
||||||
|
:return: Number of emails sent by the current test
|
||||||
|
"""
|
||||||
|
files = os.listdir(Email.TEST_DIR)
|
||||||
|
prefix = Email.email_prefix()
|
||||||
|
expr = "^" + prefix + r"\.\d+\.txt$"
|
||||||
|
subset = filter(lambda e: re.match(expr, e), files)
|
||||||
|
return len(list(subset))
|
||||||
|
|
||||||
|
def _email_path(self) -> str:
|
||||||
|
filename = self.email_prefix() + f".{self.serial}.txt"
|
||||||
|
return os.path.join(Email.TEST_DIR, filename)
|
||||||
|
|
||||||
|
def _get(self) -> str:
|
||||||
|
"""
|
||||||
|
Get this email's content by reading its file.
|
||||||
|
|
||||||
|
:return: Email content
|
||||||
|
"""
|
||||||
|
path = self._email_path()
|
||||||
|
with open(path) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def parse(self) -> "Email":
|
||||||
|
"""
|
||||||
|
Parse this email and base64-decode the body.
|
||||||
|
|
||||||
|
This function populates Email.message, Email.headers and Email.body.
|
||||||
|
|
||||||
|
Additionally, after parsing, we write over our email file with
|
||||||
|
self.glue()'d content (base64-decoded). This is done for ease
|
||||||
|
of inspection by users.
|
||||||
|
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
self.message = email.message_from_string(self.content)
|
||||||
|
self.headers = dict(self.message)
|
||||||
|
|
||||||
|
# aurweb email notifications always have base64 encoded content.
|
||||||
|
# Decode it here so self.body is human readable.
|
||||||
|
self.body = base64.b64decode(self.message.get_payload()).decode()
|
||||||
|
|
||||||
|
path = self._email_path()
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(self.glue())
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def glue(self) -> str:
|
||||||
|
"""
|
||||||
|
Glue parsed content back into a complete email document, but
|
||||||
|
base64-decoded this time.
|
||||||
|
|
||||||
|
:return: Email document as a string
|
||||||
|
"""
|
||||||
|
headers = copy.copy(self.headers)
|
||||||
|
del headers["Content-Transfer-Encoding"]
|
||||||
|
|
||||||
|
output = []
|
||||||
|
for k, v in headers.items():
|
||||||
|
output.append(f"{k}: {v}")
|
||||||
|
output.append("")
|
||||||
|
output.append(self.body)
|
||||||
|
return "\n".join(output)
|
|
@ -37,6 +37,8 @@ then clears the database for each test function run in that module.
|
||||||
It is done this way because migration has a large cost; migrating
|
It is done this way because migration has a large cost; migrating
|
||||||
ahead of each function takes too long when compared to this method.
|
ahead of each function takes too long when compared to this method.
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
@ -50,6 +52,7 @@ import aurweb.config
|
||||||
import aurweb.db
|
import aurweb.db
|
||||||
|
|
||||||
from aurweb import initdb, logging, testing
|
from aurweb import initdb, logging, testing
|
||||||
|
from aurweb.testing.email import Email
|
||||||
|
|
||||||
logger = logging.get_logger(__name__)
|
logger = logging.get_logger(__name__)
|
||||||
|
|
||||||
|
@ -120,6 +123,18 @@ def _drop_database(engine: Engine, dbname: str) -> None:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_email():
|
||||||
|
if not os.path.exists(Email.TEST_DIR):
|
||||||
|
os.makedirs(Email.TEST_DIR)
|
||||||
|
|
||||||
|
# Cleanup all email files for this test suite.
|
||||||
|
prefix = Email.email_prefix(suite=True)
|
||||||
|
files = os.listdir(Email.TEST_DIR)
|
||||||
|
for file in files:
|
||||||
|
if file.startswith(prefix):
|
||||||
|
os.remove(os.path.join(Email.TEST_DIR, file))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def setup_database(tmp_path_factory: pytest.fixture,
|
def setup_database(tmp_path_factory: pytest.fixture,
|
||||||
worker_id: pytest.fixture) -> None:
|
worker_id: pytest.fixture) -> None:
|
||||||
|
@ -129,6 +144,7 @@ def setup_database(tmp_path_factory: pytest.fixture,
|
||||||
|
|
||||||
if worker_id == "master": # pragma: no cover
|
if worker_id == "master": # pragma: no cover
|
||||||
# If we're not running tests through multiproc pytest-xdist.
|
# If we're not running tests through multiproc pytest-xdist.
|
||||||
|
setup_email()
|
||||||
yield _create_database(engine, dbname)
|
yield _create_database(engine, dbname)
|
||||||
_drop_database(engine, dbname)
|
_drop_database(engine, dbname)
|
||||||
return
|
return
|
||||||
|
@ -143,12 +159,13 @@ def setup_database(tmp_path_factory: pytest.fixture,
|
||||||
else:
|
else:
|
||||||
# Otherwise, create the data file and create the database.
|
# Otherwise, create the data file and create the database.
|
||||||
fn.write_text("1")
|
fn.write_text("1")
|
||||||
|
setup_email()
|
||||||
yield _create_database(engine, dbname)
|
yield _create_database(engine, dbname)
|
||||||
_drop_database(engine, dbname)
|
_drop_database(engine, dbname)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def db_session(setup_database: pytest.fixture) -> scoped_session:
|
def db_session(setup_database: None) -> scoped_session:
|
||||||
"""
|
"""
|
||||||
Yield a database session based on aurweb.db.name().
|
Yield a database session based on aurweb.db.name().
|
||||||
|
|
||||||
|
@ -158,6 +175,7 @@ def db_session(setup_database: pytest.fixture) -> scoped_session:
|
||||||
# configured database, because PYTEST_CURRENT_TEST is removed.
|
# configured database, because PYTEST_CURRENT_TEST is removed.
|
||||||
dbname = aurweb.db.name()
|
dbname = aurweb.db.name()
|
||||||
session = aurweb.db.get_session()
|
session = aurweb.db.get_session()
|
||||||
|
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
# Close the session and pop it.
|
# Close the session and pop it.
|
||||||
|
|
|
@ -1,2 +1,25 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Send email to temporary filesystem for tests.
|
||||||
|
dir='test-emails'
|
||||||
|
filename='email.txt'
|
||||||
|
if [ ! -z ${PYTEST_CURRENT_TEST+x} ]; then
|
||||||
|
filename="$(echo $PYTEST_CURRENT_TEST | cut -d ' ' -f 1 | sed -r 's/(\/|\.|,|:)/_/g')"
|
||||||
|
fi
|
||||||
|
mkdir -p "$dir"
|
||||||
|
|
||||||
|
path="${dir}/${filename}"
|
||||||
|
serial_file="${path}.serial"
|
||||||
|
if [ ! -f $serial_file ]; then
|
||||||
|
echo 0 > $serial_file
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increment and update $serial_file.
|
||||||
|
serial=$(($(cat $serial_file) + 1))
|
||||||
|
echo $serial > $serial_file
|
||||||
|
|
||||||
|
# Use the serial we're on to mark the email file.
|
||||||
|
# Emails have the format: PYTEST_CURRENT_TEST.s.txt
|
||||||
|
# where s is the current serial for PYTEST_CURRENT_TEST.
|
||||||
|
cat > "${path}.${serial}.txt"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
Loading…
Add table
Reference in a new issue