140 lines
5.4 KiB
Python
140 lines
5.4 KiB
Python
|
|
"""Tests for env-only account configuration.
|
||
|
|
|
||
|
|
Two properties matter more than the parsing details: a malformed or missing setting is a
|
||
|
|
startup failure rather than a quiet half-configuration, and the approved folder scope
|
||
|
|
(Decyzja (e)) is the default so a working config is short.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import date
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from mail_imap_sync.config import (
|
||
|
|
ConfigError,
|
||
|
|
account_from_env,
|
||
|
|
accounts_from_env,
|
||
|
|
archive_root_from_env,
|
||
|
|
)
|
||
|
|
|
||
|
|
BASE = {
|
||
|
|
"MAIL_ACCOUNTS": "gmail,fastmail",
|
||
|
|
"MAIL_GMAIL_USER": "u@gmail.com",
|
||
|
|
"MAIL_GMAIL_APP_PASSWORD": "pw1",
|
||
|
|
"MAIL_FASTMAIL_USER": "u@fastmail.com",
|
||
|
|
"MAIL_FASTMAIL_APP_PASSWORD": "pw2",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def env(**overrides) -> dict:
|
||
|
|
merged = dict(BASE)
|
||
|
|
merged.update(overrides)
|
||
|
|
return {k: v for k, v in merged.items() if v is not None}
|
||
|
|
|
||
|
|
|
||
|
|
class TestDefaults:
|
||
|
|
def test_gmail_defaults_to_the_special_use_all_folder(self):
|
||
|
|
account = account_from_env("gmail", env())
|
||
|
|
assert account.special_use == ("\\All",)
|
||
|
|
assert account.folders == ()
|
||
|
|
|
||
|
|
def test_fastmail_defaults_to_inbox_archive_sent(self):
|
||
|
|
account = account_from_env("fastmail", env())
|
||
|
|
assert account.folders == ("INBOX", "Archive", "Sent")
|
||
|
|
assert account.special_use == ()
|
||
|
|
|
||
|
|
def test_spam_trash_and_drafts_are_not_in_any_default_scope(self):
|
||
|
|
for name in ("gmail", "fastmail"):
|
||
|
|
scope = account_from_env(name, env()).folders
|
||
|
|
assert not {"Spam", "Trash", "Drafts"} & set(scope)
|
||
|
|
|
||
|
|
def test_known_hosts_are_defaults_not_requirements(self):
|
||
|
|
assert account_from_env("gmail", env()).host == "imap.gmail.com"
|
||
|
|
assert account_from_env("gmail", env(MAIL_GMAIL_HOST="mail.example.com")).host \
|
||
|
|
== "mail.example.com"
|
||
|
|
|
||
|
|
def test_first_tick_defaults_to_new_only(self):
|
||
|
|
assert account_from_env("fastmail", env()).initial_mode == "new-only"
|
||
|
|
|
||
|
|
|
||
|
|
class TestOverrides:
|
||
|
|
def test_folders_override_replaces_the_default_list(self):
|
||
|
|
account = account_from_env("fastmail", env(MAIL_FASTMAIL_FOLDERS="INBOX,Projects"))
|
||
|
|
assert account.folders == ("INBOX", "Projects")
|
||
|
|
|
||
|
|
def test_empty_override_means_none_not_fall_back_to_default(self):
|
||
|
|
account = account_from_env("gmail", env(MAIL_GMAIL_SPECIAL_USE="",
|
||
|
|
MAIL_GMAIL_FOLDERS="INBOX"))
|
||
|
|
assert account.special_use == ()
|
||
|
|
assert account.folders == ("INBOX",)
|
||
|
|
|
||
|
|
def test_since_mode_parses_the_date(self):
|
||
|
|
account = account_from_env("gmail", env(MAIL_GMAIL_INITIAL_MODE="since",
|
||
|
|
MAIL_GMAIL_INITIAL_SINCE="2026-06-15"))
|
||
|
|
assert account.initial_since == date(2026, 6, 15)
|
||
|
|
|
||
|
|
def test_port_override(self):
|
||
|
|
assert account_from_env("gmail", env(MAIL_GMAIL_PORT="1993")).port == 1993
|
||
|
|
|
||
|
|
def test_archive_root_override(self):
|
||
|
|
assert str(archive_root_from_env({"MAIL_ARCHIVE_ROOT": "/srv/mail"})) == "/srv/mail"
|
||
|
|
|
||
|
|
|
||
|
|
class TestFailsLoudly:
|
||
|
|
def test_missing_accounts_list(self):
|
||
|
|
with pytest.raises(ConfigError, match="MAIL_ACCOUNTS"):
|
||
|
|
accounts_from_env({})
|
||
|
|
|
||
|
|
def test_missing_password(self):
|
||
|
|
with pytest.raises(ConfigError, match="APP_PASSWORD"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_APP_PASSWORD=None))
|
||
|
|
|
||
|
|
def test_missing_user(self):
|
||
|
|
with pytest.raises(ConfigError, match="USER"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_USER=None))
|
||
|
|
|
||
|
|
def test_unknown_account_without_a_host(self):
|
||
|
|
with pytest.raises(ConfigError, match="HOST"):
|
||
|
|
account_from_env("mailbox-org", {"MAIL_MAILBOX_ORG_USER": "u",
|
||
|
|
"MAIL_MAILBOX_ORG_APP_PASSWORD": "p"})
|
||
|
|
|
||
|
|
def test_unknown_account_with_a_host_but_no_scope(self):
|
||
|
|
with pytest.raises(ConfigError, match="no scope"):
|
||
|
|
account_from_env("mailbox-org", {"MAIL_MAILBOX_ORG_USER": "u",
|
||
|
|
"MAIL_MAILBOX_ORG_APP_PASSWORD": "p",
|
||
|
|
"MAIL_MAILBOX_ORG_HOST": "imap.mailbox.org"})
|
||
|
|
|
||
|
|
def test_bad_port(self):
|
||
|
|
with pytest.raises(ConfigError, match="PORT"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_PORT="993x"))
|
||
|
|
|
||
|
|
def test_bad_initial_mode(self):
|
||
|
|
with pytest.raises(ConfigError, match="INITIAL_MODE"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_INITIAL_MODE="yesterday"))
|
||
|
|
|
||
|
|
def test_since_mode_without_a_date(self):
|
||
|
|
with pytest.raises(ConfigError, match="initial_since"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_INITIAL_MODE="since"))
|
||
|
|
|
||
|
|
def test_bad_since_date(self):
|
||
|
|
with pytest.raises(ConfigError, match="not a date"):
|
||
|
|
account_from_env("gmail", env(MAIL_GMAIL_INITIAL_MODE="since",
|
||
|
|
MAIL_GMAIL_INITIAL_SINCE="15/06/2026"))
|
||
|
|
|
||
|
|
def test_duplicate_account_name(self):
|
||
|
|
with pytest.raises(ConfigError, match="twice"):
|
||
|
|
accounts_from_env(env(MAIL_ACCOUNTS="gmail,gmail"))
|
||
|
|
|
||
|
|
|
||
|
|
class TestAccountsFromEnv:
|
||
|
|
def test_returns_accounts_in_the_listed_order(self):
|
||
|
|
accounts = accounts_from_env(env(MAIL_ACCOUNTS="fastmail,gmail"))
|
||
|
|
assert [a.name for a in accounts] == ["fastmail", "gmail"]
|
||
|
|
|
||
|
|
def test_a_single_account_is_a_valid_config(self):
|
||
|
|
accounts = accounts_from_env(env(MAIL_ACCOUNTS="gmail"))
|
||
|
|
assert [a.name for a in accounts] == ["gmail"]
|
||
|
|
|
||
|
|
def test_account_name_is_the_envelope_source(self):
|
||
|
|
assert accounts_from_env(env())[0].name == "gmail"
|