feat(refactor): PEP8, type annotations, docstrings and PyJWT security fix

This commit is contained in:
kitos
2026-06-11 11:09:41 +02:00
161 changed files with 15318 additions and 811 deletions
+408 -3
View File
@@ -12,11 +12,19 @@ an audit-log entry. The caller (router) is responsible for committing the
session via the Unit of Work pattern.
"""
# Import logging
import logging
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import Session from sqlalchemy.orm
from sqlalchemy.orm import Session
# Import settings from app.config
from app.config import settings
from app.domain.exceptions import InvalidOperationError, InvalidTransitionError
from app.domain.test_entity import TestEntity
@@ -27,6 +35,31 @@ from app.models.user import User
from app.services.audit_service import log_action
from app.services.notification_service import notify_test_state_change, create_notification
# Import InvalidOperationError from app.domain.exceptions
from app.domain.exceptions import InvalidOperationError
# Import TestEntity from app.domain.test_entity
from app.domain.test_entity import TestEntity
# Import TestState from app.models.enums
from app.models.enums import TestState
# Import Test from app.models.test
from app.models.test import Test
# Import User from app.models.user
from app.models.user import User
# Import log_action from app.services.audit_service
from app.services.audit_service import log_action
# Import from app.services.notification_service
from app.services.notification_service import (
create_notification,
notify_test_state_change,
)
# Assign logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
@@ -50,18 +83,35 @@ VALID_TRANSITIONS: dict[TestState, list[TestState]] = {
def can_transition(test: Test, target_state: TestState) -> bool:
"""Return *True* if moving *test* to *target_state* is allowed."""
"""Return *True* if moving *test* to *target_state* is allowed.
Args:
test (Test): The test whose current state is being checked.
target_state (TestState): The state to transition to.
Returns:
bool: ``True`` if the transition is permitted by ``VALID_TRANSITIONS``.
"""
# Assign current = test.state if isinstance(test.state, TestState) else TestState(test...
current = test.state if isinstance(test.state, TestState) else TestState(test.state)
# Return target_state in VALID_TRANSITIONS.get(current, [])
return target_state in VALID_TRANSITIONS.get(current, [])
# Define function transition_state
def transition_state(
# Entry: db
db: Session,
# Entry: test
test: Test,
# Entry: target_state
target_state: TestState,
# Entry: user
user: User,
*,
# Entry: action_name
action_name: str = "transition_state",
# Entry: extra_details
extra_details: dict | None = None,
) -> Test:
"""Validate and perform a state transition, log it, and flush.
@@ -71,36 +121,71 @@ def transition_state(
when the transition is illegal. The entity is authoritative for which
transitions are valid; the module-level ``VALID_TRANSITIONS`` dict is
kept temporarily for backward compatibility of ``can_transition()``.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test ORM object to transition.
target_state (TestState): Desired next state.
user (User): The user performing the transition (logged in audit).
action_name (str): Audit log action label; defaults to
``"transition_state"``.
extra_details (dict | None): Optional extra key-value pairs merged
into the audit log details.
Returns:
Test: The mutated test ORM object (state updated, flushed).
"""
# Assign entity = TestEntity.from_orm(test)
entity = TestEntity.from_orm(test)
# Assign previous_state = entity.transition_to(target_state)
previous_state = entity.transition_to(target_state)
# Assign test.state = entity.state
test.state = entity.state
# Flush changes to DB without committing the transaction
db.flush()
# Assign details = {
details: dict = {
# Literal argument value
"previous_state": previous_state,
# Literal argument value
"new_state": target_state.value,
# Literal argument value
"test_name": test.name,
# Literal argument value
"technique_id": str(test.technique_id),
}
# Check: extra_details
if extra_details:
# Call details.update()
details.update(extra_details)
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action=action_name,
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details=details,
)
# Attempt the following; catch errors below
try:
# Call notify_test_state_change()
notify_test_state_change(db, test, target_state.value)
# Handle Exception
except Exception as e:
# Log warning: "Notification failed for test %s: %s", test.id, e
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
# Return test
return test
@@ -112,27 +197,44 @@ def transition_state(
def start_execution(db: Session, test: Test, user: User) -> Test:
"""Move from ``draft`` → ``red_executing``."""
entity = TestEntity.from_orm(test)
# Call entity.start_execution()
entity.start_execution()
# Call entity.apply_to()
entity.apply_to(test)
# Flush changes to DB without committing the transaction
db.flush()
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="start_execution",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={
# Literal argument value
"previous_state": "draft",
# Literal argument value
"new_state": test.state.value,
# Literal argument value
"test_name": test.name,
# Literal argument value
"technique_id": str(test.technique_id),
},
)
# Attempt the following; catch errors below
try:
# Call notify_test_state_change()
notify_test_state_change(db, test, test.state.value)
# Handle Exception
except Exception as e:
# Log warning: "Notification failed for test %s: %s", test.id, e
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
try:
@@ -144,6 +246,7 @@ def start_execution(db: Session, test: Test, user: User) -> Test:
return test
# Define function submit_red_evidence
def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
"""Move from ``red_executing`` → ``blue_evaluating``.
@@ -151,6 +254,14 @@ def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
Requires at least one Red Team evidence file to be uploaded.
Stops the Red Team timer and creates an automatic worklog.
Starts the Blue Team timer by recording ``blue_started_at``.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test whose red-team evidence is being submitted.
user (User): The red-team user submitting the evidence.
Returns:
Test: The mutated test with state advanced and blue timer started.
"""
# Evidence is mandatory before submitting
red_evidence_count = (
@@ -167,29 +278,42 @@ def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
# Auto-resume if paused
paused_extra = 0
# Check: test.paused_at is not None
if test.paused_at is not None:
# Assign paused_extra = max(int((now - test.paused_at).total_seconds()), 0)
paused_extra = max(int((now - test.paused_at).total_seconds()), 0)
# Assign test.paused_at = None
test.paused_at = None
# Assign test = transition_state(
test = transition_state(
db, test, TestState.blue_evaluating, user,
# Keyword argument: action_name
action_name="submit_red_evidence",
)
# Create automatic worklog for Red Team phase (subtract paused time)
_create_phase_worklog(
db,
# Keyword argument: test
test=test,
# Keyword argument: user
user=user,
# Keyword argument: phase_started_at
phase_started_at=test.red_started_at,
# Keyword argument: phase_ended_at
phase_ended_at=now,
# Keyword argument: paused_seconds
paused_seconds=(test.red_paused_seconds or 0) + paused_extra,
# Keyword argument: activity_type
activity_type="red_team_execution",
# Keyword argument: description
description=f"Red Team execution: {test.name}",
)
# Start Blue Team timer
test.blue_started_at = now
# Assign test.blue_paused_seconds = 0
test.blue_paused_seconds = 0
try:
@@ -234,6 +358,7 @@ def start_blue_work(db: Session, test: Test, user: User) -> Test:
return test
# Define function submit_blue_evidence
def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
"""Move from ``blue_evaluating`` → ``in_review``.
@@ -258,12 +383,17 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
# Auto-resume if paused
paused_extra = 0
# Check: test.paused_at is not None
if test.paused_at is not None:
# Assign paused_extra = max(int((now - test.paused_at).total_seconds()), 0)
paused_extra = max(int((now - test.paused_at).total_seconds()), 0)
# Assign test.paused_at = None
test.paused_at = None
# Assign test = transition_state(
test = transition_state(
db, test, TestState.in_review, user,
# Keyword argument: action_name
action_name="submit_blue_evidence",
)
@@ -272,12 +402,17 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
# Tempo worklog reflects real working time, not just queue time.
_create_phase_worklog(
db,
# Keyword argument: test
test=test,
# Keyword argument: user
user=user,
phase_started_at=test.blue_work_started_at or test.blue_started_at,
phase_ended_at=now,
# Keyword argument: paused_seconds
paused_seconds=(test.blue_paused_seconds or 0) + paused_extra,
# Keyword argument: activity_type
activity_type="blue_team_evaluation",
# Keyword argument: description
description=f"Blue Team evaluation: {test.name}",
)
@@ -290,69 +425,125 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
return test
# Define function pause_timer
def pause_timer(db: Session, test: Test, user: User) -> Test:
"""Pause the active phase timer.
Can only be called when the test is in ``red_executing`` or
``blue_evaluating`` and is not already paused.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The currently active test.
user (User): The user pausing the timer.
Returns:
Test: The mutated test with ``paused_at`` set to the current UTC time.
"""
# Check: test.state not in (TestState.red_executing, TestState.blue_evaluating)
if test.state not in (TestState.red_executing, TestState.blue_evaluating):
# Raise InvalidOperationError
raise InvalidOperationError(
f"Cannot pause timer in '{test.state.value}' state"
)
# Check: test.paused_at is not None
if test.paused_at is not None:
# Raise InvalidOperationError
raise InvalidOperationError("Timer is already paused")
# Assign test.paused_at = datetime.utcnow()
test.paused_at = datetime.utcnow()
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="pause_timer",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={"state": test.state.value},
)
# Return test
return test
# Define function resume_timer
def resume_timer(db: Session, test: Test, user: User) -> Test:
"""Resume a paused phase timer.
Accumulates the paused duration into the appropriate counter so
it is subtracted from the final worklog.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The paused test to resume.
user (User): The user resuming the timer.
Returns:
Test: The mutated test with ``paused_at`` cleared and accumulated
pause seconds updated.
"""
# Check: test.paused_at is None
if test.paused_at is None:
# Raise InvalidOperationError
raise InvalidOperationError("Timer is not paused")
# Assign now = datetime.utcnow()
now = datetime.utcnow()
# Assign paused_seconds = max(int((now - test.paused_at).total_seconds()), 0)
paused_seconds = max(int((now - test.paused_at).total_seconds()), 0)
# Check: test.state == TestState.red_executing
if test.state == TestState.red_executing:
# Assign test.red_paused_seconds = (test.red_paused_seconds or 0) + paused_seconds
test.red_paused_seconds = (test.red_paused_seconds or 0) + paused_seconds
# Alternative: test.state == TestState.blue_evaluating
elif test.state == TestState.blue_evaluating:
# Assign test.blue_paused_seconds = (test.blue_paused_seconds or 0) + paused_seconds
test.blue_paused_seconds = (test.blue_paused_seconds or 0) + paused_seconds
# Assign test.paused_at = None
test.paused_at = None
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="resume_timer",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={"paused_seconds": paused_seconds, "state": test.state.value},
)
# Return test
return test
# Define function _create_phase_worklog
def _create_phase_worklog(
# Entry: db
db: Session,
*,
# Entry: test
test: Test,
# Entry: user
user: User,
# Entry: phase_started_at
phase_started_at: datetime | None,
# Entry: phase_ended_at
phase_ended_at: datetime,
# Entry: paused_seconds
paused_seconds: int = 0,
# Entry: activity_type
activity_type: str,
# Entry: description
description: str,
) -> None:
"""Create an automatic, integrity-hashed worklog for a completed phase.
@@ -360,32 +551,64 @@ def _create_phase_worklog(
Subtracts accumulated *paused_seconds* from the gross elapsed time
so the worklog reflects only active working time.
Also triggers Tempo sync if the test has a Jira link.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test for which the worklog is being created.
user (User): The user attributed to the worklog.
phase_started_at (datetime | None): Timestamp when the phase began;
if ``None`` the worklog is skipped with a warning.
phase_ended_at (datetime): Timestamp when the phase ended.
paused_seconds (int): Accumulated paused time in seconds to subtract
from gross elapsed time.
activity_type (str): Worklog activity type label (e.g.
``"red_team_execution"``).
description (str): Human-readable description for the worklog.
"""
# Check: not phase_started_at
if not phase_started_at:
# Log warning:
logger.warning(
# Literal argument value
"No phase start timestamp for test %s (%s), skipping worklog",
test.id, activity_type,
)
# Return control to caller
return
# Assign gross_seconds = int((phase_ended_at - phase_started_at).total_seconds())
gross_seconds = int((phase_ended_at - phase_started_at).total_seconds())
# Assign duration_seconds = max(gross_seconds - paused_seconds, 1)
duration_seconds = max(gross_seconds - paused_seconds, 1)
# Attempt the following; catch errors below
try:
# Import create_worklog from app.services.worklog_service
from app.services.worklog_service import create_worklog
# Assign wl = create_worklog(
wl = create_worklog(
db,
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: activity_type
activity_type=activity_type,
# Keyword argument: started_at
started_at=phase_started_at,
# Keyword argument: ended_at
ended_at=phase_ended_at,
# Keyword argument: duration_seconds
duration_seconds=duration_seconds,
# Keyword argument: description
description=description,
)
# Log info:
logger.info(
# Literal argument value
"Auto-worklog created for test %s: %s, %ds (worklog %s)",
test.id, activity_type, duration_seconds, wl.id,
)
@@ -393,6 +616,7 @@ def _create_phase_worklog(
# Sync to Tempo: only red_team_execution, using the already-computed
# duration so the Tempo entry is identical to the Aegis worklog.
try:
# Import auto_log_test_worklog from app.services.tempo_service
from app.services.tempo_service import auto_log_test_worklog
tempo_result = auto_log_test_worklog(db, test, user, activity_type, duration_seconds)
if tempo_result and isinstance(tempo_result, dict):
@@ -400,17 +624,26 @@ def _create_phase_worklog(
wl.tempo_worklog_id = str(tempo_result.get("tempoWorklogId", ""))
db.flush()
except Exception as e:
# Log warning: "Tempo sync failed for worklog: %s", e, exc_info=T
logger.warning("Tempo sync failed for worklog: %s", e, exc_info=True)
# Handle Exception
except Exception as e:
# Log error: "Failed to create auto-worklog for test %s: %s", t
logger.error("Failed to create auto-worklog for test %s: %s", test.id, e, exc_info=True)
# Define function validate_as_red_lead
def validate_as_red_lead(
# Entry: db
db: Session,
# Entry: test
test: Test,
# Entry: user
user: User,
# Entry: validation_status
validation_status: str,
# Entry: notes
notes: str | None = None,
) -> Test:
"""Record Red Lead's validation decision.
@@ -418,21 +651,45 @@ def validate_as_red_lead(
Delegates validation rules and state mutation entirely to
:meth:`TestEntity.validate_red`. If both leads have voted the
entity will also advance the test to ``validated`` or ``rejected``.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test being reviewed.
user (User): The red-lead user casting their vote.
validation_status (str): Validation decision, e.g. ``"approved"`` or
``"rejected"``.
notes (str | None): Optional freeform notes explaining the decision.
Returns:
Test: The mutated test with red-lead validation fields set.
"""
# Assign entity = TestEntity.from_orm(test)
entity = TestEntity.from_orm(test)
# Call entity.validate_red()
entity.validate_red(validation_status, by=user.id, notes=notes)
# Call entity.apply_to()
entity.apply_to(test)
# Flush changes to DB without committing the transaction
db.flush()
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="validate_as_red_lead",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={
# Literal argument value
"validation_status": validation_status,
# Literal argument value
"notes": notes,
# Literal argument value
"technique_id": str(test.technique_id),
},
)
@@ -441,11 +698,17 @@ def validate_as_red_lead(
return test
# Define function validate_as_blue_lead
def validate_as_blue_lead(
# Entry: db
db: Session,
# Entry: test
test: Test,
# Entry: user
user: User,
# Entry: validation_status
validation_status: str,
# Entry: notes
notes: str | None = None,
) -> Test:
"""Record Blue Lead's validation decision.
@@ -453,21 +716,45 @@ def validate_as_blue_lead(
Delegates validation rules and state mutation entirely to
:meth:`TestEntity.validate_blue`. If both leads have voted the
entity will also advance the test to ``validated`` or ``rejected``.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test being reviewed.
user (User): The blue-lead user casting their vote.
validation_status (str): Validation decision, e.g. ``"approved"`` or
``"rejected"``.
notes (str | None): Optional freeform notes explaining the decision.
Returns:
Test: The mutated test with blue-lead validation fields set.
"""
# Assign entity = TestEntity.from_orm(test)
entity = TestEntity.from_orm(test)
# Call entity.validate_blue()
entity.validate_blue(validation_status, by=user.id, notes=notes)
# Call entity.apply_to()
entity.apply_to(test)
# Flush changes to DB without committing the transaction
db.flush()
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="validate_as_blue_lead",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={
# Literal argument value
"validation_status": validation_status,
# Literal argument value
"notes": notes,
# Literal argument value
"technique_id": str(test.technique_id),
},
)
@@ -476,35 +763,61 @@ def validate_as_blue_lead(
return test
# Define function check_dual_validation
def check_dual_validation(db: Session, test: Test) -> Test:
"""Evaluate both leads' decisions and advance the test if both have voted.
All state mutation is delegated to :meth:`TestEntity.check_dual_validation`.
This function never assigns ``test.state`` directly.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test to evaluate.
Returns:
Test: The mutated test, potentially with state advanced to
``validated`` or ``rejected``.
"""
# Assign entity = TestEntity.from_orm(test)
entity = TestEntity.from_orm(test)
# Call entity.check_dual_validation()
entity.check_dual_validation()
# Call entity.apply_to()
entity.apply_to(test)
# Call _dispatch_dual_validation_effects()
_dispatch_dual_validation_effects(db, test, entity)
# Return test
return test
# Define function _dispatch_dual_validation_effects
def _dispatch_dual_validation_effects(
db: Session, test: Test, entity: TestEntity, actor: User | None = None
) -> None:
"""Dispatch side effects (notifications, cache, Jira) based on domain events."""
for event in entity.events:
# Check: event.name == "dual_validation_approved"
if event.name == "dual_validation_approved":
# Attempt the following; catch errors below
try:
# Import invalidate from app.services.score_cache
from app.services.score_cache import invalidate
# Call invalidate()
invalidate()
# Handle Exception
except Exception as e:
# Log warning: "Score cache invalidation failed: %s", e, exc_info
logger.warning("Score cache invalidation failed: %s", e, exc_info=True)
# Attempt the following; catch errors below
try:
# Call notify_test_state_change()
notify_test_state_change(db, test, "validated")
# Handle Exception
except Exception as e:
# Log warning:
logger.warning(
# Literal argument value
"Notification failed for test %s (validated): %s",
test.id, e, exc_info=True,
)
@@ -516,10 +829,15 @@ def _dispatch_dual_validation_effects(
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
elif event.name == "dual_validation_rejected":
# Attempt the following; catch errors below
try:
# Call notify_test_state_change()
notify_test_state_change(db, test, "rejected")
# Handle Exception
except Exception as e:
# Log warning:
logger.warning(
# Literal argument value
"Notification failed for test %s (rejected): %s",
test.id, e, exc_info=True,
)
@@ -585,6 +903,7 @@ def _notify_validation_conflict(db: Session, test: Test, actor: User | None) ->
)
# Define function handle_remediation_completed
def handle_remediation_completed(db: Session, test: Test, user: User) -> Test | None:
"""Create a re-test when remediation is completed.
@@ -594,121 +913,199 @@ def handle_remediation_completed(db: Session, test: Test, user: User) -> Test |
Prevents infinite loops by enforcing ``MAX_RETEST_COUNT``.
Returns the new retest or *None* if the limit was reached.
Args:
db (Session): Active SQLAlchemy database session.
test (Test): The test whose remediation was completed.
user (User): The user triggering the remediation completion.
Returns:
Test | None: The newly created retest, or ``None`` if the maximum
retest count has been reached.
"""
# Always reference the original test, not an intermediate retest
original_test_id = test.retest_of or test.id
# Check: test.retest_count >= settings.MAX_RETEST_COUNT
if test.retest_count >= settings.MAX_RETEST_COUNT:
# Max retests reached — notify and bail out
if test.created_by:
# Call create_notification()
create_notification(
db,
# Keyword argument: user_id
user_id=test.created_by,
# Keyword argument: type
type="max_retests_reached",
# Keyword argument: title
title="Maximum retests reached",
# Keyword argument: message
message=(
f'Test "{test.name}" has reached the maximum of '
f'{settings.MAX_RETEST_COUNT} retests. Manual review required.'
),
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
)
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="max_retests_reached",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=test.id,
# Keyword argument: details
details={
# Literal argument value
"retest_count": test.retest_count,
# Literal argument value
"max_allowed": settings.MAX_RETEST_COUNT,
# Literal argument value
"original_test_id": str(original_test_id),
},
)
# Return None
return None
# Assign retest = Test(
retest = Test(
# Keyword argument: technique_id
technique_id=test.technique_id,
# Keyword argument: name
name=f"[Retest #{test.retest_count + 1}] {test.name.replace(f'[Retest #{test.retest_count}] ', '')}",
# Keyword argument: description
description=test.description,
# Keyword argument: platform
platform=test.platform,
# Keyword argument: procedure_text
procedure_text=test.procedure_text,
# Keyword argument: tool_used
tool_used=test.tool_used,
# Keyword argument: state
state=TestState.draft,
# Keyword argument: created_by
created_by=test.created_by,
# Keyword argument: retest_of
retest_of=original_test_id,
# Keyword argument: retest_count
retest_count=test.retest_count + 1,
)
# Stage new record(s) for database insertion
db.add(retest)
# Flush changes to DB without committing the transaction
db.flush()
# Call log_action()
log_action(
db,
# Keyword argument: user_id
user_id=user.id,
# Keyword argument: action
action="create_retest",
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=retest.id,
# Keyword argument: details
details={
# Literal argument value
"original_test_id": str(original_test_id),
# Literal argument value
"retest_number": retest.retest_count,
# Literal argument value
"source_test_id": str(test.id),
},
)
# Notify the test creator and any red_tech users
if test.created_by:
# Call create_notification()
create_notification(
db,
# Keyword argument: user_id
user_id=test.created_by,
# Keyword argument: type
type="retest_created",
# Keyword argument: title
title="Re-test created",
# Keyword argument: message
message=(
f'A re-test has been automatically created for "{test.name}" '
f'after remediation was completed.'
),
# Keyword argument: entity_type
entity_type="test",
# Keyword argument: entity_id
entity_id=retest.id,
)
# Flush changes to DB without committing the transaction
db.flush()
# Return retest
return retest
def get_retest_chain(db: Session, test_id) -> list[Test]:
# Define function get_retest_chain
def get_retest_chain(db: Session, test_id: uuid.UUID) -> list[Test]:
"""Return the full chain of retests for a given test.
Includes the original test and all subsequent retests, ordered
by retest_count.
Args:
db (Session): Active SQLAlchemy database session.
test_id (uuid.UUID): UUID of any test in the retest chain.
Returns:
list[Test]: The original test followed by all its retests in
ascending retest-count order. Returns an empty list if the
test is not found.
"""
# Import uuid
import uuid as _uuid
# Assign tid = _uuid.UUID(str(test_id)) if not isinstance(test_id, _uuid.UUID) els...
tid = _uuid.UUID(str(test_id)) if not isinstance(test_id, _uuid.UUID) else test_id
# Find the original test first
test = db.query(Test).filter(Test.id == tid).first()
# Check: not test
if not test:
# Return []
return []
# Assign original_id = test.retest_of or test.id
original_id = test.retest_of or test.id
# Get original
original = db.query(Test).filter(Test.id == original_id).first()
# Check: not original
if not original:
# Return [test]
return [test]
# Get all retests of the original
retests = (
db.query(Test)
# Chain .filter() call
.filter(Test.retest_of == original_id)
# Chain .order_by() call
.order_by(Test.retest_count)
# Chain .all() call
.all()
)
# Return [original] + retests
return [original] + retests
# Define function reopen_test
def reopen_test(db: Session, test: Test, user: User) -> Test:
"""Move a ``rejected`` test back to ``draft`` for continued work.
@@ -719,20 +1116,27 @@ def reopen_test(db: Session, test: Test, user: User) -> Test:
re-validate the updated submission. Phase timing is reset so the timer
starts fresh for the new execution attempt.
"""
# Assign test = transition_state(
test = transition_state(
db, test, TestState.draft, user,
# Keyword argument: action_name
action_name="reopen_test",
)
# Clear validation DECISIONS — leads must re-validate the new attempt.
# Rejection NOTES are intentionally kept so teams see what needs fixing.
test.red_validation_status = None
# Assign test.red_validated_by = None
test.red_validated_by = None
# Assign test.red_validated_at = None
test.red_validated_at = None
# test.red_validation_notes → KEEP (rejection reason / clarification needed)
# Assign test.blue_validation_status = None
test.blue_validation_status = None
# Assign test.blue_validated_by = None
test.blue_validated_by = None
# Assign test.blue_validated_at = None
test.blue_validated_at = None
# test.blue_validation_notes → KEEP (rejection reason / clarification needed)
@@ -749,4 +1153,5 @@ def reopen_test(db: Session, test: Test, user: User) -> Test:
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
# Return test
return test