erplibre/test/test_mail_accounts.py

234 lines
7.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import json
import os
import stat
import tempfile
import unittest
from pathlib import Path
from script.todo.mail.accounts import (
PRESETS,
Account,
AccountError,
account_from_preset,
find,
load,
save,
write_template,
)
class TestPresets(unittest.TestCase):
def test_four_presets(self):
self.assertEqual(
set(PRESETS), {"gmail", "outlook", "icloud", "generic"}
)
def test_gmail_servers(self):
self.assertEqual(PRESETS["gmail"]["imap"]["host"], "imap.gmail.com")
self.assertEqual(PRESETS["gmail"]["imap"]["port"], 993)
self.assertEqual(PRESETS["gmail"]["smtp"]["host"], "smtp.gmail.com")
self.assertEqual(PRESETS["gmail"]["smtp"]["port"], 587)
def test_security_values_are_known(self):
for key, preset in PRESETS.items():
for proto in ("imap", "smtp"):
self.assertIn(
preset[proto]["security"],
("ssl", "starttls", "none"),
f"{key}.{proto}",
)
def test_app_password_flag(self):
self.assertTrue(PRESETS["gmail"]["app_password"])
self.assertTrue(PRESETS["icloud"]["app_password"])
self.assertFalse(PRESETS["generic"]["app_password"])
class TestAccountFromPreset(unittest.TestCase):
def test_fills_servers_and_user(self):
acc = account_from_preset("perso", "moi@gmail.com", "gmail")
self.assertEqual(acc.imap.host, "imap.gmail.com")
self.assertEqual(acc.imap.user, "moi@gmail.com")
self.assertEqual(acc.smtp.user, "moi@gmail.com")
def test_user_override(self):
acc = account_from_preset(
"perso", "moi@x.ca", "generic", user="login-different"
)
self.assertEqual(acc.imap.user, "login-different")
def test_secret_ref_defaults_to_kdbx(self):
acc = account_from_preset("perso", "moi@x.ca", "generic")
self.assertEqual(acc.secret_ref, "kdbx:ERPLibre/Mail/perso")
def test_secret_ref_keyring(self):
acc = account_from_preset(
"perso", "moi@x.ca", "generic", vault="keyring"
)
self.assertEqual(acc.secret_ref, "keyring:perso")
def test_cache_key_ref(self):
acc = account_from_preset("perso", "moi@x.ca", "generic")
self.assertEqual(
acc.cache_key_ref(), "kdbx:ERPLibre/Mail/perso/cache-key"
)
def test_cache_mode_inherits_by_default(self):
self.assertIsNone(
account_from_preset("perso", "moi@x.ca", "generic").cache_mode
)
def test_unknown_preset_raises(self):
with self.assertRaises(AccountError):
account_from_preset("perso", "moi@x.ca", "aol")
def test_empty_name_raises(self):
with self.assertRaises(AccountError):
account_from_preset("", "moi@x.ca", "generic")
def test_name_with_slash_raises(self):
"""Le nom sert de segment de chemin et de référence kdbx."""
with self.assertRaises(AccountError):
account_from_preset("per/so", "moi@x.ca", "generic")
class TestRoundtrip(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.path = Path(self.tmp.name) / "accounts.json"
def tearDown(self):
self.tmp.cleanup()
def test_save_then_load(self):
acc = account_from_preset("perso", "moi@gmail.com", "gmail")
save([acc], self.path)
loaded = load(self.path)
self.assertEqual(len(loaded), 1)
self.assertEqual(loaded[0].to_dict(), acc.to_dict())
def test_file_is_0600(self):
save([account_from_preset("perso", "moi@x.ca", "generic")], self.path)
mode = stat.S_IMODE(os.stat(self.path).st_mode)
self.assertEqual(mode, 0o600)
def test_parent_dir_is_0700(self):
nested = Path(self.tmp.name) / "mail" / "accounts.json"
save([account_from_preset("perso", "moi@x.ca", "generic")], nested)
mode = stat.S_IMODE(os.stat(nested.parent).st_mode)
self.assertEqual(mode, 0o700)
def test_no_window_at_the_process_umask(self):
"""`write_text` puis `chmod` laisserait le fichier lisible à l'umask
du process le temps entre les deux appels. Au moment où `chmod` est
appelé, le fichier doit déjà être en 0600 — la preuve qu'il n'a
jamais existé autrement."""
from unittest.mock import patch
seen = []
original_chmod = os.chmod
def spy(path, mode):
if Path(path) == self.path:
seen.append(stat.S_IMODE(os.stat(path).st_mode))
return original_chmod(path, mode)
with patch("os.chmod", side_effect=spy):
save(
[account_from_preset("perso", "moi@x.ca", "generic")],
self.path,
)
self.assertEqual(seen, [0o600])
def test_load_missing_file_returns_empty(self):
self.assertEqual(load(Path(self.tmp.name) / "absent.json"), [])
def test_no_password_key_is_written(self):
save([account_from_preset("perso", "moi@x.ca", "generic")], self.path)
raw = self.path.read_text()
self.assertNotIn("password", raw.lower())
def test_corrupt_json_raises(self):
self.path.write_text("{ pas du json")
with self.assertRaises(AccountError):
load(self.path)
def test_duplicate_name_raises_on_save(self):
acc = account_from_preset("perso", "moi@x.ca", "generic")
with self.assertRaises(AccountError):
save([acc, acc], self.path)
def test_find(self):
accs = [
account_from_preset("perso", "a@x.ca", "generic"),
account_from_preset("travail", "b@x.ca", "generic"),
]
self.assertEqual(find(accs, "travail").email, "b@x.ca")
self.assertIsNone(find(accs, "absent"))
class TestTemplate(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.path = Path(self.tmp.name) / "accounts.json"
def tearDown(self):
self.tmp.cleanup()
def test_writes_valid_json(self):
write_template(self.path)
data = json.loads(self.path.read_text())
self.assertEqual(data["version"], 1)
def test_has_one_example_per_preset(self):
write_template(self.path)
data = json.loads(self.path.read_text())
presets = {a["preset"] for a in data["accounts"]}
self.assertEqual(presets, set(PRESETS))
def test_examples_are_disabled(self):
"""Un modèle ne doit rien tenter de synchroniser tel quel."""
write_template(self.path)
data = json.loads(self.path.read_text())
self.assertTrue(all(not a["enabled"] for a in data["accounts"]))
def test_carries_comments(self):
write_template(self.path)
data = json.loads(self.path.read_text())
self.assertIn("_comment", data)
def test_refuses_to_overwrite(self):
self.path.write_text("{}")
with self.assertRaises(AccountError):
write_template(self.path)
def test_force_overwrites(self):
self.path.write_text("{}")
write_template(self.path, force=True)
self.assertIn("accounts", json.loads(self.path.read_text()))
def test_no_window_at_the_process_umask(self):
from unittest.mock import patch
seen = []
original_chmod = os.chmod
def spy(path, mode):
if Path(path) == self.path:
seen.append(stat.S_IMODE(os.stat(path).st_mode))
return original_chmod(path, mode)
with patch("os.chmod", side_effect=spy):
write_template(self.path)
self.assertEqual(seen, [0o600])
if __name__ == "__main__":
unittest.main()