import logging import os import tempfile from pathlib import Path from unittest import mock from redmine_reporter.yaml_config import ( check_file_permissions, ensure_config_dir, expand_filename_template, resolve_env_vars, ) class TestResolveEnvVars: """Tests for ${VAR} resolution.""" @mock.patch.dict(os.environ, {"MY_VAR": "hello"}, clear=True) def test_replaces_var_with_env_value(self): assert resolve_env_vars("prefix_${MY_VAR}_suffix") == "prefix_hello_suffix" @mock.patch.dict(os.environ, {}, clear=True) def test_missing_var_returns_empty_and_warns(self, caplog): with caplog.at_level(logging.WARNING): result = resolve_env_vars("${MISSING_VAR}") assert result == "" assert "MISSING_VAR" in caplog.text def test_no_braces_returns_unchanged(self): assert resolve_env_vars("plain text no vars") == "plain text no vars" @mock.patch.dict(os.environ, {"A": "${B}", "B": "final"}, clear=True) def test_non_recursive_no_double_resolution(self): # A → literal "${B}", B → "final". Non-recursive means we get "${B}", not "final". result = resolve_env_vars("${A}") assert result == "${B}" class TestEnsureConfigDir: """Tests for config directory creation.""" def test_creates_dir_with_0700(self): with tempfile.TemporaryDirectory() as tmp: config_dir = Path(tmp) / "test_config" result = ensure_config_dir(config_dir) assert result == config_dir assert result.exists() assert result.is_dir() mode = result.stat().st_mode & 0o777 assert mode == 0o700 def test_noop_if_dir_exists(self): with tempfile.TemporaryDirectory() as tmp: config_dir = Path(tmp) / "existing" config_dir.mkdir(mode=0o700) mtime_before = config_dir.stat().st_mtime result = ensure_config_dir(config_dir) assert result == config_dir assert config_dir.stat().st_mtime == mtime_before class TestCheckFilePermissions: """Tests for config file permission checks.""" def test_warns_on_permissive_file(self): with tempfile.TemporaryDirectory() as tmp: f = Path(tmp) / "open.yml" f.write_text("key: value") f.chmod(0o644) warnings = check_file_permissions(f) assert len(warnings) >= 1 assert "0644" in warnings[0] or "permissions" in warnings[0].lower() def test_silent_on_0600(self): with tempfile.TemporaryDirectory() as tmp: f = Path(tmp) / "secure.yml" f.write_text("key: value") f.chmod(0o600) warnings = check_file_permissions(f) assert warnings == [] class TestExpandFilenameTemplate: """Tests for filename template expansion.""" def test_expands_all_placeholders(self): result = expand_filename_template( "report_{author}_{from}_{to}.{ext}", author="Кокос А.А.", from_date="2026-06-01", to_date="2026-06-30", ext="xlsx", ) assert result == "report_Кокос_А.А._2026-06-01_2026-06-30.xlsx" def test_date_placeholder_dd_mm_yyyy(self): result = expand_filename_template( "отчёт_{date}.{ext}", author="Кокос А.А.", from_date="2026-03-01", to_date="2026-03-31", ext="odt", ) assert result == "отчёт_31_03_2026.odt" def test_no_placeholders_returns_unchanged(self): result = expand_filename_template( "report.odt", author="Кокос А.А.", from_date="2026-01-01", to_date="2026-01-31", ext="odt", ) assert result == "report.odt" def test_unknown_placeholder_left_as_is(self): result = expand_filename_template( "{author}_{unknown}.{ext}", author="Кокос А.А.", from_date="2026-01-01", to_date="2026-01-31", ext="xlsx", ) assert result == "Кокос_А.А._{unknown}.xlsx" class TestResolveOutputPath: """Tests for resolve_output_path().""" def test_none_returns_none(self): from redmine_reporter.yaml_config import resolve_output_path assert resolve_output_path(None) is None def test_explicit_path_with_known_extension_is_unchanged(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path("/tmp/report.xlsx") assert result == "/tmp/report.xlsx" def test_explicit_path_with_unknown_extension_is_unchanged(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path("report.odt") assert result == "report.odt" def test_no_extension_appends_default_format(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path("report", default_format="xlsx") assert result == "report.xlsx" def test_no_extension_appends_custom_default_format(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path("file", default_format="csv") assert result == "file.csv" def test_bare_format_name_resolves_to_template_path(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path( "xlsx", output_dir="/tmp/reports", filename_template="{date}.{ext}", author="Кокос А.А.", from_date="2026-06-01", to_date="2026-06-30", default_format="xlsx", ) assert result == "/tmp/reports/30_06_2026.xlsx" def test_bare_format_name_odt_resolves_to_template_path(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path( "odt", output_dir="~/reports", filename_template="{author}_{from}_{to}.{ext}", author="Кокос А.А.", from_date="2026-06-01", to_date="2026-06-30", default_format="xlsx", ) assert result.startswith("/") and result.endswith( "/reports/Кокос_А.А._2026-06-01_2026-06-30.odt" ) assert "~" not in result def test_bare_format_name_with_empty_output_dir_uses_cwd(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path( "csv", output_dir="", filename_template="report.{ext}", author="A", from_date="2026-01-01", to_date="2026-01-31", ) assert not result.startswith("/") assert "report.csv" in result def test_unknown_bare_format_is_treated_as_path(self): from redmine_reporter.yaml_config import resolve_output_path result = resolve_output_path("unknown_format") assert result == "unknown_format.xlsx" class TestSavePeriodToConfig: """Tests for save_period_to_config().""" def test_creates_last_used_in_empty_config(self, tmp_path): import yaml from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True) with open(config_path) as fh: data = yaml.safe_load(fh) assert data["period"]["last_used"]["from"] == "2026-06-01" assert data["period"]["last_used"]["to"] == "2026-06-30" assert "default_from" not in data["period"] def test_overwrites_existing_last_used(self, tmp_path): import yaml from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" config_path.write_text( "period:\n" " last_used:\n" " from: '2026-05-01'\n" " to: '2026-05-31'\n" ) save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True) with open(config_path) as fh: data = yaml.safe_load(fh) assert data["period"]["last_used"]["from"] == "2026-06-01" assert data["period"]["last_used"]["to"] == "2026-06-30" def test_dynamic_false_overwrites_defaults(self, tmp_path): import yaml from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" config_path.write_text( "period:\n" " default_from: '2026-01-01'\n" " default_to: '2026-01-31'\n" ) save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", False) with open(config_path) as fh: data = yaml.safe_load(fh) assert data["period"]["default_from"] == "2026-06-01" assert data["period"]["default_to"] == "2026-06-30" assert data["period"]["last_used"]["from"] == "2026-06-01" assert data["period"]["last_used"]["to"] == "2026-06-30" def test_datetime_precision_stores_timestamps(self, tmp_path): import yaml from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" save_period_to_config( str(config_path), "2026-06-30T09:00:00", "2026-06-30T12:00:00", "datetime", True, ) with open(config_path) as fh: data = yaml.safe_load(fh) assert data["period"]["last_used"]["from"] == "2026-06-30T09:00:00" assert data["period"]["last_used"]["to"] == "2026-06-30T12:00:00" def test_preserves_existing_sections(self, tmp_path): import yaml from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" config_path.write_text( "redmine:\n" " url: https://example.com\n" " author: Test\n" "output:\n" " dir: /tmp\n" ) save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True) with open(config_path) as fh: data = yaml.safe_load(fh) assert data["redmine"]["url"] == "https://example.com" assert data["redmine"]["author"] == "Test" assert data["output"]["dir"] == "/tmp" assert data["period"]["last_used"]["from"] == "2026-06-01" def test_sets_permissions_0600(self, tmp_path): from redmine_reporter.yaml_config import save_period_to_config config_path = tmp_path / "config.yml" save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True) mode = config_path.stat().st_mode & 0o777 assert mode == 0o600