diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/tasks/test_sender.py | 261 |
1 files changed, 124 insertions, 137 deletions
diff --git a/tests/tasks/test_sender.py b/tests/tasks/test_sender.py index dac56a5..6e33887 100644 --- a/tests/tasks/test_sender.py +++ b/tests/tasks/test_sender.py @@ -90,16 +90,60 @@ def mock_sender_requests( @pytest.fixture def sender_module(app: Flask) -> ModuleType: - # Importing sender registers Huey tasks. The dependency on the - # app fixture guarantees that Huey has been initialized first. _ = app - from webmentions_ssg.tasks import sender return sender @pytest.mark.parametrize( + ("status", "expected"), + [ + pytest.param(200, False, id="success"), + pytest.param(400, False, id="client-error"), + pytest.param(408, True, id="request-timeout"), + pytest.param(425, True, id="too-early"), + pytest.param(429, True, id="too-many-requests"), + pytest.param(500, True, id="server-error-lower-bound"), + pytest.param(599, True, id="server-error-upper-bound"), + pytest.param(600, False, id="outside-server-error-range"), + ], +) +def test_temporary_http_status( + sender_module: ModuleType, status: int, expected: bool +) -> None: + assert sender_module.temporary_http_status(status) is expected + + +@pytest.mark.parametrize( + ("desired_revision", "processed_revision", "revision", "expected"), + [ + pytest.param(2, None, 2, True, id="unprocessed-current"), + pytest.param(2, 1, 2, True, id="older-processed-current"), + pytest.param(2, 2, 2, False, id="already-processed"), + pytest.param(2, 3, 2, False, id="newer-processed"), + pytest.param(3, 1, 2, False, id="superseded"), + ], +) +def test_attempt_is_current( + app: Flask, + sender_module: ModuleType, + desired_revision: int, + processed_revision: int | None, + revision: int, + expected: bool, +) -> None: + identifier = create_sent_webmention( + app, desired_revision=desired_revision, processed_revision=processed_revision + ) + + with app.app_context(): + webmention = db.session.get(SentWebmention, identifier) + assert webmention is not None + assert sender_module.attempt_is_current(webmention, revision) is expected + + +@pytest.mark.parametrize( ("value", "expected"), ( ( @@ -119,8 +163,10 @@ def sender_module(app: Flask) -> ModuleType: ("https://example.com/webmention", {"webmention", "alternate"}), ), ( - "<https://example.com/webmention>; " - 'type="text/html"; rel="webmention"; title="Endpoint"', + ( + '<https://example.com/webmention>; type="text/html"; ' + 'rel="webmention"; title="Endpoint"' + ), ("https://example.com/webmention", {"webmention"}), ), ( @@ -159,7 +205,10 @@ def test_parse_link_value_rejects_malformed_value( ( ( "Link", - "<https://example.com/test/2/webmention?head=true>; rel=webmention", + ( + "<https://example.com/test/2/webmention?head=true>;" + "rel=webmention" + ), ), ), "", @@ -223,7 +272,10 @@ def test_parse_link_value_rejects_malformed_value( ( ( "LinK", - "<https://example.com/test/7/webmention?head=true>; rel=webmention", + ( + "<https://example.com/test/7/webmention?head=true>; " + "rel=webmention" + ), ), ), "", @@ -235,7 +287,10 @@ def test_parse_link_value_rejects_malformed_value( ( ( "Link", - '<https://example.com/test/8/webmention?head=true>; rel="webmention"', + ( + "<https://example.com/test/8/webmention?head=true>; " + 'rel="webmention"' + ), ), ), "", @@ -259,8 +314,10 @@ def test_parse_link_value_rejects_malformed_value( ( ( "Link", - "<https://example.com/test/10/webmention?head=true>; " - 'rel="webmention somethingelse"', + ( + "<https://example.com/test/10/webmention?head=true>; " + 'rel="webmention somethingelse"' + ), ), ), "", @@ -407,8 +464,10 @@ def test_parse_link_value_rejects_malformed_value( ("Link", '<https://example.com/test/18/webmention/error>; rel="other"'), ( "Link", - "<https://example.com/test/18/webmention?head=true>; " - 'rel="webmention"', + ( + "<https://example.com/test/18/webmention?head=true>; " + 'rel="webmention"' + ), ), ), "", @@ -420,10 +479,12 @@ def test_parse_link_value_rejects_malformed_value( ( ( "Link", - "<https://example.com/test/19/webmention/error>; " - 'rel="other", ' - "<https://example.com/test/19/webmention?head=true>; " - 'rel="webmention"', + ( + "<https://example.com/test/19/webmention/error>; " + 'rel="other", ' + "<https://example.com/test/19/webmention?head=true>; " + 'rel="webmention"' + ), ), ), "", @@ -625,7 +686,6 @@ def test_send_webmention_persists_success( assert webmention.status_url == STATUS_URL assert webmention.last_attempted_at is not None assert webmention.last_sent_at is not None - assert not webmention.pending def test_send_webmention_marks_unsupported_target_processed( @@ -663,7 +723,6 @@ def test_send_webmention_marks_unsupported_target_processed( assert webmention.response_status is None assert webmention.status_url is None assert webmention.last_attempted_at is not None - assert not webmention.pending def test_send_webmention_persists_permanent_failure( @@ -693,7 +752,6 @@ def test_send_webmention_persists_permanent_failure( assert webmention.endpoint == ENDPOINT_URL assert webmention.response_status == 400 assert webmention.status_url is None - assert not webmention.pending def test_send_webmention_persists_temporary_failure_and_reraises( @@ -727,7 +785,6 @@ def test_send_webmention_persists_temporary_failure_and_reraises( assert webmention.endpoint == ENDPOINT_URL assert webmention.response_status == 503 assert webmention.status_url is None - assert webmention.pending def test_send_webmention_ignores_processed_revision( @@ -750,14 +807,17 @@ def test_send_webmention_ignores_processed_revision( post.assert_not_called() -def test_send_webmention_ignores_result_for_superseded_revision( - app: Flask, sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch +@pytest.mark.parametrize( + "outcome", ["success", "unsupported", "temporary-failure", "permanent-failure"] +) +def test_send_webmention_ignores_superseded_attempt( + app: Flask, sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch, outcome: str ) -> None: identifier = create_sent_webmention(app) - _, _, _, post = mock_sender_requests(sender_module, monkeypatch) + _, _, discover, post = mock_sender_requests(sender_module, monkeypatch) - def supersede_revision(*args, **kwargs) -> tuple[int, None]: + def supersede_revision() -> None: webmention = db.session.get(SentWebmention, identifier) assert webmention is not None @@ -765,9 +825,40 @@ def test_send_webmention_ignores_result_for_superseded_revision( webmention.desired_revision = 2 db.session.commit() - return (202, None) + match outcome: + case "success": + + def post_success(*args, **kwargs) -> tuple[int, None]: + supersede_revision() + return (202, None) + + post.side_effect = post_success + + case "unsupported": + + def discover_unsupported(*args, **kwargs) -> None: + supersede_revision() + + discover.side_effect = discover_unsupported - post.side_effect = supersede_revision + case "temporary-failure": + + def post_temporary_failure(*args, **kwargs) -> None: + supersede_revision() + raise sender_module.TemporarySenderError("Temporary failure") + + post.side_effect = post_temporary_failure + + case "permanent-failure": + + def post_permanent_failure(*args, **kwargs) -> tuple[int, None]: + supersede_revision() + return (400, None) + + post.side_effect = post_permanent_failure + + case _: + raise AssertionError(f"Unexpected outcome: {outcome}") sender_module.send_webmention.call_local(identifier) @@ -779,17 +870,14 @@ def test_send_webmention_ignores_result_for_superseded_revision( assert webmention.processed_revision is None assert webmention.sent_revision is None assert webmention.status is None - assert webmention.pending def test_send_webmention_ignores_unknown_identifier( sender_module: ModuleType, caplog: pytest.LogCaptureFixture ) -> None: identifier = uuid.uuid7() - with caplog.at_level(logging.WARNING): sender_module.send_webmention.call_local(identifier) - assert f"Cannot send unknown SentWebmention {identifier}" in caplog.text @@ -798,9 +886,7 @@ def test_ensure_public_request_accepts_public_url( ) -> None: is_public_url = Mock(return_value=True) monkeypatch.setattr(sender_module, "is_public_url", is_public_url) - sender_module.ensure_public_request(httpx.Request("GET", TARGET_URL)) - is_public_url.assert_called_once_with(TARGET_URL) @@ -809,13 +895,11 @@ def test_ensure_public_request_rejects_non_public_address( ) -> None: is_public_url = Mock(return_value=False) monkeypatch.setattr(sender_module, "is_public_url", is_public_url) - with pytest.raises( sender_module.PermanentSenderError, match="Request resolves to a non-public address", ): sender_module.ensure_public_request(httpx.Request("GET", TARGET_URL)) - is_public_url.assert_called_once_with(TARGET_URL) @@ -826,23 +910,25 @@ def test_ensure_public_request_maps_dns_failure_to_temporary_error( side_effect=AddressResolutionError("Could not resolve hostname") ) monkeypatch.setattr(sender_module, "is_public_url", is_public_url) - with pytest.raises( sender_module.TemporarySenderError, match="Request hostname could not be resolved", ): sender_module.ensure_public_request(httpx.Request("GET", TARGET_URL)) - is_public_url.assert_called_once_with(TARGET_URL) -def test_resolve_endpoint_rejects_non_http_url(sender_module: ModuleType) -> None: +def test_resolve_endpoint_rejects_invalid_url( + sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch +) -> None: + is_http_url = Mock(return_value=False) + monkeypatch.setattr(sender_module, "is_http_url", is_http_url) response = httpx.Response(200, request=httpx.Request("GET", TARGET_URL)) - with pytest.raises( sender_module.PermanentSenderError, match="Invalid Webmention endpoint" ): - sender_module.resolve_endpoint(response, "mailto:example@example.com") + sender_module.resolve_endpoint(response, "/webmention") + is_http_url.assert_called_once_with(ENDPOINT_URL) def test_endpoint_from_headers_ignores_malformed_link( @@ -853,7 +939,6 @@ def test_endpoint_from_headers_ignores_malformed_link( headers=[("Link", "not-a-link"), ("Link", "</webmention>; rel=webmention")], request=httpx.Request("GET", TARGET_URL), ) - assert sender_module.endpoint_from_headers(response) == ENDPOINT_URL @@ -1065,101 +1150,3 @@ def test_post_webmention( assert form["source"] == SOURCE_URL assert form["target"] == TARGET_URL - - -def test_send_webmention_ignores_unsupported_result_for_superseded_revision( - app: Flask, sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch -) -> None: - identifier = create_sent_webmention(app) - - _, _, discover, post = mock_sender_requests( - sender_module, monkeypatch, endpoint=None - ) - - def supersede_revision(*args, **kwargs) -> None: - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - - webmention.desired_revision = 2 - db.session.commit() - - discover.side_effect = supersede_revision - - sender_module.send_webmention.call_local(identifier) - - post.assert_not_called() - - with app.app_context(): - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - assert webmention.desired_revision == 2 - assert webmention.processed_revision is None - assert webmention.sent_revision is None - assert webmention.status is None - assert webmention.pending - - -def test_send_webmention_ignores_temporary_failure_for_superseded_revision( - app: Flask, sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch -) -> None: - identifier = create_sent_webmention(app) - - _, _, _, post = mock_sender_requests(sender_module, monkeypatch) - - def supersede_revision(*args, **kwargs) -> tuple[int, None]: - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - - webmention.desired_revision = 2 - db.session.commit() - - raise sender_module.TemporarySenderError("Temporary failure") - - post.side_effect = supersede_revision - - sender_module.send_webmention.call_local(identifier) - - with app.app_context(): - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - assert webmention.desired_revision == 2 - assert webmention.processed_revision is None - assert webmention.sent_revision is None - assert webmention.status is None - assert webmention.pending - - -def test_send_webmention_ignores_permanent_failure_for_superseded_revision( - app: Flask, sender_module: ModuleType, monkeypatch: pytest.MonkeyPatch -) -> None: - identifier = create_sent_webmention(app) - - _, _, _, post = mock_sender_requests(sender_module, monkeypatch) - - def supersede_revision(*args, **kwargs) -> tuple[int, None]: - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - - webmention.desired_revision = 2 - db.session.commit() - - return (400, None) - - post.side_effect = supersede_revision - - sender_module.send_webmention.call_local(identifier) - - with app.app_context(): - webmention = db.session.get(SentWebmention, identifier) - - assert webmention is not None - assert webmention.desired_revision == 2 - assert webmention.processed_revision is None - assert webmention.sent_revision is None - assert webmention.status is None - assert webmention.pending |
