fix: normalize datetimes to aware UTC in dedup

Dedup with precision=datetime crashed with TypeError: redminelib returns
naive created_on/updated_on while the cutoff from _compute_dedup_cutoff
is aware UTC. Normalize at a single point: _parse_datetime now always
returns an aware datetime (naive treated as UTC), matching its docstring.
The dedup cutoff is normalized the same way, and any residual comparison
TypeError is wrapped in RedmineAPIError instead of leaking raw.

Also fix --commit saving last_used.to as naive local time; it now stores
aware UTC (datetime.now(timezone.utc)) so the next run computes a correct
aware cutoff.

Closes #58
This commit is contained in:
Кокос Артем Николаевич
2026-07-17 12:11:35 +07:00
parent c4ec23048a
commit 46674ba926
4 changed files with 164 additions and 22 deletions

View File

@@ -656,6 +656,49 @@ class TestCommitFlag:
assert "T" in saved_from
assert "T" in saved_to
@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_with_precision_datetime_saves_aware_utc(
self, mock_save, mock_fetch, tmp_path
):
"""При precision=datetime --commit сохраняет last_used как aware UTC (#58)."""
from datetime import datetime, timezone
import yaml
issue = _MockIssue()
mock_fetch.return_value = [(issue, 1.0, None)]
config_path = tmp_path / "config.yml"
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()
mock_get.return_value = mock_formatter
code = main(
[
"--date",
"2026-06-30--2026-06-30",
"--commit",
"--output",
str(tmp_path / "report.xlsx"),
"--config-path",
str(config_path),
]
)
assert code == 0
call_args = mock_save.call_args
assert call_args is not None
saved_from, saved_to = call_args.args[1], call_args.args[2]
for saved in (saved_from, saved_to):
parsed = datetime.fromisoformat(saved)
assert parsed.tzinfo is not None, f"{saved} must be timezone-aware"
assert parsed.utcoffset() == timezone.utc.utcoffset(None)
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
@mock.patch("redmine_reporter.cli.fetch_issues_with_spent_time")
def test_commit_no_entries_does_not_save(self, mock_fetch, tmp_path):