fix(tempo,evidence): fix SystemExit crash + evidence not shown in frontend
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

tempo: tempoapiclient raises SystemExit (BaseException) on API errors like
'User is invalid' 400 responses; except Exception never catches it, killing
the uvicorn worker and causing a 500. Wrap create_worklog() to intercept
BaseException and re-raise as RuntimeError so callers can catch it safely.

evidence: TestOut schema was missing red_evidences / blue_evidences fields.
The ORM model has evidences loaded via joinedload but they were never
serialized into the API response. Add both fields to TestOut and override
model_validate to split Test.evidences by team, injecting the backend-proxy
download_url for each one (/api/v1/evidence/{id}/file).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 11:57:52 +02:00
parent 0955f35015
commit e623a0887d
2 changed files with 50 additions and 9 deletions

View File

@@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict
from app.domain.enums import DataClassification from app.domain.enums import DataClassification
from app.models.enums import TestResult, TestState from app.models.enums import TestResult, TestState
from app.schemas.evidence import EvidenceOut
# ── Create ────────────────────────────────────────────────────────── # ── Create ──────────────────────────────────────────────────────────
@@ -166,12 +167,40 @@ class TestOut(BaseModel):
technique_mitre_id: str | None = None technique_mitre_id: str | None = None
technique_name: str | None = None technique_name: str | None = None
# Evidences split by team (populated from the ORM relationship)
red_evidences: list[EvidenceOut] = []
blue_evidences: list[EvidenceOut] = []
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@classmethod @classmethod
def model_validate(cls, obj, **kwargs): def model_validate(cls, obj, **kwargs):
"""Override to populate technique fields from the relationship.""" """Override to populate technique and evidence fields from ORM relationships."""
if hasattr(obj, "technique") and obj.technique is not None: if hasattr(obj, "technique") and obj.technique is not None:
obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id
obj.__dict__["technique_name"] = obj.technique.name obj.__dict__["technique_name"] = obj.technique.name
# Split evidences by team and inject the backend-proxy download URL
if hasattr(obj, "evidences") and obj.evidences is not None:
red_evs: list[EvidenceOut] = []
blue_evs: list[EvidenceOut] = []
for ev in obj.evidences:
ev_out = EvidenceOut(
id=ev.id,
test_id=ev.test_id,
file_name=ev.file_name,
sha256_hash=ev.sha256_hash,
uploaded_by=ev.uploaded_by,
uploaded_at=ev.uploaded_at,
team=ev.team,
notes=ev.notes,
download_url=f"/api/v1/evidence/{ev.id}/file",
)
if ev.team and ev.team.value == "blue":
blue_evs.append(ev_out)
else:
red_evs.append(ev_out)
obj.__dict__["red_evidences"] = red_evs
obj.__dict__["blue_evidences"] = blue_evs
return super().model_validate(obj, **kwargs) return super().model_validate(obj, **kwargs)

View File

@@ -70,8 +70,13 @@ def log_worklog(
time_spent_seconds: int, time_spent_seconds: int,
description: str, description: str,
) -> dict: ) -> dict:
"""Create a worklog entry in Tempo using *user*'s personal token.""" """Create a worklog entry in Tempo using *user*'s personal token.
Note: tempoapiclient raises SystemExit (not Exception) on API errors, so
we intercept BaseException and re-raise as RuntimeError to keep it non-fatal.
"""
tempo = get_user_tempo_client(user) tempo = get_user_tempo_client(user)
try:
return tempo.create_worklog( return tempo.create_worklog(
accountId=author_account_id, accountId=author_account_id,
issueId=jira_issue_id, issueId=jira_issue_id,
@@ -79,6 +84,13 @@ def log_worklog(
timeSpentSeconds=time_spent_seconds, timeSpentSeconds=time_spent_seconds,
description=description, description=description,
) )
except Exception:
raise
except BaseException as exc:
# tempoapiclient raises SystemExit on HTTP errors (e.g. 400 Bad Request).
# SystemExit is a BaseException, not Exception, so convert it so callers
# can catch it with the usual `except Exception` pattern.
raise RuntimeError(f"Tempo API error: {exc}") from exc
def auto_log_test_worklog( def auto_log_test_worklog(