"""Validation tests for T-106: Test Workflow Service. Mocked DB sessions exercise the workflow service without PostgreSQL. Uses the same import stack as the rest of the suite (``tests.conftest``) so this module must **not** replace entries in ``sys.modules`` — doing so broke JWT verification in API tests by splitting :mod:`app.auth` (loaded early via conftest) from :mod:`app.routers.auth` / :mod:`app.dependencies.auth`. """ import uuid from unittest.mock import MagicMock, patch from app.domain.exceptions import InvalidTransitionError from app.models.enums import TestState from app.services.test_workflow_service import ( can_transition, reopen_test, start_execution, submit_blue_evidence, submit_red_evidence, transition_state, validate_as_blue_lead, validate_as_red_lead, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_test(state: TestState = TestState.draft, **kwargs) -> MagicMock: t = MagicMock() t.id = uuid.uuid4() t.name = "Mock Test" t.technique_id = uuid.uuid4() t.state = state t.red_validation_status = kwargs.get("red_validation_status", None) t.blue_validation_status = kwargs.get("blue_validation_status", None) t.red_validated_by = None t.red_validated_at = None t.red_validation_notes = None t.blue_validated_by = None t.blue_validated_at = None t.blue_validation_notes = None t.execution_date = None t.red_started_at = None t.blue_started_at = None t.paused_at = None t.red_paused_seconds = 0 t.blue_paused_seconds = 0 return t def _make_user(role: str = "red_tech") -> MagicMock: user = MagicMock() user.id = uuid.uuid4() user.role = role return user def _make_db() -> MagicMock: return MagicMock() # --------------------------------------------------------------------------- # 1. draft -> red_executing works # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_draft_to_red_executing(mock_log): test = _make_test(TestState.draft) user = _make_user("red_tech") db = _make_db() result = start_execution(db, test, user) assert result.state == TestState.red_executing assert result.execution_date is not None db.flush.assert_called() mock_log.assert_called() print(" [PASS] Transition draft -> red_executing works") # --------------------------------------------------------------------------- # 2. draft -> validated fails (not allowed) # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_draft_to_validated_fails(mock_log): test = _make_test(TestState.draft) user = _make_user("admin") db = _make_db() try: transition_state(db, test, TestState.validated, user) assert False, "Should have raised InvalidTransitionError" except InvalidTransitionError as exc: assert exc.code == "INVALID_TRANSITION" assert exc.current_state == "draft" assert exc.target_state == "validated" print(" [PASS] Transition draft -> validated correctly fails") # --------------------------------------------------------------------------- # 3. red_executing -> blue_evaluating works # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_red_executing_to_blue_evaluating(mock_log): test = _make_test(TestState.red_executing) user = _make_user("red_tech") db = _make_db() result = submit_red_evidence(db, test, user) assert result.state == TestState.blue_evaluating db.flush.assert_called() mock_log.assert_called() print(" [PASS] Transition red_executing -> blue_evaluating works") # --------------------------------------------------------------------------- # 4. check_dual_validation -> validated when both approved # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_dual_validation_both_approved(mock_log): test = _make_test(TestState.in_review) user_red = _make_user("red_lead") user_blue = _make_user("blue_lead") db = _make_db() validate_as_red_lead(db, test, user_red, "approved", "LGTM") validate_as_blue_lead(db, test, user_blue, "approved", "Detection OK") assert test.state == TestState.validated print(" [PASS] check_dual_validation -> validated when both approved") # --------------------------------------------------------------------------- # 5. check_dual_validation -> rejected when one rejects # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_dual_validation_one_rejected(mock_log): test = _make_test(TestState.in_review) user_red = _make_user("red_lead") db = _make_db() validate_as_red_lead(db, test, user_red, "rejected", "Insufficient evidence") assert test.state == TestState.rejected print(" [PASS] check_dual_validation -> rejected when one rejects") # --------------------------------------------------------------------------- # 6. reopen_test clears validation fields # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_reopen_clears_validation(mock_log): test = _make_test( TestState.rejected, red_validation_status="rejected", blue_validation_status="approved", ) user = _make_user("red_lead") db = _make_db() result = reopen_test(db, test, user) assert result.state == TestState.draft assert result.red_validation_status is None assert result.blue_validation_status is None assert result.red_validated_by is None assert result.red_validated_at is None assert result.red_validation_notes is None assert result.blue_validated_by is None assert result.blue_validated_at is None assert result.blue_validation_notes is None db.flush.assert_called() print(" [PASS] reopen_test clears validation fields and moves to draft") # --------------------------------------------------------------------------- # 7. Every transition generates an audit log # --------------------------------------------------------------------------- @patch("app.services.test_workflow_service.log_action") def test_transitions_generate_audit_logs(mock_log): test = _make_test(TestState.draft) user = _make_user("red_tech") db = _make_db() start_execution(db, test, user) assert mock_log.call_count >= 1 c1 = mock_log.call_count submit_red_evidence(db, test, user) assert mock_log.call_count > c1 c2 = mock_log.call_count submit_blue_evidence(db, test, user) assert mock_log.call_count > c2 print(" [PASS] Each transition generates an audit log") # --------------------------------------------------------------------------- # 8. can_transition correctness # --------------------------------------------------------------------------- def test_can_transition_map(): test = _make_test(TestState.draft) assert can_transition(test, TestState.red_executing) is True assert can_transition(test, TestState.validated) is False assert can_transition(test, TestState.blue_evaluating) is False test.state = TestState.red_executing assert can_transition(test, TestState.blue_evaluating) is True assert can_transition(test, TestState.draft) is False test.state = TestState.blue_evaluating assert can_transition(test, TestState.in_review) is True test.state = TestState.in_review assert can_transition(test, TestState.validated) is True assert can_transition(test, TestState.rejected) is True assert can_transition(test, TestState.draft) is False test.state = TestState.rejected assert can_transition(test, TestState.draft) is True test.state = TestState.validated assert can_transition(test, TestState.draft) is False assert can_transition(test, TestState.rejected) is False print(" [PASS] can_transition map is correct") # --------------------------------------------------------------------------- # Run all # --------------------------------------------------------------------------- if __name__ == "__main__": print("T-106 Validation: Test Workflow Service") print("=" * 50) test_draft_to_red_executing() test_draft_to_validated_fails() test_red_executing_to_blue_evaluating() test_dual_validation_both_approved() test_dual_validation_one_rejected() test_reopen_clears_validation() test_transitions_generate_audit_logs() test_can_transition_map() print("=" * 50) print("ALL T-106 validations PASSED!")