From 03d7d1cc801280c6129ee03d268c7e94ffa00790 Mon Sep 17 00:00:00 2001 From: Kitos Date: Mon, 18 May 2026 13:36:26 +0200 Subject: [PATCH] feat(tempo): harden worklog sync and add tests [FASE-1.4] Add tempo-api-python-client dependency, TEMPO_API_VERSION setting, enum-safe Jira link lookup, work type on create_worklog, and mocked auto_log tests. --- backend/app/config.py | 1 + backend/app/services/tempo_service.py | 31 +++++++++----- backend/requirements.txt | 1 + backend/tests/test_tempo_service.py | 60 +++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 backend/tests/test_tempo_service.py diff --git a/backend/app/config.py b/backend/app/config.py index e86022b..c2f17cc 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -57,6 +57,7 @@ class Settings(BaseSettings): # ── Tempo Integration ───────────────────────────────────────────── TEMPO_ENABLED: bool = False TEMPO_API_TOKEN: str = "" + TEMPO_API_VERSION: int = 4 TEMPO_DEFAULT_WORK_TYPE: str = "Red Team" # ── OSINT / Intelligence ──────────────────────────────────────── diff --git a/backend/app/services/tempo_service.py b/backend/app/services/tempo_service.py index 41a6540..9569787 100644 --- a/backend/app/services/tempo_service.py +++ b/backend/app/services/tempo_service.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from app.config import settings from app.domain.exceptions import InvalidOperationError -from app.models.jira_link import JiraLink +from app.models.jira_link import JiraLink, JiraLinkEntityType logger = logging.getLogger(__name__) @@ -33,17 +33,21 @@ def log_worklog( date: str, time_spent_seconds: int, description: str, + work_type: str | None = None, ) -> dict: """Create a worklog entry in Tempo.""" tempo = get_tempo_client() - worklog = tempo.create_worklog( - accountId=author_account_id, - issueId=jira_issue_id, - dateFrom=date, - timeSpentSeconds=time_spent_seconds, - description=description, - ) - return worklog + kwargs: dict = { + "accountId": author_account_id, + "issueId": jira_issue_id, + "dateFrom": date, + "timeSpentSeconds": time_spent_seconds, + "description": description, + } + wt = work_type or settings.TEMPO_DEFAULT_WORK_TYPE + if wt: + kwargs["workType"] = wt + return tempo.create_worklog(**kwargs) def auto_log_test_worklog( @@ -61,7 +65,10 @@ def auto_log_test_worklog( link = ( db.query(JiraLink) - .filter(JiraLink.entity_id == test.id, JiraLink.entity_type == "test") + .filter( + JiraLink.entity_id == test.id, + JiraLink.entity_type == JiraLinkEntityType.test, + ) .first() ) @@ -77,7 +84,9 @@ def auto_log_test_worklog( result = log_worklog( jira_issue_id=int(link.jira_issue_id), author_account_id=getattr(user, "jira_account_id", "") or "", - date=test.created_at.strftime("%Y-%m-%d"), + date=(getattr(test, "updated_at", None) or test.created_at).strftime( + "%Y-%m-%d", + ), time_spent_seconds=duration, description=f"[Aegis] {activity_type}: {test.name}", ) diff --git a/backend/requirements.txt b/backend/requirements.txt index 1cd7569..7dad6aa 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,6 +19,7 @@ slowapi defusedxml redis>=5.0.0 atlassian-python-api>=4.0.0 +tempo-api-python-client>=0.8.0 weasyprint>=62.0 docxtpl>=0.18.0 diff --git a/backend/tests/test_tempo_service.py b/backend/tests/test_tempo_service.py new file mode 100644 index 0000000..8e5a295 --- /dev/null +++ b/backend/tests/test_tempo_service.py @@ -0,0 +1,60 @@ +"""Tempo service unit tests (FASE-1.4).""" + +from datetime import datetime +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +import pytest + +from app.domain.exceptions import InvalidOperationError +from app.models.jira_link import JiraLink, JiraLinkEntityType +from app.services import tempo_service + + +def test_auto_log_test_worklog_skips_when_disabled(monkeypatch, db): + monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", False) + test = MagicMock() + test.id = uuid4() + user = MagicMock() + assert tempo_service.auto_log_test_worklog(db, test, user, "red_team_execution") is None + + +def test_get_tempo_client_raises_when_disabled(monkeypatch): + monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", False) + with pytest.raises(InvalidOperationError, match="not enabled"): + tempo_service.get_tempo_client() + + +@patch("app.services.tempo_service.log_worklog") +def test_auto_log_test_worklog_calls_tempo(mock_log_worklog, monkeypatch, db, admin_user): + monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", True) + mock_log_worklog.return_value = {"id": "wl-1"} + + test_id = uuid4() + link = JiraLink( + entity_type=JiraLinkEntityType.test, + entity_id=test_id, + jira_issue_key="TST-10", + jira_issue_id="10010", + created_by=admin_user.id, + ) + db.add(link) + db.commit() + + test = MagicMock() + test.id = test_id + test.name = "Phishing simulation" + test.red_started_at = datetime(2026, 5, 18, 10, 0, 0) + test.updated_at = datetime(2026, 5, 18, 12, 0, 0) + test.created_at = test.updated_at + + result = tempo_service.auto_log_test_worklog( + db, test, admin_user, "red_team_execution", + ) + + assert result == {"id": "wl-1"} + mock_log_worklog.assert_called_once() + kwargs = mock_log_worklog.call_args.kwargs + assert kwargs["jira_issue_id"] == 10010 + assert kwargs["time_spent_seconds"] > 0 + assert "[Aegis]" in kwargs["description"]