"""Task logic unit tests.""" import json import uuid from unittest.mock import AsyncMock import pytest from fastapi import HTTPException from pydantic import HttpUrl from pytest_mock import MockerFixture from product_staging.logic import task as task_logic from product_staging.logic.utils import fargate from product_staging.models import task as task_model from product_staging.models.task import Task async def test_create_template_task(mocker: MockerFixture) -> None: """Verify create_template_task persists a task, launches fargate, and returns the token.""" identity_uuid = uuid.uuid4() expected_token = uuid.uuid4() expected_task = Task( token=expected_token, status="in_progress", created_by=identity_uuid, created_at=1744300000.0, ) artist_urls: list[HttpUrl] = [ HttpUrl("https://open.spotify.com/artist/abc123"), HttpUrl("https://open.spotify.com/artist/def456"), ] album_urls: list[HttpUrl] = [ HttpUrl("https://open.spotify.com/album/xyz789"), ] mock_create_task = mocker.patch.object( task_model, "create_task", new_callable=AsyncMock, return_value=expected_task, ) mock_run_task = mocker.patch.object( fargate, "run_task", return_value="arn:aws:ecs:us-east-1:123:task/abc", ) mocker.patch("product_staging.logic.task.config.ENVIRONMENT", "test") mocker.patch( "product_staging.logic.task.config.SPOTIFY_EXPORT_CATALOG_JOB_NAME", "spotify-export", ) result = await task_logic.create_template_task( identity_uuid=identity_uuid, artist_urls=artist_urls, album_urls=album_urls, ) assert result == expected_token mock_create_task.assert_called_once_with(identity_uuid=identity_uuid) mock_run_task.assert_called_once_with( task_name="test-spotify-export", container_name="spotify-export", env_vars={ "TASK_TOKEN": str(expected_token), "IDENTITY_UUID": str(identity_uuid), "ARTISTS": json.dumps([str(u) for u in artist_urls]), "ALBUMS": json.dumps([str(u) for u in album_urls]), }, ) async def test_get_template_task_status_in_progress_before_timeout( mocker: MockerFixture, ) -> None: """Verify in-progress tasks are not timed out before 15 minutes elapse.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="in_progress", created_by=identity_uuid, created_at=1000.0, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) mocker.patch("product_staging.logic.task.time.time", return_value=1899.0) result = await task_logic.get_template_task_status( token=token, identity_uuid=identity_uuid, ) assert result.status == "in_progress" async def test_get_template_task_status_timeout_at_boundary( mocker: MockerFixture, ) -> None: """Verify timeout is triggered exactly at the 15-minute boundary.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="in_progress", created_by=identity_uuid, created_at=1000.0, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) mocker.patch("product_staging.logic.task.time.time", return_value=1900.0) result = await task_logic.get_template_task_status( token=token, identity_uuid=identity_uuid, ) assert result.status == "timeout" async def test_get_template_task_status_timeout_after_boundary( mocker: MockerFixture, ) -> None: """Verify timeout status is returned once elapsed time exceeds the timeout.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="in_progress", created_by=identity_uuid, created_at=1000.0, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) mocker.patch("product_staging.logic.task.time.time", return_value=1901.0) result = await task_logic.get_template_task_status( token=token, identity_uuid=identity_uuid, ) assert result.status == "timeout" async def test_get_template_task_status_success_returns_download_url( mocker: MockerFixture, ) -> None: """Verify successful tasks return success status and a download URL.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="success", created_by=identity_uuid, created_at=1000.0, payload={"key": "file.xlsx"}, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) mock_generate_download_link = mocker.patch.object( task_logic.s3, "generate_download_link", new_callable=AsyncMock, return_value="https://example.com/download", ) result = await task_logic.get_template_task_status( token=token, identity_uuid=identity_uuid, ) assert result.status == "success" assert isinstance(result.download_url, HttpUrl) assert str(result.download_url) == "https://example.com/download" mock_generate_download_link.assert_called_once_with( key="file.xlsx", filename="file.xlsx" ) async def test_get_template_task_status_failure_returns_failed( mocker: MockerFixture, ) -> None: """Verify failed tasks return failure status.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="failure", created_by=identity_uuid, created_at=1000.0, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) result = await task_logic.get_template_task_status( token=token, identity_uuid=identity_uuid, ) assert result.status == "failure" async def test_get_template_task_status_returns_404_when_identity_mismatch( mocker: MockerFixture, ) -> None: """Verify status lookup is not exposed to identities that do not own the task.""" token = uuid.uuid4() task_owner_identity_uuid = uuid.uuid4() requesting_identity_uuid = uuid.uuid4() mock_task = Task( token=token, status="in_progress", created_by=task_owner_identity_uuid, created_at=1000.0, ) mocker.patch.object( task_model, "get_task", new_callable=AsyncMock, return_value=mock_task, ) with pytest.raises(HTTPException) as exc_info: await task_logic.get_template_task_status( token=token, identity_uuid=requesting_identity_uuid, ) assert exc_info.value.status_code == 404 async def test_mark_task_success(mocker: MockerFixture) -> None: """Verify mark_task_success completes the task and returns payload.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() payload = {"key": "value"} mock_complete_task = mocker.patch.object( task_model, "complete_task", new_callable=AsyncMock, return_value=Task( token=token, status="success", created_by=identity_uuid, created_at=1744300000.0, payload=payload, ), ) result = await task_logic.mark_task_success( token=token, identity_uuid=identity_uuid, payload=payload, ) assert result == payload mock_complete_task.assert_called_once_with(token=token, payload=payload) async def test_mark_task_success_returns_404_when_identity_mismatch( mocker: MockerFixture, ) -> None: """Verify mark_task_success returns 404 when requester does not own the task.""" token = uuid.uuid4() task_owner_identity_uuid = uuid.uuid4() requesting_identity_uuid = uuid.uuid4() payload = {"key": "value"} mock_complete_task = mocker.patch.object( task_model, "complete_task", new_callable=AsyncMock, return_value=Task( token=token, status="success", created_by=task_owner_identity_uuid, created_at=1744300000.0, payload=payload, ), ) with pytest.raises(HTTPException) as exc_info: await task_logic.mark_task_success( token=token, identity_uuid=requesting_identity_uuid, payload=payload, ) assert exc_info.value.status_code == 404 mock_complete_task.assert_called_once_with(token=token, payload=payload) async def test_mark_task_failure(mocker: MockerFixture) -> None: """Verify mark_task_failure fails the task and returns payload.""" token = uuid.uuid4() identity_uuid = uuid.uuid4() payload = { "error": "Task failed due to error", "details": "Additional error details", } mock_fail_task = mocker.patch.object( task_model, "fail_task", new_callable=AsyncMock, return_value=Task( token=token, status="failure", created_by=identity_uuid, created_at=1744300000.0, payload=payload, ), ) result = await task_logic.mark_task_failure( token=token, identity_uuid=identity_uuid, payload=payload, ) assert result == payload mock_fail_task.assert_called_once_with(token=token, payload=payload) async def test_mark_task_failure_returns_404_when_identity_mismatch( mocker: MockerFixture, ) -> None: """Verify mark_task_failure returns 404 when requester does not own the task.""" token = uuid.uuid4() task_owner_identity_uuid = uuid.uuid4() requesting_identity_uuid = uuid.uuid4() payload = { "error": "Task failed due to error", "details": "Additional error details", } mock_fail_task = mocker.patch.object( task_model, "fail_task", new_callable=AsyncMock, return_value=Task( token=token, status="failure", created_by=task_owner_identity_uuid, created_at=1744300000.0, payload=payload, ), ) with pytest.raises(HTTPException) as exc_info: await task_logic.mark_task_failure( token=token, identity_uuid=requesting_identity_uuid, payload=payload, ) assert exc_info.value.status_code == 404 mock_fail_task.assert_called_once_with(token=token, payload=payload)