"""Тесты чтения настроек из переменных окружения.""" import os import tempfile import unittest from splitapi.config import Config, ConfigError, from_env BASE = {"PUBLIC_URL": "https://trainer.example.com"} DEV_MAIL = {**BASE, "MAIL_BACKEND": "console"} # явное согласие на режим «письма в лог» class ConfigTests(unittest.TestCase): def test_minimal_local_config_has_safe_defaults(self): cfg = from_env({"PUBLIC_URL": "http://127.0.0.1:8080"}) self.assertEqual(cfg.bind, "127.0.0.1") self.assertEqual(cfg.port, 8000) self.assertEqual(cfg.mail_backend, "console") self.assertTrue(cfg.allow_registration) self.assertFalse(cfg.trust_proxy) def test_public_url_is_required_and_validated(self): for env in [{}, {"PUBLIC_URL": ""}, {"PUBLIC_URL": "ftp://x.org"}, {"PUBLIC_URL": "not a url"}, {"PUBLIC_URL": "https://x.org/?q=1"}, {"PUBLIC_URL": "https://x.org/#frag"}, {"PUBLIC_URL": "https://"}, {"PUBLIC_URL": "https://user:pw@x.org"}]: with self.subTest(env=env), self.assertRaises(ConfigError): from_env({**env, "MAIL_BACKEND": "console"}) def test_derived_urls(self): cfg = from_env({**DEV_MAIL, "PUBLIC_URL": "https://example.com/trainer"}) self.assertEqual(cfg.origin, "https://example.com") self.assertEqual(cfg.base_url, "https://example.com/trainer/") self.assertTrue(cfg.secure_cookie) def test_http_url_gives_non_secure_cookie_and_keeps_port_in_origin(self): cfg = from_env({"PUBLIC_URL": "http://127.0.0.1:8080/"}) self.assertEqual(cfg.origin, "http://127.0.0.1:8080") self.assertEqual(cfg.base_url, "http://127.0.0.1:8080/") self.assertFalse(cfg.secure_cookie) def test_public_url_is_normalized_like_a_browser_origin(self): cfg = from_env({**DEV_MAIL, "PUBLIC_URL": "https://Example.COM:443/app/"}) self.assertEqual((cfg.public_url, cfg.origin), ("https://example.com/app", "https://example.com")) self.assertEqual(from_env({**DEV_MAIL, "PUBLIC_URL": "http://example.com:80"}).origin, "http://example.com") self.assertEqual(from_env({**DEV_MAIL, "PUBLIC_URL": "https://example.com:8443"}).origin, "https://example.com:8443") def test_international_domain_is_stored_as_punycode_like_browsers_send_it_in_origin(self): cfg = from_env({**DEV_MAIL, "PUBLIC_URL": "https://тренер.рф/app"}) self.assertEqual(cfg.origin, "https://xn--e1aaxkdh.xn--p1ai") self.assertEqual(cfg.public_url, "https://xn--e1aaxkdh.xn--p1ai/app") def test_cookie_name_uses_host_prefix_only_over_https(self): self.assertEqual(from_env(DEV_MAIL).cookie_name, "__Host-session") self.assertEqual(from_env({"PUBLIC_URL": "http://127.0.0.1:8080"}).cookie_name, "session") def test_smtp_host_switches_backend_to_smtp(self): cfg = from_env({**BASE, "SMTP_HOST": "smtp.example.com", "SMTP_USER": "u", "SMTP_PASSWORD": "p"}) self.assertEqual(cfg.mail_backend, "smtp") self.assertEqual((cfg.smtp_host, cfg.smtp_port, cfg.smtp_security), ("smtp.example.com", 587, "starttls")) def test_console_mailer_is_not_the_silent_default_on_a_public_site(self): with self.assertRaises(ConfigError) as ctx: from_env(BASE) self.assertIn("SMTP", str(ctx.exception)) for local in ("http://localhost:8080", "http://127.0.0.1", "http://[::1]:8080"): with self.subTest(local=local): self.assertEqual(from_env({"PUBLIC_URL": local}).mail_backend, "console") def test_explicit_backend_wins_and_is_validated(self): self.assertEqual(from_env({**BASE, "SMTP_HOST": "h", "MAIL_BACKEND": "console"}).mail_backend, "console") with self.assertRaises(ConfigError): from_env({**BASE, "MAIL_BACKEND": "carrier-pigeon"}) with self.assertRaises(ConfigError): from_env({**BASE, "MAIL_BACKEND": "smtp"}) # без SMTP_HOST def test_smtp_security_validated(self): with self.assertRaises(ConfigError): from_env({**BASE, "SMTP_HOST": "h", "SMTP_SECURITY": "magic"}) self.assertEqual(from_env({**BASE, "SMTP_HOST": "h", "SMTP_SECURITY": "ssl", "SMTP_PORT": "465"}).smtp_port, 465) def test_password_from_file_for_docker_secrets(self): with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f: f.write("file-secret\n") try: cfg = from_env({**BASE, "SMTP_HOST": "h", "SMTP_PASSWORD_FILE": f.name}) self.assertEqual(cfg.smtp_password, "file-secret") finally: os.unlink(f.name) def test_missing_password_file_is_an_error(self): with self.assertRaises(ConfigError): from_env({**BASE, "SMTP_HOST": "h", "SMTP_PASSWORD_FILE": "/nonexistent/secret"}) def test_booleans_and_numbers(self): cfg = from_env({**DEV_MAIL, "ALLOW_REGISTRATION": "false", "TRUST_PROXY": "1", "PORT": "9000", "SESSION_DAYS": "7"}) self.assertFalse(cfg.allow_registration) self.assertTrue(cfg.trust_proxy) self.assertEqual((cfg.port, cfg.session_days), (9000, 7)) def test_invalid_values_rejected(self): for env in [{"PORT": "0"}, {"PORT": "99999"}, {"PORT": "abc"}, {"SESSION_DAYS": "0"}, {"ALLOW_REGISTRATION": "maybe"}]: with self.subTest(env=env), self.assertRaises(ConfigError): from_env({**DEV_MAIL, **env}) def test_empty_values_fall_back_to_defaults(self): """docker compose передаёт незаданные переменные пустыми строками.""" cfg = from_env({**DEV_MAIL, "PORT": "", "SESSION_DAYS": "", "SMTP_PORT": "", "SMTP_SECURITY": "", "MAIL_FROM": ""}) self.assertEqual((cfg.port, cfg.session_days, cfg.smtp_port, cfg.smtp_security), (8000, 30, 587, "starttls")) def test_default_sender_uses_site_host(self): self.assertIn("noreply@trainer.example.com", from_env(DEV_MAIL).mail_from) def test_config_is_immutable(self): cfg = from_env(DEV_MAIL) with self.assertRaises(Exception): cfg.port = 1 # type: ignore[misc] self.assertIsInstance(cfg, Config) if __name__ == "__main__": unittest.main()