From 0830b36cd645b66d6d30a6467782184ec5a4781c Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 28 May 2026 12:06:34 +0200 Subject: [PATCH] fix(schemas): avoid lazy-load in TestOut.model_validate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/schemas/test.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/app/schemas/test.py b/backend/app/schemas/test.py index 0d4e2a9..df7b276 100644 --- a/backend/app/schemas/test.py +++ b/backend/app/schemas/test.py @@ -175,16 +175,26 @@ class TestOut(BaseModel): @classmethod 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: obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id 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: + # Only split evidences when they are already in memory (loaded via joinedload) + raw_evs = obj.__dict__.get("evidences") if hasattr(obj, "__dict__") else None + if raw_evs is not None: red_evs: list[EvidenceOut] = [] blue_evs: list[EvidenceOut] = [] - for ev in obj.evidences: + for ev in raw_evs: ev_out = EvidenceOut( id=ev.id, test_id=ev.test_id,