import json from io import BytesIO from typing import Any from unittest.mock import MagicMock, patch from urllib.error import HTTPError, URLError import pytest from src.worker.task_protection import TaskProtectionResult, disable, enable _AGENT_URI = "http://169.254.170.2/api/abc123" _EXPECTED_URL = f"{_AGENT_URI}/task-protection/v1/state" @pytest.fixture(autouse=True) def _set_agent_uri(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("ECS_AGENT_URI", _AGENT_URI) def _json_response(body: dict[str, Any]) -> MagicMock: response = MagicMock() response.read.return_value = json.dumps(body).encode() response.__enter__.return_value = response response.__exit__.return_value = None return response def _http_error(code: int, body: bytes = b"") -> HTTPError: return HTTPError(_EXPECTED_URL, code, "err", hdrs=None, fp=BytesIO(body)) # type: ignore[arg-type] class TestEnable: def test_sends_put_with_expires_in_minutes(self) -> None: mock_urlopen = MagicMock(return_value=_json_response({})) with patch("src.worker.task_protection.urlopen", mock_urlopen): result = enable(expires_in_minutes=60) assert result == TaskProtectionResult.ENABLED request = mock_urlopen.call_args.args[0] assert request.full_url == _EXPECTED_URL assert request.get_method() == "PUT" assert request.headers["Content-type"] == "application/json" assert json.loads(request.data) == {"ProtectionEnabled": True, "ExpiresInMinutes": 60} @pytest.mark.parametrize( ("reason", "expected"), [ pytest.param("DEPLOYMENT_BLOCKED", TaskProtectionResult.DEPLOYMENT_BLOCKED, id="deployment_blocked"), pytest.param( "TASK_STOPPING_OR_STOPPED", TaskProtectionResult.TASK_STOPPING_OR_STOPPED, id="task_stopping_or_stopped" ), pytest.param("TASK_NOT_VALID", TaskProtectionResult.FAILED, id="task_not_valid"), pytest.param("MISSING", TaskProtectionResult.FAILED, id="missing"), pytest.param("UNKNOWN_FUTURE_REASON", TaskProtectionResult.FAILED, id="unknown_reason"), ], ) def test_failure_reason_mapping(self, reason: str, expected: TaskProtectionResult) -> None: body = {"failure": {"Reason": reason}} with patch("src.worker.task_protection.urlopen", return_value=_json_response(body)): result = enable(expires_in_minutes=60) assert result == expected @pytest.mark.parametrize( "error", [ pytest.param(URLError("connection refused"), id="network_error"), pytest.param(_http_error(500), id="http_5xx"), pytest.param(_http_error(429), id="http_429"), pytest.param(_http_error(403), id="http_4xx"), ], ) def test_agent_call_failure_returns_failed(self, error: Exception) -> None: with patch("src.worker.task_protection.urlopen", side_effect=error): result = enable(expires_in_minutes=60) assert result == TaskProtectionResult.FAILED @pytest.mark.parametrize( "failure", [ pytest.param({}, id="empty_failure"), pytest.param(None, id="null_failure"), pytest.param("error", id="string_failure"), pytest.param({"detail": "no reason field"}, id="failure_without_reason"), ], ) def test_malformed_failure_shape_returns_failed(self, failure: object) -> None: body = {"failure": failure} with patch("src.worker.task_protection.urlopen", return_value=_json_response(body)): result = enable(expires_in_minutes=60) assert result == TaskProtectionResult.FAILED class TestDisable: def test_sends_put_protection_disabled(self) -> None: mock_urlopen = MagicMock(return_value=_json_response({})) with patch("src.worker.task_protection.urlopen", mock_urlopen): disable() request = mock_urlopen.call_args.args[0] assert request.full_url == _EXPECTED_URL assert request.get_method() == "PUT" assert json.loads(request.data) == {"ProtectionEnabled": False} def test_returns_true_on_empty_success_response(self) -> None: with patch("src.worker.task_protection.urlopen", return_value=_json_response({})): assert disable() is True def test_returns_false_on_failure_in_response(self) -> None: with patch("src.worker.task_protection.urlopen", return_value=_json_response({"failure": {"reason": "x"}})): assert disable() is False def test_returns_false_on_network_error(self) -> None: with patch("src.worker.task_protection.urlopen", side_effect=URLError("connection refused")): assert disable() is False