From aa746b780dbb68cb3521e3343e39283c69e2a857 Mon Sep 17 00:00:00 2001 From: Lyra Thorpe Date: Wed, 17 Jun 2026 16:14:22 +0100 Subject: [PATCH] test: add pytest coverage and run tests in CI --- .gitea/workflows/build-and-publish.yaml | 9 ++ README.md | 10 ++ requirements.txt | 1 + tests/test_proxy_server.py | 121 ++++++++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 tests/test_proxy_server.py diff --git a/.gitea/workflows/build-and-publish.yaml b/.gitea/workflows/build-and-publish.yaml index 604bc71..ca17cbd 100644 --- a/.gitea/workflows/build-and-publish.yaml +++ b/.gitea/workflows/build-and-publish.yaml @@ -40,7 +40,16 @@ jobs: # Full history and tags are required to derive the next version # from the conventional-commit messages since the last release. fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install test dependencies + run: python -m pip install --upgrade pip && pip install -r requirements.txt + + - name: Run unit tests + run: pytest -q - name: Determine registry host run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV" diff --git a/README.md b/README.md index 841525a..0a89b3e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,16 @@ docker run --rm -p 110:110 -p 25:25 \ legacy-email-proxy ``` +## Tests + +Run tests locally with: + +```bash +python -m pip install --upgrade pip +pip install -r requirements.txt +pytest -q +``` + ## Notes This implementation begins the proxy with a minimal POP3 command set and SMTP delivery path. It is designed to start development on the required application architecture. diff --git a/requirements.txt b/requirements.txt index 6322c79..595c94e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ aiosmtpd>=1.6.3 +pytest>=8.0.0 diff --git a/tests/test_proxy_server.py b/tests/test_proxy_server.py new file mode 100644 index 0000000..02f349b --- /dev/null +++ b/tests/test_proxy_server.py @@ -0,0 +1,121 @@ +import imaplib +import smtplib + +import pytest + +from proxy_server import IMAPBackend, SMTPProxyHandler, Settings, env_bool + + +class DummyIMAP: + def __init__(self, host, port): + self.host = host + self.port = port + self.logged_in = False + self.selected = None + + def login(self, username, password): + self.logged_in = True + + def select(self, mailbox): + self.selected = mailbox + + def uid(self, command, *args): + if command == "search": + return "OK", [b"1 2 3"] + if command == "fetch" and args[1] == "(RFC822.SIZE)": + return "OK", [(b"1 (RFC822.SIZE 1024)", b"")] + if command == "fetch" and args[1] == "(RFC822)": + return "OK", [b"1 (RFC822 {10}", b"Hello", b" World", b")"] + return "NO", [] + + def logout(self): + pass + + def expunge(self): + pass + + +def test_env_bool_interprets_truthy_values(monkeypatch): + monkeypatch.setenv("FEATURE_ENABLED", "true") + assert env_bool("FEATURE_ENABLED") is True + monkeypatch.setenv("FEATURE_ENABLED", "1") + assert env_bool("FEATURE_ENABLED") is True + monkeypatch.setenv("FEATURE_ENABLED", "off") + assert env_bool("FEATURE_ENABLED") is False + monkeypatch.delenv("FEATURE_ENABLED", raising=False) + assert env_bool("FEATURE_ENABLED", default=True) is True + + +def test_settings_validate_requires_backend_hosts(): + original_imap = Settings.BACKEND_IMAP_HOST + original_smtp = Settings.BACKEND_SMTP_HOST + Settings.BACKEND_IMAP_HOST = None + Settings.BACKEND_SMTP_HOST = None + with pytest.raises(RuntimeError, match="Missing required environment variables"): + Settings.validate() + Settings.BACKEND_IMAP_HOST = original_imap + Settings.BACKEND_SMTP_HOST = original_smtp + + +def test_settings_validate_succeeds_with_backends(): + original_imap = Settings.BACKEND_IMAP_HOST + original_smtp = Settings.BACKEND_SMTP_HOST + Settings.BACKEND_IMAP_HOST = "imap.example.com" + Settings.BACKEND_SMTP_HOST = "smtp.example.com" + Settings.validate() + Settings.BACKEND_IMAP_HOST = original_imap + Settings.BACKEND_SMTP_HOST = original_smtp + + +def test_imap_backend_can_login_and_fetch(monkeypatch): + monkeypatch.setattr(imaplib, "IMAP4_SSL", lambda host, port: DummyIMAP(host, port)) + backend = IMAPBackend("user", "pass") + backend.login() + assert backend.connection.logged_in + assert backend.connection.selected == "INBOX" + assert backend.list_uids() == [b"1", b"2", b"3"] + assert backend.fetch_message_size(b"1") == 1024 + assert b"Hello" in backend.fetch_message(b"1") + + +def test_smtp_proxy_handler_forwards_message_over_ssl(monkeypatch): + captured = {} + + class DummySMTP: + def __init__(self, host, port, timeout=0): + captured["instance"] = self + self.host = host + self.port = port + self.timeout = timeout + self.logged_in = False + self.sent = None + + def login(self, user, password): + self.logged_in = True + + def sendmail(self, sender, recipients, message): + self.sent = (sender, recipients, message) + + def quit(self): + captured["quit"] = True + + monkeypatch.setattr(smtplib, "SMTP_SSL", DummySMTP) + previous_ssl = Settings.BACKEND_SMTP_USE_SSL + previous_user = Settings.BACKEND_SMTP_USER + previous_pass = Settings.BACKEND_SMTP_PASS + Settings.BACKEND_SMTP_USE_SSL = True + Settings.BACKEND_SMTP_USER = "smtp-user" + Settings.BACKEND_SMTP_PASS = "smtp-pass" + + handler = SMTPProxyHandler() + handler.send_message("from@example.com", ["to@example.com"], b"Subject: Test\r\n\r\nBody\r\n") + + assert captured["instance"].host == Settings.BACKEND_SMTP_HOST + assert captured["instance"].logged_in is True + assert captured["instance"].sent[0] == "from@example.com" + assert captured["instance"].sent[1] == ["to@example.com"] + assert b"Subject: Test" in captured["instance"].sent[2] + + Settings.BACKEND_SMTP_USE_SSL = previous_ssl + Settings.BACKEND_SMTP_USER = previous_user + Settings.BACKEND_SMTP_PASS = previous_pass