fix(schemas): avoid lazy-load in TestOut.model_validate
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Accessing obj.evidences on a session-expired ORM object (mutation endpoints
do commit+refresh without joinload) triggers a lazy query that fails or
returns stale data. Use obj.__dict__.get('evidences') instead — SQLAlchemy
stores joinloaded relationships in __dict__; absent means not loaded.
Mutation endpoints (submit-red, submit-blue, etc.) return empty evidence
lists, which is fine: the frontend invalidates and refetches GET /tests/{id},
which uses joinedload and correctly populates red_evidences / blue_evidences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,16 +175,26 @@ class TestOut(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def model_validate(cls, obj, **kwargs):
|
def model_validate(cls, obj, **kwargs):
|
||||||
"""Override to populate technique and evidence fields from ORM relationships."""
|
"""Override to populate technique and evidence fields from ORM relationships.
|
||||||
|
|
||||||
|
Evidences are only processed when the relationship was **explicitly loaded**
|
||||||
|
(via joinedload or prior access). Accessing ``obj.evidences`` blindly on a
|
||||||
|
session-expired ORM object triggers a lazy query that fails on mutation
|
||||||
|
endpoints that do not joinload the relationship. We inspect ``obj.__dict__``
|
||||||
|
directly — SQLAlchemy stores loaded relationships there; if the key is absent
|
||||||
|
the relationship is unloaded and we leave the lists empty (the frontend
|
||||||
|
invalidates and refetches the detail endpoint, which *does* joinload).
|
||||||
|
"""
|
||||||
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
|
# Only split evidences when they are already in memory (loaded via joinedload)
|
||||||
if hasattr(obj, "evidences") and obj.evidences is not None:
|
raw_evs = obj.__dict__.get("evidences") if hasattr(obj, "__dict__") else None
|
||||||
|
if raw_evs is not None:
|
||||||
red_evs: list[EvidenceOut] = []
|
red_evs: list[EvidenceOut] = []
|
||||||
blue_evs: list[EvidenceOut] = []
|
blue_evs: list[EvidenceOut] = []
|
||||||
for ev in obj.evidences:
|
for ev in raw_evs:
|
||||||
ev_out = EvidenceOut(
|
ev_out = EvidenceOut(
|
||||||
id=ev.id,
|
id=ev.id,
|
||||||
test_id=ev.test_id,
|
test_id=ev.test_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user