import os import tempfile from pathlib import Path from unittest import mock import pytest from redmine_reporter.config import ( DEFAULT_REDMINE_VERIFY, AppConfig, Config, EmailConfig, ) @mock.patch.dict( os.environ, { "REDMINE_URL": "https://red.eltex.loc/", "REDMINE_USER": "test", "REDMINE_PASSWORD": "secret", }, clear=True, ) def test_config_valid_with_password_fallback(): Config.validate() # не должно быть исключения @mock.patch.dict( os.environ, { "REDMINE_URL": "https://red.eltex.loc/", "REDMINE_API_KEY": "token", }, clear=True, ) def test_config_valid_with_api_key(): Config.validate() # не должно быть исключения @mock.patch.dict(os.environ, {}, clear=True) def test_config_missing_url(): with pytest.raises(ValueError, match="REDMINE_URL"): Config.validate() @mock.patch.dict(os.environ, {"REDMINE_URL": "https://red.eltex.loc/"}, clear=True) def test_config_missing_auth(): with pytest.raises(ValueError, match="REDMINE_API_KEY"): Config.validate() @mock.patch.dict(os.environ, {"REDMINE_URL": " https://red.eltex.loc/ "}, clear=True) def test_get_redmine_url_strips_spaces_and_trailing_slash(): assert Config.get_redmine_url() == "https://red.eltex.loc" @mock.patch.dict(os.environ, {"REDMINE_AUTHOR": " Иванов И.И. "}, clear=True) def test_get_author(): assert Config.get_author("") == "Иванов И.И." assert Config.get_author("Петров П.П.") == "Петров П.П." @mock.patch.dict(os.environ, {}, clear=True) def test_get_author_fallback(): """Если ни CLI, ни .env не задали автора -- возвращается пустая строка.""" assert Config.get_author("") == "" @mock.patch.dict( os.environ, { "DEFAULT_FROM_DATE": "2026-01-01", "DEFAULT_TO_DATE": "2026-01-31", }, clear=True, ) def test_get_default_date_range_from_env(): assert Config.get_default_date_range() == "2026-01-01--2026-01-31" @mock.patch.dict( os.environ, {"DEFAULT_FROM_DATE": "2026-01-01"}, clear=True, ) def test_get_default_date_range_from_env_without_to(): """Если DEFAULT_TO_DATE не задан, конец периода — сегодня.""" from datetime import date today = date.today().isoformat() assert Config.get_default_date_range() == f"2026-01-01--{today}" @mock.patch.dict(os.environ, {}, clear=True) def test_get_default_date_range_fallback(): """Если даты не заданы -- используется текущий месяц с начала до сегодня.""" from datetime import date today = date.today() start = today.replace(day=1) result = Config.get_default_date_range() assert result == f"{start.isoformat()}--{today.isoformat()}" @mock.patch.dict(os.environ, {}, clear=True) def test_get_redmine_verify_default(): assert Config.get_redmine_verify() == DEFAULT_REDMINE_VERIFY @pytest.mark.parametrize("value", ["0", "false", "False", "no", "off"]) def test_get_redmine_verify_false_values(value): with mock.patch.dict(os.environ, {"REDMINE_VERIFY": value}, clear=True): assert Config.get_redmine_verify() is False @pytest.mark.parametrize("value", ["1", "true", "True", "yes", "on"]) def test_get_redmine_verify_true_values(value): with mock.patch.dict(os.environ, {"REDMINE_VERIFY": value}, clear=True): assert Config.get_redmine_verify() is True @mock.patch.dict(os.environ, {"REDMINE_VERIFY": "/tmp/redmine-ca.pem"}, clear=True) def test_get_redmine_verify_custom_path(): assert Config.get_redmine_verify() == "/tmp/redmine-ca.pem" # -- #54: REDMINE_URL обязан использовать HTTPS -- class TestConfigRequiresHttpsUrl: """Config.validate() отклоняет URL без TLS: API-ключ идёт в заголовках.""" @mock.patch.dict( os.environ, {"REDMINE_URL": "http://red.eltex.loc/", "REDMINE_API_KEY": "token"}, clear=True, ) def test_http_url_rejected(self): with pytest.raises(ValueError, match="HTTPS"): Config.validate() @mock.patch.dict( os.environ, {"REDMINE_URL": "red.eltex.loc", "REDMINE_API_KEY": "token"}, clear=True, ) def test_url_without_scheme_rejected(self): with pytest.raises(ValueError, match="HTTPS"): Config.validate() @mock.patch.dict( os.environ, {"REDMINE_URL": "ftp://red.eltex.loc/", "REDMINE_API_KEY": "token"}, clear=True, ) def test_non_http_scheme_rejected(self): with pytest.raises(ValueError, match="HTTPS"): Config.validate() @mock.patch.dict( os.environ, {"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_API_KEY": "token"}, clear=True, ) def test_https_url_accepted(self): Config.validate() @mock.patch.dict( os.environ, {"REDMINE_URL": "HTTPS://red.eltex.loc/", "REDMINE_API_KEY": "token"}, clear=True, ) def test_uppercase_scheme_accepted(self): Config.validate() # -- #15: .env не должен переопределять переменные окружения -- @mock.patch("dotenv.load_dotenv") def test_env_var_takes_priority_over_dotenv(mock_load): """load_dotenv вызывается с override=False — env vars не перебиваются .env.""" import importlib from redmine_reporter import config as cfg_mod importlib.reload(cfg_mod) mock_load.assert_called_once_with(override=False) # -- #35: get_redmine_password должен делать .strip() -- @mock.patch.dict(os.environ, {"REDMINE_PASSWORD": " secret123 "}, clear=True) def test_get_redmine_password_strips_whitespace(): """Пароль обрезается от whitespace, как и все остальные геттеры.""" assert Config.get_redmine_password() == "secret123" # --------------------------------------------------------------------------- # AppConfig tests — YAML config loading # --------------------------------------------------------------------------- FULL_YAML = """\ redmine: url: https://redmine.example.com/ api_key: ${MISSING_API_KEY_FOR_TEST} author: "Тестовый Автор" verify_ssl: false period: precision: datetime default_from: "2026-06-01" default_to: "2026-06-30" dynamic: true last_used: from: "2026-06-30T09:00:00" to: "2026-06-30T12:00:00" output: dir: ~/reports filename: "{author}_{from}_{to}.{ext}" default_format: xlsx email: smtp: host: smtp.example.com port: 587 user: bot@example.com password: ${MISSING_SMTP_PASSWORD_FOR_TEST} tls: true from: bot@example.com to: - boss@example.com cc: [] bcc: [] subject: "Отчёт {author} за {period}" body_text: "Во вложении отчёт." attach: true """ class TestAppConfigFromYaml: """Tests for AppConfig.from_yaml().""" def test_loads_all_sections_from_valid_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text(FULL_YAML) cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_url == "https://redmine.example.com/" assert cfg.redmine_api_key == "" # ${MISSING_API_KEY_FOR_TEST} not set assert cfg.redmine_author == "Тестовый Автор" assert cfg.redmine_verify is False assert cfg.period_precision == "datetime" assert cfg.period_default_from == "2026-06-01" assert cfg.period_default_to == "2026-06-30" assert cfg.period_dynamic is True assert cfg.period_last_used_from == "2026-06-30T09:00:00" assert cfg.period_last_used_to == "2026-06-30T12:00:00" assert cfg.output_dir == "~/reports" assert cfg.output_filename == "{author}_{from}_{to}.{ext}" assert cfg.output_default_format == "xlsx" assert cfg.email.smtp.host == "smtp.example.com" assert cfg.email.smtp.port == 587 assert cfg.email.smtp.user == "bot@example.com" assert cfg.email.smtp.password == "" assert cfg.email.smtp.tls is True assert cfg.email.from_ == "bot@example.com" assert cfg.email.to == ["boss@example.com"] assert cfg.email.subject == "Отчёт {author} за {period}" def test_partial_yaml_uses_defaults_for_missing(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n url: https://x.com/\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_url == "https://x.com/" # defaults for everything else assert cfg.redmine_author == "" assert cfg.period_precision == "date" assert cfg.output_dir == "" assert cfg.email.to == [] def test_empty_file_returns_all_defaults(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("") cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_url == "" assert cfg.redmine_api_key == "" assert cfg.period_precision == "date" def test_file_not_found_returns_all_defaults(self): cfg = AppConfig.from_yaml("/nonexistent/path/config.yml") assert cfg.redmine_url == "" @mock.patch.dict(os.environ, {"REDMINE_API_KEY": "secret-token"}, clear=True) def test_env_var_resolved_in_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n api_key: ${REDMINE_API_KEY}\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_api_key == "secret-token" def test_verify_ssl_true_returns_default_ca_path(self): """verify_ssl: true → DEFAULT_REDMINE_VERIFY (путь), не Python True.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n verify_ssl: true\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_verify == DEFAULT_REDMINE_VERIFY assert cfg.redmine_verify is not True # не бул! def test_verify_ssl_false_returns_false(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n verify_ssl: false\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_verify is False def test_missing_env_var_logs_warning(self, caplog): import logging with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n api_key: ${MISSING_KEY}\n") with caplog.at_level(logging.WARNING): cfg = AppConfig.from_yaml(yaml_path) assert cfg.redmine_api_key == "" assert "MISSING_KEY" in caplog.text def test_unknown_top_level_key_logs_warning(self, caplog): import logging with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("unknown_section:\n foo: bar\n") with caplog.at_level(logging.WARNING): AppConfig.from_yaml(yaml_path) assert "unknown_section" in caplog.text # --------------------------------------------------------------------------- # Config priority chain tests — YAML fallback # --------------------------------------------------------------------------- class TestConfigYamlFallback: """Tests that Config.get_*() falls back to YAML when env/CLI not set.""" @mock.patch.dict(os.environ, {}, clear=True) def test_url_from_yaml_when_no_env_or_cli(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n url: https://yaml-redmine.example.com/\n") Config._cli_url = None Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_redmine_url() == "https://yaml-redmine.example.com" @mock.patch.dict(os.environ, {}, clear=True) def test_cli_beats_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n url: https://yaml-redmine.example.com/\n") Config._app = AppConfig.from_yaml(yaml_path) Config.set_redmine_url("https://cli-override.example.com") assert Config.get_redmine_url() == "https://cli-override.example.com" @mock.patch.dict( os.environ, {"REDMINE_URL": "https://env-redmine.example.com/"}, clear=True ) def test_env_beats_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n url: https://yaml-redmine.example.com/\n") Config._cli_url = None Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_redmine_url() == "https://env-redmine.example.com" @mock.patch.dict(os.environ, {}, clear=True) def test_date_range_from_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n default_from: '2026-03-01'\n default_to: '2026-03-15'\n" ) Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_default_date_range() == "2026-03-01--2026-03-15" @mock.patch.dict(os.environ, {}, clear=True) def test_author_from_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("redmine:\n author: 'Автор Из Ямла'\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_author("") == "Автор Из Ямла" @mock.patch.dict(os.environ, {}, clear=True) def test_validate_passes_with_yaml_url_and_api_key(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "redmine:\n url: https://yaml.example.com/\n api_key: yaml-token\n" ) Config._cli_url = None Config._cli_api_key = None Config._app = AppConfig.from_yaml(yaml_path) # Не должно быть исключения Config.validate() @mock.patch.dict(os.environ, {}, clear=True) def test_default_date_range_env_beats_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n default_from: '2026-03-01'\n default_to: '2026-03-15'\n" ) Config._app = AppConfig.from_yaml(yaml_path) with mock.patch.dict( os.environ, {"DEFAULT_FROM_DATE": "2026-07-01", "DEFAULT_TO_DATE": "2026-07-15"}, clear=True, ): assert Config.get_default_date_range() == "2026-07-01--2026-07-15" class TestConfigPeriodPrecision: """Tests for period.precision and period.last_used.""" @mock.patch.dict(os.environ, {}, clear=True) def test_get_period_precision_defaults_to_date(self): """Без YAML-конфига precision == 'date'.""" Config._app = None assert Config.get_period_precision() == "date" @mock.patch.dict(os.environ, {}, clear=True) def test_get_period_precision_from_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("period:\n precision: datetime\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_period_precision() == "datetime" @mock.patch.dict(os.environ, {}, clear=True) def test_get_last_used_from_yaml(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n last_used:\n from: '2026-06-30T09:00:00'\n to: '2026-06-30T12:00:00'\n" ) Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_last_used_from() == "2026-06-30T09:00:00" assert Config.get_last_used_to() == "2026-06-30T12:00:00" @mock.patch.dict(os.environ, {}, clear=True) def test_get_last_used_returns_empty_when_not_set(self): Config._app = AppConfig() assert Config.get_last_used_from() == "" assert Config.get_last_used_to() == "" @mock.patch.dict(os.environ, {}, clear=True) def test_yaml_loads_last_used_field(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n last_used:\n from: '2026-07-01T08:00:00'\n to: '2026-07-01T18:00:00'\n" ) cfg = AppConfig.from_yaml(yaml_path) assert cfg.period_last_used_from == "2026-07-01T08:00:00" assert cfg.period_last_used_to == "2026-07-01T18:00:00" @mock.patch.dict(os.environ, {}, clear=True) def test_yaml_without_last_used_has_empty_fields(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("period:\n precision: date\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.period_last_used_from == "" assert cfg.period_last_used_to == "" class TestComputeNextPeriod: """Tests for compute_next_period().""" def test_full_month_goes_to_next_full_month(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2026-06-01", "2026-06-30", "date") assert nf == "2026-07-01" assert nt == "2026-07-31" def test_december_to_next_year_january(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2025-12-01", "2025-12-31", "date") assert nf == "2026-01-01" assert nt == "2026-01-31" def test_february_2026_non_leap_to_march(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2026-02-01", "2026-02-28", "date") assert nf == "2026-03-01" assert nt == "2026-03-31" def test_arbitrary_range_same_length_from_to_plus_one(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2026-06-15", "2026-06-20", "date") assert nf == "2026-06-21" assert nt == "2026-06-26" def test_single_day_range_moves_one_day(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2026-06-15", "2026-06-15", "date") assert nf == "2026-06-16" assert nt == "2026-06-16" def test_cross_month_range(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period("2026-06-25", "2026-07-05", "date") assert nf == "2026-07-06" assert nt == "2026-07-16" def test_datetime_precision_moves_by_seconds(self): from redmine_reporter.config import compute_next_period nf, nt = compute_next_period( "2026-06-30T09:00:00", "2026-06-30T12:00:00", "datetime" ) assert nf == "2026-06-30T12:00:01" assert nt == "2026-06-30T15:00:01" class TestDefaultDateRangeWithLastUsed: """Tests that get_default_date_range() uses last_used when dynamic=True.""" @mock.patch.dict(os.environ, {}, clear=True) def test_uses_last_used_when_dynamic_true(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n" " precision: date\n" " dynamic: true\n" " last_used:\n" " from: '2026-05-01'\n" " to: '2026-05-31'\n" ) Config._app = AppConfig.from_yaml(yaml_path) result = Config.get_default_date_range() assert result == "2026-06-01--2026-06-30" @mock.patch.dict(os.environ, {}, clear=True) def test_ignores_last_used_when_dynamic_false(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n" " precision: date\n" " dynamic: false\n" " default_from: '2026-03-01'\n" " default_to: '2026-03-15'\n" " last_used:\n" " from: '2026-06-01'\n" " to: '2026-06-30'\n" ) Config._app = AppConfig.from_yaml(yaml_path) result = Config.get_default_date_range() assert result == "2026-03-01--2026-03-15" @mock.patch.dict(os.environ, {}, clear=True) def test_falls_back_when_no_last_used_even_with_dynamic(self): with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n" " precision: date\n" " dynamic: true\n" " default_from: '2026-04-01'\n" " default_to: '2026-04-15'\n" ) Config._app = AppConfig.from_yaml(yaml_path) result = Config.get_default_date_range() assert result == "2026-04-01--2026-04-15" @mock.patch.dict(os.environ, {}, clear=True) def test_falls_back_to_today_when_default_to_missing(self): """Если default_from задан, а default_to нет — конец периода сегодня.""" from datetime import date with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "period:\n" " precision: date\n" " dynamic: false\n" " default_from: '2026-04-01'\n" ) Config._app = AppConfig.from_yaml(yaml_path) today = date.today().isoformat() result = Config.get_default_date_range() assert result == f"2026-04-01--{today}" class TestGetEmailConfig: """Tests for Config.get_email_config().""" @mock.patch.dict(os.environ, {}, clear=True) def test_get_email_config_from_yaml(self): """get_email_config() возвращает EmailConfig из YAML.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "email:\n" " smtp:\n" " host: smtp.example.com\n" " port: 587\n" " user: bot@example.com\n" " password: secret\n" " tls: true\n" " from: bot@example.com\n" " to:\n" " - boss@example.com\n" ) Config._app = AppConfig.from_yaml(yaml_path) cfg = Config.get_email_config() assert cfg is not None assert cfg.smtp.host == "smtp.example.com" assert cfg.smtp.port == 587 assert cfg.smtp.user == "bot@example.com" assert cfg.smtp.password == "secret" assert cfg.smtp.tls is True assert cfg.from_ == "bot@example.com" assert cfg.to == ["boss@example.com"] @mock.patch.dict(os.environ, {}, clear=True) def test_get_email_config_returns_none_when_no_yaml(self): """Без YAML-конфига get_email_config() возвращает None.""" Config._app = None assert Config.get_email_config() is None @mock.patch.dict(os.environ, {}, clear=True) def test_get_email_config_returns_none_when_no_host(self): """С YAML но без smtp.host — возвращает None.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("email:\n smtp:\n host: ''\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_email_config() is None class TestEmailConfigHtml: """Tests for EmailConfig.html field.""" def test_email_config_html_from_yaml(self): """email.html: true загружается из YAML.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "email:\n html: true\n smtp:\n host: smtp.example.com\n" ) cfg = AppConfig.from_yaml(yaml_path) assert cfg.email.html is True def test_email_config_html_defaults_to_false(self): """email.html по умолчанию False.""" assert EmailConfig().html is False with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("email:\n smtp:\n host: smtp.example.com\n") cfg = AppConfig.from_yaml(yaml_path) assert cfg.email.html is False def test_email_config_html_false_from_yaml(self): """email.html: false явно загружается из YAML.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "email:\n html: false\n smtp:\n host: smtp.example.com\n" ) cfg = AppConfig.from_yaml(yaml_path) assert cfg.email.html is False def test_email_config_html_invalid_string_defaults_to_false(self): """email.html со строкой 'invalid' приводится к False.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text( "email:\n html: 'invalid'\n smtp:\n host: smtp.example.com\n" ) cfg = AppConfig.from_yaml(yaml_path) assert cfg.email.html is False class TestReportNoTime: """Tests for Config.get_report_no_time().""" @mock.patch.dict(os.environ, {}, clear=True) def test_get_report_no_time_from_yaml(self): """report.no_time: true загружается из YAML.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("report:\n no_time: true\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_report_no_time() is True @mock.patch.dict(os.environ, {}, clear=True) def test_get_report_no_time_defaults_to_false(self): """Без YAML-конфига report.no_time по умолчанию False.""" Config._app = None assert Config.get_report_no_time() is False @mock.patch.dict(os.environ, {}, clear=True) def test_get_report_no_time_from_yaml_false(self): """report.no_time: false явно загружается из YAML.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("report:\n no_time: false\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_report_no_time() is False @mock.patch.dict(os.environ, {}, clear=True) def test_get_report_no_time_invalid_type_defaults_to_false(self): """report.no_time со строковым значением приводится к False.""" with tempfile.TemporaryDirectory() as tmp: yaml_path = Path(tmp) / "config.yml" yaml_path.write_text("report:\n no_time: 'invalid'\n") Config._app = AppConfig.from_yaml(yaml_path) assert Config.get_report_no_time() is False