fix(tempo,evidence): fix SystemExit crash + evidence not shown in frontend
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user