refactor(core): introduce Unit of Work and remove commits from services
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Add UnitOfWork context manager in domain/unit_of_work.py with commit/rollback/flush API and auto-rollback on exception

- Remove all db.commit() from test_workflow_service (8 calls), notification_service (4 calls), status_service (1 call)

- Services now only stage changes via db.add/db.flush; caller owns the transaction boundary

- Update routers/tests.py: wrap 9 workflow endpoints in UnitOfWork context managers

- Update routers/notifications.py: wrap mark_as_read and mark_all_as_read in UnitOfWork
This commit is contained in:
2026-02-18 12:51:55 +01:00
parent 98e8ca1eef
commit bfce1a8a0e
6 changed files with 123 additions and 74 deletions

View File

@@ -43,6 +43,7 @@ from app.schemas.test import (
TestRemediationUpdate,
)
from app.schemas.test_template import TestTemplateInstantiate
from app.domain.unit_of_work import UnitOfWork
from app.services.audit_service import log_action
from app.services.status_service import recalculate_technique_status
from app.services.test_workflow_service import (
@@ -434,7 +435,9 @@ def start_execution(
):
"""Move a test from ``draft`` to ``red_executing``."""
test = _get_test_or_404(db, test_id)
test = wf_start_execution(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_start_execution(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -452,7 +455,9 @@ def submit_red(
):
"""Red Team finalises — move from ``red_executing`` to ``blue_evaluating``."""
test = _get_test_or_404(db, test_id)
test = wf_submit_red(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_submit_red(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -470,7 +475,9 @@ def submit_blue(
):
"""Blue Team finalises — move from ``blue_evaluating`` to ``in_review``."""
test = _get_test_or_404(db, test_id)
test = wf_submit_blue(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_submit_blue(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -488,7 +495,9 @@ def pause_timer(
):
"""Pause the running timer for the current phase (red_executing or blue_evaluating)."""
test = _get_test_or_404(db, test_id)
test = wf_pause_timer(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_pause_timer(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -506,7 +515,9 @@ def resume_timer(
):
"""Resume the paused timer for the current phase."""
test = _get_test_or_404(db, test_id)
test = wf_resume_timer(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_resume_timer(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -525,16 +536,15 @@ def validate_red(
):
"""Red Lead approves or rejects the red side of a test."""
test = _get_test_with_technique(db, test_id)
test = wf_validate_red(
db, test, current_user,
validation_status=payload.red_validation_status,
notes=payload.red_validation_notes,
)
# Recalculate technique status if test reached a terminal state
if test.state in (TestState.validated, TestState.rejected):
recalculate_technique_status(db, test.technique)
with UnitOfWork(db) as uow:
test = wf_validate_red(
db, test, current_user,
validation_status=payload.red_validation_status,
notes=payload.red_validation_notes,
)
if test.state in (TestState.validated, TestState.rejected):
recalculate_technique_status(db, test.technique)
uow.commit()
db.refresh(test)
return test
@@ -553,16 +563,15 @@ def validate_blue(
):
"""Blue Lead approves or rejects the blue side of a test."""
test = _get_test_with_technique(db, test_id)
test = wf_validate_blue(
db, test, current_user,
validation_status=payload.blue_validation_status,
notes=payload.blue_validation_notes,
)
# Recalculate technique status if test reached a terminal state
if test.state in (TestState.validated, TestState.rejected):
recalculate_technique_status(db, test.technique)
with UnitOfWork(db) as uow:
test = wf_validate_blue(
db, test, current_user,
validation_status=payload.blue_validation_status,
notes=payload.blue_validation_notes,
)
if test.state in (TestState.validated, TestState.rejected):
recalculate_technique_status(db, test.technique)
uow.commit()
db.refresh(test)
return test
@@ -580,7 +589,9 @@ def reopen(
):
"""Reopen a rejected test, moving it back to ``draft``."""
test = _get_test_or_404(db, test_id)
test = wf_reopen(db, test, current_user)
with UnitOfWork(db) as uow:
test = wf_reopen(db, test, current_user)
uow.commit()
db.refresh(test)
return test
@@ -610,25 +621,23 @@ def update_remediation(
for field, value in update_data.items():
setattr(test, field, value)
db.commit()
with UnitOfWork(db) as uow:
log_action(
db,
user_id=current_user.id,
action="update_remediation",
entity_type="test",
entity_id=test.id,
details={"updated_fields": list(update_data.keys())},
)
new_status = update_data.get("remediation_status")
if new_status == "completed" and old_remediation_status != "completed":
wf_handle_remediation(db, test, current_user)
uow.commit()
db.refresh(test)
log_action(
db,
user_id=current_user.id,
action="update_remediation",
entity_type="test",
entity_id=test.id,
details={"updated_fields": list(update_data.keys())},
)
# Auto-create retest when remediation is marked completed
new_status = update_data.get("remediation_status")
if new_status == "completed" and old_remediation_status != "completed":
retest = wf_handle_remediation(db, test, current_user)
if retest:
db.refresh(test)
return test