style: apply ruff format to all source files
This commit is contained in:
@@ -62,7 +62,9 @@ def test_cli_returns_zero_on_no_entries(mock_fetch):
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_cli_config_error():
|
||||
"""Невалидный конфиг -- выход 1."""
|
||||
code = main(["--date", "2026-01-01--2026-01-31", "--config-path", "/nonexistent/config.yml"])
|
||||
code = main(
|
||||
["--date", "2026-01-01--2026-01-31", "--config-path", "/nonexistent/config.yml"]
|
||||
)
|
||||
assert code == 1
|
||||
|
||||
|
||||
@@ -183,7 +185,7 @@ def test_cli_config_file_loading(mock_fetch, tmp_path):
|
||||
"""--config загружает переменные из указанного .env-файла."""
|
||||
config_path = tmp_path / "custom.env"
|
||||
config_path.write_text(
|
||||
"REDMINE_URL=https://config.redmine.loc\n" "REDMINE_API_KEY=config-token\n",
|
||||
"REDMINE_URL=https://config.redmine.loc\nREDMINE_API_KEY=config-token\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
mock_fetch.return_value = None
|
||||
@@ -248,7 +250,9 @@ def test_cli_prints_readable_timeout_error(mock_fetch, capsys):
|
||||
"""CLI выводит понятное сообщение при таймауте."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
mock_fetch.side_effect = RedmineAPIError("Redmine request timed out after 30 seconds")
|
||||
mock_fetch.side_effect = RedmineAPIError(
|
||||
"Redmine request timed out after 30 seconds"
|
||||
)
|
||||
code = main(["--date", "2026-01-01--2026-01-31"])
|
||||
captured = capsys.readouterr()
|
||||
assert code == 1
|
||||
@@ -263,7 +267,9 @@ def test_cli_no_time_passed_to_formatter(mock_fetch, tmp_path):
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
output = str(tmp_path / "report.xlsx")
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get_formatter:
|
||||
with mock.patch(
|
||||
"redmine_reporter.cli.get_formatter_by_extension"
|
||||
) as mock_get_formatter:
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get_formatter.return_value = mock_formatter
|
||||
main(["--date", "2026-01-01--2026-01-31", "--output", output, "--no-time"])
|
||||
@@ -454,7 +460,9 @@ class TestOutputPathResolution:
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
def test_output_without_extension_appends_default_format(self, mock_fetch, tmp_path):
|
||||
def test_output_without_extension_appends_default_format(
|
||||
self, mock_fetch, tmp_path
|
||||
):
|
||||
"""--output report без расширения → добавляет .xlsx."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
@@ -548,7 +556,9 @@ class TestCommitFlag:
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(yaml.dump({"period": {"precision": "date", "dynamic": True}}))
|
||||
config_path.write_text(
|
||||
yaml.dump({"period": {"precision": "date", "dynamic": True}})
|
||||
)
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
@@ -583,7 +593,9 @@ class TestCommitFlag:
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(yaml.dump({"period": {"precision": "datetime", "dynamic": True}}))
|
||||
config_path.write_text(
|
||||
yaml.dump({"period": {"precision": "datetime", "dynamic": True}})
|
||||
)
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
@@ -630,7 +642,9 @@ class TestCommitFlag:
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||
def test_commit_dynamic_false_overwrites_defaults(self, mock_save, mock_fetch, tmp_path):
|
||||
def test_commit_dynamic_false_overwrites_defaults(
|
||||
self, mock_save, mock_fetch, tmp_path
|
||||
):
|
||||
"""При dynamic=false --commit перезаписывает default_from/to."""
|
||||
import yaml
|
||||
|
||||
@@ -638,7 +652,9 @@ class TestCommitFlag:
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(yaml.dump({"period": {"precision": "date", "dynamic": False}}))
|
||||
config_path.write_text(
|
||||
yaml.dump({"period": {"precision": "date", "dynamic": False}})
|
||||
)
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
@@ -663,7 +679,9 @@ class TestCommitFlag:
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||
def test_commit_without_output_uses_default_path(self, mock_save, mock_fetch, tmp_path):
|
||||
def test_commit_without_output_uses_default_path(
|
||||
self, mock_save, mock_fetch, tmp_path
|
||||
):
|
||||
"""--commit без --output сохраняет файл по шаблону из конфига."""
|
||||
import yaml
|
||||
|
||||
@@ -702,7 +720,9 @@ class TestCommitFlag:
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
@mock.patch("redmine_reporter.cli.save_period_to_config")
|
||||
def test_commit_prints_info_to_stderr(self, mock_save, mock_fetch, capsys, tmp_path):
|
||||
def test_commit_prints_info_to_stderr(
|
||||
self, mock_save, mock_fetch, capsys, tmp_path
|
||||
):
|
||||
"""--commit выводит сообщение о фиксации в stderr."""
|
||||
import yaml
|
||||
|
||||
@@ -710,7 +730,9 @@ class TestCommitFlag:
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(yaml.dump({"period": {"precision": "date", "dynamic": True}}))
|
||||
config_path.write_text(
|
||||
yaml.dump({"period": {"precision": "date", "dynamic": True}})
|
||||
)
|
||||
|
||||
with mock.patch("redmine_reporter.cli.get_formatter_by_extension") as mock_get:
|
||||
mock_formatter = mock.MagicMock()
|
||||
@@ -741,7 +763,9 @@ class TestCommitFlag:
|
||||
mock_fetch.return_value = []
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(yaml.dump({"period": {"precision": "date", "dynamic": True}}))
|
||||
config_path.write_text(
|
||||
yaml.dump({"period": {"precision": "date", "dynamic": True}})
|
||||
)
|
||||
|
||||
code = main(
|
||||
[
|
||||
@@ -791,10 +815,13 @@ class TestSendFlag:
|
||||
output = str(tmp_path / "report.xlsx")
|
||||
code = main(
|
||||
[
|
||||
"--date", "2026-06-01--2026-06-30",
|
||||
"--output", output,
|
||||
"--date",
|
||||
"2026-06-01--2026-06-30",
|
||||
"--output",
|
||||
output,
|
||||
"--send",
|
||||
"--config-path", str(config_path),
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
@@ -804,7 +831,9 @@ class TestSendFlag:
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
@mock.patch("redmine_reporter.cli.send_report")
|
||||
@mock.patch("redmine_reporter.cli.get_formatter_by_extension")
|
||||
def test_send_without_output_saves_to_default_path(self, mock_get, mock_send, mock_fetch, tmp_path):
|
||||
def test_send_without_output_saves_to_default_path(
|
||||
self, mock_get, mock_send, mock_fetch, tmp_path
|
||||
):
|
||||
"""--send без --output сохраняет файл по шаблону, затем отправляет."""
|
||||
issue = _MockIssue()
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
@@ -830,9 +859,11 @@ class TestSendFlag:
|
||||
|
||||
code = main(
|
||||
[
|
||||
"--date", "2026-06-01--2026-06-30",
|
||||
"--date",
|
||||
"2026-06-01--2026-06-30",
|
||||
"--send",
|
||||
"--config-path", str(config_path),
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
@@ -851,10 +882,13 @@ class TestSendFlag:
|
||||
|
||||
code = main(
|
||||
[
|
||||
"--date", "2026-06-01--2026-06-30",
|
||||
"--output", str(tmp_path / "report.xlsx"),
|
||||
"--date",
|
||||
"2026-06-01--2026-06-30",
|
||||
"--output",
|
||||
str(tmp_path / "report.xlsx"),
|
||||
"--send",
|
||||
"--config-path", str(config_path),
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
@@ -865,7 +899,9 @@ class TestSendFlag:
|
||||
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
|
||||
@mock.patch("redmine_reporter.cli.send_report")
|
||||
@mock.patch("redmine_reporter.cli.get_formatter_by_extension")
|
||||
def test_send_smtp_error_reported_to_stderr(self, mock_get, mock_send, mock_fetch, tmp_path, capsys):
|
||||
def test_send_smtp_error_reported_to_stderr(
|
||||
self, mock_get, mock_send, mock_fetch, tmp_path, capsys
|
||||
):
|
||||
"""Ошибка SMTP выводится в stderr, exit code 1."""
|
||||
from redmine_reporter.client import RedmineAPIError
|
||||
|
||||
@@ -873,7 +909,9 @@ class TestSendFlag:
|
||||
mock_fetch.return_value = [(issue, 1.0)]
|
||||
mock_formatter = mock.MagicMock()
|
||||
mock_get.return_value = mock_formatter
|
||||
mock_send.side_effect = RedmineAPIError("Не удалось подключиться к SMTP-серверу bad:587")
|
||||
mock_send.side_effect = RedmineAPIError(
|
||||
"Не удалось подключиться к SMTP-серверу bad:587"
|
||||
)
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
@@ -890,10 +928,13 @@ class TestSendFlag:
|
||||
|
||||
code = main(
|
||||
[
|
||||
"--date", "2026-06-01--2026-06-30",
|
||||
"--output", str(tmp_path / "report.xlsx"),
|
||||
"--date",
|
||||
"2026-06-01--2026-06-30",
|
||||
"--output",
|
||||
str(tmp_path / "report.xlsx"),
|
||||
"--send",
|
||||
"--config-path", str(config_path),
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
@@ -932,11 +973,14 @@ class TestSendFlag:
|
||||
|
||||
code = main(
|
||||
[
|
||||
"--date", "2026-06-01--2026-06-30",
|
||||
"--output", str(tmp_path / "report.xlsx"),
|
||||
"--date",
|
||||
"2026-06-01--2026-06-30",
|
||||
"--output",
|
||||
str(tmp_path / "report.xlsx"),
|
||||
"--send",
|
||||
"--commit",
|
||||
"--config-path", str(config_path),
|
||||
"--config-path",
|
||||
str(config_path),
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
|
||||
@@ -157,7 +157,9 @@ def test_fetch_uses_username_password_when_no_api_key(mock_redmine_class):
|
||||
assert "key" not in kwargs
|
||||
|
||||
|
||||
@mock.patch.dict(os.environ, {**PASSWORD_ENV, "REDMINE_VERIFY": "/tmp/redmine-ca.pem"}, clear=True)
|
||||
@mock.patch.dict(
|
||||
os.environ, {**PASSWORD_ENV, "REDMINE_VERIFY": "/tmp/redmine-ca.pem"}, clear=True
|
||||
)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_fetch_uses_custom_verify_path(mock_redmine_class):
|
||||
mock_redmine = mock_redmine_class.return_value
|
||||
@@ -390,7 +392,9 @@ def test_fetch_mounts_retry_adapter(mock_redmine_class):
|
||||
assert "http://" in prefixes
|
||||
|
||||
# Проверяем retry-конфигурацию адаптера
|
||||
https_adapter = next(call.args[1] for call in mount_calls if call.args[0] == "https://")
|
||||
https_adapter = next(
|
||||
call.args[1] for call in mount_calls if call.args[0] == "https://"
|
||||
)
|
||||
max_retries = https_adapter.max_retries
|
||||
assert max_retries.total == 3
|
||||
assert 429 in max_retries.status_forcelist
|
||||
@@ -422,7 +426,9 @@ def test_fetch_chunks_large_issue_count(mock_redmine_class):
|
||||
ids_str = kwargs.get("issue_id", "")
|
||||
call_chunks.append(ids_str)
|
||||
ids = [int(x) for x in ids_str.split(",")]
|
||||
return [mock.MagicMock(id=i, project="P", subject="T", status="New") for i in ids]
|
||||
return [
|
||||
mock.MagicMock(id=i, project="P", subject="T", status="New") for i in ids
|
||||
]
|
||||
|
||||
mock_redmine.issue.filter.side_effect = issue_filter_side_effect
|
||||
|
||||
@@ -473,7 +479,9 @@ def test_dedup_filters_entries_created_before_cutoff(mock_redmine_class):
|
||||
mock_issue2.project = "P"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1, mock_issue2]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
@@ -482,7 +490,9 @@ def test_dedup_filters_entries_created_before_cutoff(mock_redmine_class):
|
||||
|
||||
@mock.patch.dict(os.environ, PASSWORD_ENV, clear=True)
|
||||
@mock.patch("redmine_reporter.client.Redmine")
|
||||
def test_dedup_filters_entries_with_old_created_even_if_updated_recently(mock_redmine_class):
|
||||
def test_dedup_filters_entries_with_old_created_even_if_updated_recently(
|
||||
mock_redmine_class,
|
||||
):
|
||||
"""Записи с created_on < cutoff исключаются даже при updated_on >= cutoff (AND-логика)."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -500,7 +510,9 @@ def test_dedup_filters_entries_with_old_created_even_if_updated_recently(mock_re
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
mock_redmine.issue.filter.return_value = []
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -531,7 +543,9 @@ def test_dedup_keeps_entries_created_after_cutoff(mock_redmine_class):
|
||||
mock_issue1.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
@@ -563,7 +577,9 @@ def test_dedup_entries_without_created_on_are_kept(mock_redmine_class):
|
||||
mock_issue1.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue1]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
@@ -589,7 +605,9 @@ def test_dedup_handles_string_created_on(mock_redmine_class):
|
||||
mock_redmine.time_entry.filter.return_value = [e1]
|
||||
mock_redmine.issue.filter.return_value = []
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@@ -665,7 +683,9 @@ def test_dedup_mixed_entries_correct_filtering(mock_redmine_class):
|
||||
mock_issue_akiy.status = "New"
|
||||
mock_redmine.issue.filter.return_value = [mock_issue_new, mock_issue_akiy]
|
||||
|
||||
result = fetch_issues_with_spent_time("2026-07-01", "2026-07-01", dedup_before=dedup_cutoff)
|
||||
result = fetch_issues_with_spent_time(
|
||||
"2026-07-01", "2026-07-01", dedup_before=dedup_cutoff
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 2
|
||||
|
||||
@@ -328,7 +328,9 @@ class TestConfigYamlFallback:
|
||||
|
||||
assert Config.get_redmine_url() == "https://cli-override.example.com"
|
||||
|
||||
@mock.patch.dict(os.environ, {"REDMINE_URL": "https://env-redmine.example.com/"}, clear=True)
|
||||
@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"
|
||||
@@ -501,7 +503,9 @@ class TestComputeNextPeriod:
|
||||
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")
|
||||
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"
|
||||
|
||||
|
||||
@@ -134,7 +134,9 @@ def odt_formatter():
|
||||
)
|
||||
),
|
||||
):
|
||||
yield ODTFormatter(author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31")
|
||||
yield ODTFormatter(
|
||||
author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31"
|
||||
)
|
||||
|
||||
|
||||
# -- Тесты упаковки formatters как полноценного пакета --
|
||||
@@ -171,7 +173,11 @@ def _simulate_missing_odfpy():
|
||||
|
||||
saved = {}
|
||||
for key in list(sys.modules.keys()):
|
||||
if key == "odf" or key.startswith("odf.") or key == "redmine_reporter.formatters.odt":
|
||||
if (
|
||||
key == "odf"
|
||||
or key.startswith("odf.")
|
||||
or key == "redmine_reporter.formatters.odt"
|
||||
):
|
||||
saved[key] = sys.modules.pop(key)
|
||||
return saved
|
||||
|
||||
@@ -474,7 +480,9 @@ def test_odt_empty_author_no_garbage_in_header(fake_rows):
|
||||
)
|
||||
),
|
||||
):
|
||||
formatter = ODTFormatter(author="", from_date="2026-01-01", to_date="2026-01-31")
|
||||
formatter = ODTFormatter(
|
||||
author="", from_date="2026-01-01", to_date="2026-01-31"
|
||||
)
|
||||
doc = formatter.format(fake_rows)
|
||||
|
||||
from odf.text import P
|
||||
@@ -501,7 +509,9 @@ def test_odt_formatter_save_creates_valid_file(fake_rows, tmp_path):
|
||||
)
|
||||
),
|
||||
):
|
||||
formatter = ODTFormatter(author="Тест", from_date="2026-01-01", to_date="2026-01-31")
|
||||
formatter = ODTFormatter(
|
||||
author="Тест", from_date="2026-01-01", to_date="2026-01-31"
|
||||
)
|
||||
output_file = tmp_path / "report.odt"
|
||||
formatter.save(fake_rows, str(output_file))
|
||||
|
||||
@@ -537,7 +547,9 @@ def test_odt_has_covered_cells_for_spans(fake_rows):
|
||||
)
|
||||
),
|
||||
):
|
||||
formatter = ODTFormatter(author="Тест", from_date="2026-01-01", to_date="2026-01-31")
|
||||
formatter = ODTFormatter(
|
||||
author="Тест", from_date="2026-01-01", to_date="2026-01-31"
|
||||
)
|
||||
doc = formatter.format(fake_rows)
|
||||
|
||||
from odf.table import CoveredTableCell
|
||||
|
||||
@@ -36,12 +36,16 @@ class TestBuildMessage:
|
||||
def test_subject_substitution(self):
|
||||
"""{author} и {period} подставляются в тему."""
|
||||
cfg = _make_email_config(subject="Отчёт {author} за {period}", attach=False)
|
||||
msg = build_message(cfg, "/tmp/report.xlsx", "Кокос А.Н.", "2026-06-01--2026-06-30")
|
||||
msg = build_message(
|
||||
cfg, "/tmp/report.xlsx", "Кокос А.Н.", "2026-06-01--2026-06-30"
|
||||
)
|
||||
assert msg["Subject"] == "Отчёт Кокос А.Н. за 2026-06-01--2026-06-30"
|
||||
|
||||
def test_body_substitution(self):
|
||||
"""{author} и {period} подставляются в тело."""
|
||||
cfg = _make_email_config(body_text="Автор: {author}, период: {period}", attach=False)
|
||||
cfg = _make_email_config(
|
||||
body_text="Автор: {author}, период: {period}", attach=False
|
||||
)
|
||||
msg = build_message(cfg, "/tmp/report.xlsx", "Иванов", "Q1")
|
||||
plain_part = msg.get_payload()[0]
|
||||
assert "Автор: Иванов, период: Q1" in plain_part.as_string()
|
||||
@@ -81,14 +85,20 @@ class TestBuildMessage:
|
||||
msg = build_message(cfg, str(report), "A", "P")
|
||||
assert len(msg.get_payload()) == 1
|
||||
|
||||
@pytest.mark.parametrize("ext,expected_mime", [
|
||||
(".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
(".odt", "application/vnd.oasis.opendocument.text"),
|
||||
(".csv", "text/csv"),
|
||||
(".html", "text/html"),
|
||||
(".json", "application/json"),
|
||||
(".md", "text/markdown"),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"ext,expected_mime",
|
||||
[
|
||||
(
|
||||
".xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
),
|
||||
(".odt", "application/vnd.oasis.opendocument.text"),
|
||||
(".csv", "text/csv"),
|
||||
(".html", "text/html"),
|
||||
(".json", "application/json"),
|
||||
(".md", "text/markdown"),
|
||||
],
|
||||
)
|
||||
def test_mime_type_by_extension(self, ext, expected_mime, tmp_path):
|
||||
report = tmp_path / f"report{ext}"
|
||||
report.write_text("content")
|
||||
@@ -154,7 +164,9 @@ class TestSendReport:
|
||||
with mock.patch("smtplib.SMTP") as mock_smtp_class:
|
||||
mock_smtp = mock.MagicMock()
|
||||
mock_smtp_class.return_value.__enter__.return_value = mock_smtp
|
||||
mock_smtp.login.side_effect = smtplib.SMTPAuthenticationError(535, b"Bad auth")
|
||||
mock_smtp.login.side_effect = smtplib.SMTPAuthenticationError(
|
||||
535, b"Bad auth"
|
||||
)
|
||||
|
||||
with pytest.raises(RedmineAPIError, match="Ошибка аутентификации SMTP"):
|
||||
send_report(cfg, str(report), "A", "P")
|
||||
@@ -176,7 +188,9 @@ class TestSendReport:
|
||||
with mock.patch("smtplib.SMTP") as mock_smtp_class:
|
||||
mock_smtp = mock.MagicMock()
|
||||
mock_smtp_class.return_value.__enter__.return_value = mock_smtp
|
||||
mock_smtp.send_message.side_effect = smtplib.SMTPException("Something went wrong")
|
||||
mock_smtp.send_message.side_effect = smtplib.SMTPException(
|
||||
"Something went wrong"
|
||||
)
|
||||
|
||||
with pytest.raises(RedmineAPIError, match="Ошибка отправки письма"):
|
||||
send_report(cfg, str(report), "A", "P")
|
||||
|
||||
@@ -217,7 +217,9 @@ class TestSavePeriodToConfig:
|
||||
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)
|
||||
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)
|
||||
@@ -233,10 +235,12 @@ class TestSavePeriodToConfig:
|
||||
|
||||
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"
|
||||
"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)
|
||||
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)
|
||||
@@ -251,10 +255,12 @@ class TestSavePeriodToConfig:
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
"period:\n" " default_from: '2026-01-01'\n" " default_to: '2026-01-31'\n"
|
||||
"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)
|
||||
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)
|
||||
@@ -299,7 +305,9 @@ class TestSavePeriodToConfig:
|
||||
" dir: /tmp\n"
|
||||
)
|
||||
|
||||
save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True)
|
||||
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)
|
||||
@@ -314,7 +322,9 @@ class TestSavePeriodToConfig:
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
|
||||
save_period_to_config(str(config_path), "2026-06-01", "2026-06-30", "date", True)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user