From d839be877605e50daa262f0e8e1c0a3965532f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D1=81?= Date: Sun, 25 Jan 2026 12:40:23 +0700 Subject: [PATCH] Add unit-tests --- tests/test_cli.py | 23 +++++++++++++++++++ tests/test_client.py | 36 +++++++++++++++++++++++++++++ tests/test_config.py | 25 ++++++++++++++++++++ tests/test_report_builder.py | 44 ++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 27 ++++++++++++++++++++++ 5 files changed, 155 insertions(+) create mode 100644 tests/test_cli.py create mode 100644 tests/test_client.py create mode 100644 tests/test_config.py create mode 100644 tests/test_report_builder.py create mode 100644 tests/test_utils.py diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..b8e3150 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,23 @@ +import sys +from io import StringIO +from unittest import mock +from redmine_reporter.cli import main + + +@mock.patch.dict("os.environ", { + "REDMINE_URL": "https://red.eltex.loc/", + "REDMINE_USER": "x", + "REDMINE_PASSWORD": "y" +}) +@mock.patch("redmine_reporter.client.fetch_issues_with_spent_time") +def test_cli_smoke(mock_fetch): + mock_fetch.return_value = [] + old_stdout = sys.stdout + sys.stdout = captured = StringIO() + try: + code = main(["--date", "2026-01-01--2026-01-31"]) + assert code == 0 + output = captured.getvalue() + assert "Total issues: 0" in output + finally: + sys.stdout = old_stdout diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..f23d223 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,36 @@ +import pytest +from unittest import mock +from redmine_reporter.client import fetch_issues_with_spent_time + + +@mock.patch("redmine_reporter.client.Redmine") +def test_fetch_issues_with_spent_time(mock_redmine_class): + # Подготовка моков + mock_redmine = mock_redmine_class.return_value + mock_user = mock.MagicMock() + mock_user.id = 123 + mock_redmine.user.get.return_value = mock_user + + # Два time entry на одну задачу + mock_entry1 = mock.MagicMock() + mock_entry1.issue.id = 101 + mock_entry1.hours = 2.0 + mock_entry2 = mock.MagicMock() + mock_entry2.issue.id = 101 + mock_entry2.hours = 1.5 + mock_redmine.time_entry.filter.return_value = [mock_entry1, mock_entry2] + + # Мок задачи + mock_issue = mock.MagicMock() + mock_issue.id = 101 + mock_issue.project = "Проект X" + mock_issue.subject = "Тестовая задача" + mock_issue.status = "New" + mock_redmine.issue.filter.return_value = [mock_issue] + + result = fetch_issues_with_spent_time("2026-01-01", "2026-01-31") + + assert result is not None + assert len(result) == 1 + issue, total_hours = result[0] + assert total_hours == 3.5 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..8ca4f1c --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,25 @@ +import os +import pytest +from unittest import mock +from redmine_reporter.config import Config + + +@mock.patch.dict(os.environ, { + "REDMINE_URL": "https://red.eltex.loc/", + "REDMINE_USER": "test", + "REDMINE_PASSWORD": "secret" +}) +def test_config_valid(): + Config.validate() # не должно быть исключения + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_config_missing(): + with pytest.raises(ValueError, match="REDMINE_URL"): + Config.validate() + + +@mock.patch.dict(os.environ, {"REDMINE_AUTHOR": "Иванов И.И."}) +def test_get_author(): + assert Config.get_author("") == "Иванов И.И." + assert Config.get_author("Петров П.П.") == "Петров П.П." diff --git a/tests/test_report_builder.py b/tests/test_report_builder.py new file mode 100644 index 0000000..5de4b03 --- /dev/null +++ b/tests/test_report_builder.py @@ -0,0 +1,44 @@ +import pytest +from redmine_reporter.report_builder import build_grouped_report, STATUS_TRANSLATION +from redmine_reporter.utils import get_version + + +class MockIssue: + def __init__(self, project, subject, status, fixed_version=None): + self.project = project + self.subject = subject + self.status = status + + if fixed_version is not None: + self.fixed_version = fixed_version + + +def test_status_translation(): + assert STATUS_TRANSLATION["Closed"] == "Закрыто" + assert STATUS_TRANSLATION["New"] == "В работе" + assert STATUS_TRANSLATION["Resolved"] == "Решена" + + +def test_build_grouped_report(): + issues = [ + (MockIssue("Камеры", "Фича A", "New", "v2.5.0"), 2.0), + (MockIssue("Камеры", "Баг B", "Resolved", "v2.5.0"), 1.5), + (MockIssue("ПО", "Доки", "Pending", None), 4.0), + ] + rows = build_grouped_report(issues) + + assert len(rows) == 3 + # Первая строка — полное название проекта и версии + assert rows[0]["display_project"] == "Камеры" + assert rows[0]["display_version"] == "v2.5.0" + # Вторая — пустые display_* из-за совпадения + assert rows[1]["display_project"] == "" + assert rows[1]["display_version"] == "" + # Третья — новый проект + assert rows[2]["display_project"] == "ПО" + assert rows[2]["display_version"] == "" + + # Проверка перевода и времени + assert rows[0]["status_ru"] == "В работе" + assert rows[0]["time_text"] == "2ч" + assert rows[1]["time_text"] == "1ч 30м" diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..1b30673 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,27 @@ +import pytest +from redmine_reporter.utils import hours_to_human, get_month_name_from_range, get_version + + +def test_hours_to_human(): + assert hours_to_human(0) == "0ч" + assert hours_to_human(1.0) == "1ч" + assert hours_to_human(2.5) == "2ч 30м" + assert hours_to_human(0.75) == "45м" + assert hours_to_human(3.1666) == "3ч 10м" # ≈ 3ч 10м + + +def test_get_month_name_from_range(): + assert get_month_name_from_range("2026-01-01", "2026-01-31") == "Январь" + assert get_month_name_from_range("2025-12-01", "2026-02-15") == "Февраль" # берётся to_date + assert get_month_name_from_range("invalid", "also_invalid") == "Январь" # fallback + + +def test_get_version(): + class MockIssue: + pass + issue_with = MockIssue() + issue_with.fixed_version = "v2.5.0" + assert get_version(issue_with) == "v2.5.0" + + issue_without = MockIssue() + assert get_version(issue_without) == ""