from typing import Any import pytest from aws_testing_utils.lambda_handler import LambdaHandler from test_fixtures.mysql import MySQLConnection from config import FINALIZE_JOB_FUNCTION_NAME pytestmark = pytest.mark.lambda_name("finalize-job") def test_marks_job_completed( art_relations_db: MySQLConnection, transfer_job: dict[str, Any], lambda_handler: LambdaHandler, ) -> None: lambda_handler.invoke(FINALIZE_JOB_FUNCTION_NAME, {"job_id": transfer_job["job_id"]}) job = art_relations_db.fetchone( "SELECT status, transfer_completed_on FROM project_transfer_job WHERE job_id = %s", (transfer_job["job_id"],), ) assert job is not None assert job["status"] == "COMPLETED" assert job["transfer_completed_on"] is not None def test_patch_on_already_completed_job_surfaces_function_error( transfer_job: dict[str, Any], lambda_handler: LambdaHandler, ) -> None: """Regression test for ows-project-manager's terminal-state guard. PM's PATCH /transfer/job/{id} refuses to update jobs already in a terminal state (confirmed for FAILED in test_handle_error; the same guard is expected for COMPLETED). The lambda surfaces that rejection as a FunctionError on the AWS response — the signal Step Functions uses for error routing, so a runaway SFN retry can't silently re-finalize a job and clobber transfer_completed_on. If PM ever changes to allow re-PATCH on COMPLETED, this test catches the change. """ job_id = transfer_job["job_id"] # First invocation: QUEUED → COMPLETED. lambda_handler.invoke(FINALIZE_JOB_FUNCTION_NAME, {"job_id": job_id}) # Second invocation: PM rejects → lambda raises → FunctionError. response = lambda_handler.invoke( FINALIZE_JOB_FUNCTION_NAME, {"job_id": job_id}, assertion=False, ) assert response["StatusCode"] == 200 assert "FunctionError" in response, ( f"Expected lambda to surface PM's already-COMPLETED guard as a FunctionError " f"for job {job_id}, but response had none: " f"{ {k: v for k, v in response.items() if k != 'Payload'} }" ) def test_function_error_when_job_id_does_not_exist( lambda_handler: LambdaHandler, ) -> None: """Generic error-propagation check: PM's PATCH on a non-existent job_id returns 4xx, which the lambda must surface as a FunctionError so SFN can route to its error handler rather than silently swallow the failure. Uses a job_id chosen high enough that no real QA job is expected to have it. No fixture is needed — the lambda fails before any DB mutation. """ nonexistent_job_id = 2_000_000_000 response = lambda_handler.invoke( FINALIZE_JOB_FUNCTION_NAME, {"job_id": nonexistent_job_id}, assertion=False, ) assert response["StatusCode"] == 200 assert "FunctionError" in response, ( f"Expected lambda to surface PM's 4xx for nonexistent job {nonexistent_job_id} " f"as a FunctionError, but response had none: " f"{ {k: v for k, v in response.items() if k != 'Payload'} }" )