Files
redmine-reporter/tests/test_formatters.py
Кокос Артем Николаевич 8bc8181ce3 Add Redmine API token authentication
2026-05-22 17:19:00 +07:00

219 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
import io
from typing import List
from unittest import mock
from redmine_reporter.types import ReportRow
from redmine_reporter.formatters.console import TableFormatter, CompactFormatter
from redmine_reporter.formatters.csv import CSVFormatter
from redmine_reporter.formatters.markdown import MarkdownFormatter
from redmine_reporter.formatters.odt import ODTFormatter
from odf.opendocument import OpenDocument, OpenDocumentText
def _make_empty_odt_bytes() -> bytes:
"""Создаёт минимальный валидный ODT-документ в памяти."""
doc = OpenDocumentText()
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
def make_fake_report_rows() -> List[ReportRow]:
"""
Генерирует фейковый отчёт с полным покрытием логики группировки:
- Проект A: версия v1.0 (2 задачи), версия v2.0 (1 задача)
- Проект B: версия <N/A> (1 задача)
- Проект C: версия v1.0 (1 задача), версия v1.1 (2 задачи)
"""
return [
{
"project": "Проект A",
"version": "v1.0",
"display_project": "Проект A",
"display_version": "v1.0",
"issue_id": 101,
"subject": "Реализовать фичу X",
"status_ru": "В работе",
"time_text": "4ч 30м",
},
{
"project": "Проект A",
"version": "v1.0",
"display_project": "",
"display_version": "",
"issue_id": 102,
"subject": "Исправить баг Y",
"status_ru": "Решена",
"time_text": "",
},
{
"project": "Проект A",
"version": "v2.0",
"display_project": "",
"display_version": "v2.0",
"issue_id": 103,
"subject": "Документация Z",
"status_ru": "Ожидание",
"time_text": "",
},
{
"project": "Проект B",
"version": "<N/A>",
"display_project": "Проект B",
"display_version": "<N/A>",
"issue_id": 201,
"subject": "Обновить README",
"status_ru": "Закрыто",
"time_text": "",
},
{
"project": "Проект C",
"version": "v1.0",
"display_project": "Проект C",
"display_version": "v1.0",
"issue_id": 301,
"subject": "Настроить CI",
"status_ru": "В работе",
"time_text": "3ч 15м",
},
{
"project": "Проект C",
"version": "v1.1",
"display_project": "",
"display_version": "v1.1",
"issue_id": 302,
"subject": "Добавить тесты",
"status_ru": "В работе",
"time_text": "",
},
{
"project": "Проект C",
"version": "v1.1",
"display_project": "",
"display_version": "",
"issue_id": 303,
"subject": "Рефакторинг",
"status_ru": "Решена",
"time_text": "6ч 45м",
},
]
@pytest.fixture
def fake_rows():
return make_fake_report_rows()
@pytest.fixture
def odt_formatter():
"""ODTFormatter с замоканной загрузкой шаблона."""
odt_bytes = _make_empty_odt_bytes()
mock_file = mock.MagicMock()
mock_file.__enter__ = mock.MagicMock(return_value=io.BytesIO(odt_bytes))
mock_file.__exit__ = mock.MagicMock(return_value=False)
mock_path = mock.MagicMock()
mock_path.open.return_value = mock_file
with mock.patch(
"redmine_reporter.formatters.odt.resources.files",
return_value=mock.MagicMock(
joinpath=mock.MagicMock(
return_value=mock.MagicMock(open=mock.MagicMock(return_value=mock_file))
)
),
):
yield ODTFormatter(
author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31"
)
# -- Параметризованные тесты текстовых форматтеров --
TEXT_FORMATTER_FACTORIES = [
("table", lambda: TableFormatter()),
("compact", lambda: CompactFormatter()),
("csv", lambda: CSVFormatter()),
("markdown", lambda: MarkdownFormatter()),
]
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
def test_text_formatter_returns_nonempty_string(fake_rows, name, factory):
"""Текстовые форматтеры возвращают непустую строку."""
result = factory().format(fake_rows)
assert isinstance(result, str)
assert len(result.strip()) > 0
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
def test_text_formatter_contains_key_content(fake_rows, name, factory):
"""Вывод содержит ключевые данные из отчёта."""
output = factory().format(fake_rows)
assert "Проект A" in output
assert "Проект B" in output
assert "В работе" in output
assert "<N/A>" in output
assert "6ч 45м" in output
if name == "csv":
# В CSV issue_id и subject -- отдельные колонки
assert "101" in output
assert "Реализовать фичу X" in output
else:
assert "101. Реализовать фичу X" in output
@pytest.mark.parametrize("name, factory", TEXT_FORMATTER_FACTORIES)
def test_text_formatter_empty_rows(name, factory):
"""Форматтер не падает на пустом списке строк."""
result = factory().format([])
assert isinstance(result, str)
# -- Тесты консольных форматтеров --
def test_table_formatter_save_raises(fake_rows):
with pytest.raises(NotImplementedError):
TableFormatter().save(fake_rows, "/dev/null")
def test_compact_formatter_save_raises(fake_rows):
with pytest.raises(NotImplementedError):
CompactFormatter().save(fake_rows, "/dev/null")
# -- Тесты ODT форматтера --
def test_odt_formatter_returns_opendocument(fake_rows, odt_formatter):
"""ODTFormatter.format() возвращает объект OpenDocument."""
result = odt_formatter.format(fake_rows)
assert isinstance(result, OpenDocument)
def test_odt_formatter_save_creates_valid_file(fake_rows, tmp_path):
"""ODT можно сохранить -- файл валиден (сигнатура ZIP)."""
odt_bytes = _make_empty_odt_bytes()
mock_file = mock.MagicMock()
mock_file.__enter__ = mock.MagicMock(return_value=io.BytesIO(odt_bytes))
mock_file.__exit__ = mock.MagicMock(return_value=False)
with mock.patch(
"redmine_reporter.formatters.odt.resources.files",
return_value=mock.MagicMock(
joinpath=mock.MagicMock(
return_value=mock.MagicMock(open=mock.MagicMock(return_value=mock_file))
)
),
):
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))
assert output_file.exists()
assert output_file.read_bytes()[:2] == b"PK" # сигнатура ZIP