diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/tasks/test_extension.py | 175 | ||||
| -rw-r--r-- | tests/tasks/test_receiver.py | 5 | ||||
| -rw-r--r-- | tests/tasks/test_scanner.py | 97 | ||||
| -rw-r--r-- | tests/tasks/test_sender.py | 261 |
4 files changed, 322 insertions, 216 deletions
diff --git a/tests/tasks/test_extension.py b/tests/tasks/test_extension.py new file mode 100644 index 0000000..5e9cdfa --- /dev/null +++ b/tests/tasks/test_extension.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> +# +# SPDX-License-Identifier: BSD-3-Clause + +from collections.abc import Callable +from typing import Any + +import huey as huey_package +import pytest +from flask import Flask, has_app_context +from huey import crontab + +from webmentions_ssg.tasks.extension import Huey + + +def test_init_without_app() -> None: + huey = Huey() + assert huey.app is None + with pytest.raises(RuntimeError, match="Huey has not been initialized"): + _ = huey.huey + + +def test_init_with_app(app: Flask) -> None: + huey = Huey(app) + assert huey.app is app + assert huey.huey.name == app.import_name + assert huey.huey.results is True + assert huey.huey.store_none is False + assert huey.huey.utc is True + assert huey.huey.immediate is True + assert app.extensions["huey"] is huey + + +def test_init_app_uses_huey_configuration(app: Flask) -> None: + app.config["HUEY"] = { + "url": "memory://", + "name": "custom-name", + "results": False, + "store_none": True, + "utc": False, + "immediate": False, + } + + huey = Huey() + huey.init_app(app) + + assert huey.huey.name == "custom-name" + assert huey.huey.results is False + assert huey.huey.store_none is True + assert huey.huey.utc is False + assert huey.huey.immediate is False + + +def test_huey_url_takes_precedence_over_huey_configuration(app: Flask) -> None: + app.config["HUEY"] = {"url": "blackhole://"} + app.config["HUEY_URL"] = "memory://" + + huey = Huey(app) + + assert type(huey.huey).__name__ == "MemoryHuey" + + +def task_decorator(huey: Huey, periodic: bool) -> Callable[[Callable[..., Any]], Any]: + if periodic: + return huey.periodic_task(crontab(minute="*")) + + return huey.task() + + +@pytest.mark.parametrize("periodic", [False, True], ids=["task", "periodic-task"]) +def test_task_runs_in_app_context(app: Flask, periodic: bool) -> None: + huey = Huey(app) + + @task_decorator(huey, periodic) + def add(a: int, b: int) -> int: + assert has_app_context() + return a + b + + assert add.call_local(2, 3) == 5 + + +@pytest.mark.parametrize("periodic", [False, True], ids=["task", "periodic-task"]) +def test_task_raises_without_app(app: Flask, periodic: bool) -> None: + huey = Huey(app) + + @task_decorator(huey, periodic) + def task() -> None: + pass + + huey.app = None + + with pytest.raises(RuntimeError, match="Flask app is not available"): + task.call_local() + + +def test_getattr_delegates_to_huey(app: Flask) -> None: + huey = Huey(app) + assert huey.immediate is True + + +@pytest.mark.parametrize( + ("url", "backend_name", "storage_kwargs"), + [ + ("redis://localhost:6379/0", "RedisHuey", {"url": "redis://localhost:6379/0"}), + ( + "rediss://localhost:6379/0", + "RedisHuey", + {"url": "rediss://localhost:6379/0"}, + ), + ( + "redis+priority://localhost:6379/0", + "PriorityRedisHuey", + {"url": "redis://localhost:6379/0"}, + ), + ( + "redis+expire://localhost:6379/0", + "RedisExpireHuey", + {"url": "redis://localhost:6379/0"}, + ), + ( + "redis+priority+expire://localhost:6379/0", + "PriorityRedisExpireHuey", + {"url": "redis://localhost:6379/0"}, + ), + ("sqlite:///var/huey.db", "SqliteHuey", {"filename": "var/huey.db"}), + ("file:///var/huey-queue", "FileHuey", {"path": "var/huey-queue"}), + ( + "postgres://user:password@localhost/database", + "PostgresHuey", + {"dsn": "postgres://user:password@localhost/database"}, + ), + ( + "postgresql://user:password@localhost/database", + "PostgresHuey", + {"dsn": "postgresql://user:password@localhost/database"}, + ), + ("memory://", "MemoryHuey", {}), + ("blackhole://", "BlackHoleHuey", {}), + ], +) +def test_backend_from_url( + monkeypatch: pytest.MonkeyPatch, + url: str, + backend_name: str, + storage_kwargs: dict[str, str], +) -> None: + backend_class = type(f"Test{backend_name}", (), {}) + + monkeypatch.setattr(huey_package, backend_name, backend_class) + + actual_class, actual_storage_kwargs = Huey.backend_from_url(url) + + assert actual_class is backend_class + assert actual_storage_kwargs == storage_kwargs + + +@pytest.mark.parametrize( + ("url", "message"), + [ + ( + "sqlite://var/huey.db", + "SQLite Huey URLs must look like sqlite:///var/huey.db", + ), + ("sqlite:///", "SQLite Huey URL must include a database path"), + ( + "file://var/huey-queue", + "File Huey URLs must look like file:///var/huey-queue", + ), + ("file:///", "File Huey URL must include a directory path"), + ("amqp://localhost", "Unsupported HUEY_URL scheme: 'amqp'"), + ], +) +def test_backend_from_url_rejects_invalid_url(url: str, message: str) -> None: + with pytest.raises(RuntimeError, match=message): + Huey.backend_from_url(url) diff --git a/tests/tasks/test_receiver.py b/tests/tasks/test_receiver.py index 17dd45a..daa271d 100644 --- a/tests/tasks/test_receiver.py +++ b/tests/tasks/test_receiver.py @@ -54,9 +54,7 @@ def create_webmention( def get_webmention_state(app: Flask, identifier: uuid.UUID) -> tuple[str, str | None]: with app.app_context(): webmention = db.session.get(ReceivedWebmention, identifier) - assert webmention is not None - return (webmention.status, webmention.failure_reason) @@ -433,9 +431,7 @@ def test_verify_webmention_marks_row_verifying_before_check( return True source_mentions_target.side_effect = verify_source - receiver.verify_webmention.call_local(identifier) - assert get_webmention_state(app, identifier) == ("verified", None) @@ -521,7 +517,6 @@ def test_ensure_public_request_accepts_public_url( is_public_url: Mock, receiver: ModuleType ) -> None: receiver.ensure_public_request(httpx.Request("GET", SOURCE_URL)) - is_public_url.assert_called_once_with(SOURCE_URL) diff --git a/tests/tasks/test_scanner.py b/tests/tasks/test_scanner.py index 332b8bf..3de5e16 100644 --- a/tests/tasks/test_scanner.py +++ b/tests/tasks/test_scanner.py @@ -4,6 +4,7 @@ from pathlib import Path from types import ModuleType +from unittest.mock import Mock from uuid import UUID import pytest @@ -20,13 +21,11 @@ SOURCE_URL = f"{BASE_URL}example/" @pytest.fixture -def scanner_module(app: Flask) -> ModuleType: - # Importing scanner registers Huey tasks. The dependency on the - # app fixture guarantees that Huey has been initialized first. +def scanner_module(app: Flask, monkeypatch: pytest.MonkeyPatch) -> ModuleType: _ = app - from webmentions_ssg.tasks import scanner + monkeypatch.setattr(scanner, "is_http_url", Mock(return_value=True)) return scanner @@ -96,11 +95,6 @@ def configure_scanner(app: Flask, root: Path, *, base_url: str | None = None) -> app.config["WEBMENTIONS_SSG_SOURCE_BASE_URL"] = base_url -# --------------------------------------------------------------------------- -# Microformats parsing -# --------------------------------------------------------------------------- - - def test_parse_entry_returns_microformats_entry(scanner_module: ModuleType) -> None: document = parse_html( f""" @@ -277,11 +271,6 @@ def test_primary_entry_rejects_nonmatching_u_url(scanner_module: ModuleType) -> scanner_module.primary_entry(document, SOURCE_URL) -# --------------------------------------------------------------------------- -# e-content -# --------------------------------------------------------------------------- - - def test_content_element_returns_e_content(scanner_module: ModuleType) -> None: document = parse_html( """ @@ -346,11 +335,6 @@ def test_content_element_rejects_multiple_e_content(scanner_module: ModuleType) scanner_module.content_element(entry) -# --------------------------------------------------------------------------- -# Canonical URLs -# --------------------------------------------------------------------------- - - def test_canonical_url_returns_absolute_url(scanner_module: ModuleType) -> None: document = parse_html( f""" @@ -393,6 +377,8 @@ def test_canonical_url_returns_none_when_missing(scanner_module: ModuleType) -> def test_canonical_url_resolves_relative_url_with_base_url( scanner_module: ModuleType, ) -> None: + scanner_module.is_http_url.side_effect = [False, True] + document = parse_html( """ <html> @@ -414,6 +400,8 @@ def test_canonical_url_resolves_relative_url_with_base_url( def test_relative_canonical_requires_base_url(scanner_module: ModuleType) -> None: + scanner_module.is_http_url.return_value = False + document = parse_html( """ <html> @@ -453,11 +441,6 @@ def test_empty_canonical_is_rejected(scanner_module: ModuleType) -> None: ) -# --------------------------------------------------------------------------- -# Target extraction -# --------------------------------------------------------------------------- - - def test_scan_source_uses_canonical_without_base_url( scanner_module: ModuleType, tmp_path: Path ) -> None: @@ -657,24 +640,6 @@ def test_scan_source_extracts_reaction_properties( assert scanned.targets == frozenset({target}) -def test_scan_source_extracts_anchor_from_e_content( - scanner_module: ModuleType, tmp_path: Path -) -> None: - path = write_post( - tmp_path, - "example", - """ - <a href="https://example.com/target"> - Target - </a> - """, - ) - - scanned = scanner_module.scan_source_file(path, root=tmp_path, base_url=None) - - assert scanned.targets == frozenset({"https://example.com/target"}) - - @pytest.mark.parametrize("tag", ("link", "area")) def test_scan_source_ignores_non_anchor_href_elements( scanner_module: ModuleType, tmp_path: Path, tag: str @@ -714,11 +679,6 @@ def test_scan_source_ignores_self_fragment( assert scanned.targets == frozenset({"https://example.com/target"}) -# --------------------------------------------------------------------------- -# Database reconciliation -# --------------------------------------------------------------------------- - - def test_scan_creates_source_and_webmentions( app: Flask, scanner_module: ModuleType, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: @@ -900,7 +860,6 @@ def test_updated_source_increments_revision( assert webmention.desired_revision == 2 assert webmention.processed_revision == 1 assert webmention.sent_revision == 1 - assert webmention.pending identifier = webmention.uuid @@ -972,7 +931,6 @@ def test_new_target_is_added_on_update( assert webmention.desired_revision == 2 assert webmention.processed_revision is None assert webmention.sent_revision is None - assert webmention.pending identifier = webmention.uuid @@ -1055,7 +1013,6 @@ def test_removed_sent_target_is_queued_again( assert webmention.desired_revision == 2 assert webmention.processed_revision == 1 assert webmention.sent_revision == 1 - assert webmention.pending assert queued == [identifier] @@ -1124,16 +1081,10 @@ def test_removed_unsent_target_is_not_queued( assert webmention.desired_revision == 2 assert webmention.processed_revision == 2 assert webmention.sent_revision is None - assert not webmention.pending assert queued == [] -# --------------------------------------------------------------------------- -# Source deletion/restoration -# --------------------------------------------------------------------------- - - def test_deleted_source_queues_previously_sent_webmention( app: Flask, scanner_module: ModuleType, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: @@ -1186,7 +1137,6 @@ def test_deleted_source_queues_previously_sent_webmention( assert webmention.desired_revision == 2 assert webmention.processed_revision == 1 assert webmention.sent_revision == 1 - assert webmention.pending assert queued == [identifier] @@ -1231,7 +1181,6 @@ def test_deleted_source_does_not_queue_unsent_webmention( assert webmention.desired_revision == 2 assert webmention.processed_revision == 2 assert webmention.sent_revision is None - assert not webmention.pending assert queued == [] @@ -1298,18 +1247,12 @@ def test_restored_source_creates_new_revision( assert webmention.active assert webmention.desired_revision == 3 assert webmention.sent_revision == 1 - assert webmention.pending identifier = webmention.uuid assert queued == [identifier] -# --------------------------------------------------------------------------- -# Queue recovery and invalid sources -# --------------------------------------------------------------------------- - - def test_pending_webmention_is_requeued_on_next_scan( app: Flask, scanner_module: ModuleType, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: @@ -1436,6 +1379,8 @@ def test_scan_sources_accepts_valid_base_url( def test_scan_sources_rejects_invalid_base_url( app: Flask, scanner_module: ModuleType, tmp_path: Path ) -> None: + scanner_module.is_http_url.return_value = False + configure_scanner(app, tmp_path, base_url="not-a-url") with pytest.raises(RuntimeError, match="absolute HTTP or HTTPS URL"): @@ -1565,6 +1510,8 @@ def test_scan_source_ignores_reaction_to_matching_hostname( def test_canonical_url_rejects_non_http_resolved_url( scanner_module: ModuleType, ) -> None: + scanner_module.is_http_url.return_value = False + document = parse_html( """ <html> @@ -1634,19 +1581,21 @@ def test_parse_entry_rejects_invalid_parser_output( scanner_module.parse_entry(element, SOURCE_URL) -@pytest.mark.parametrize( - "value", - [ - pytest.param(" ", id="empty"), - pytest.param("mailto:example@example.com", id="non-http"), - ], -) -def test_normalize_target_ignores_invalid_target( - scanner_module: ModuleType, value: str +def test_normalize_target_ignores_empty_target(scanner_module: ModuleType) -> None: + assert ( + scanner_module.normalize_target(" ", base_url=SOURCE_URL, source_url=SOURCE_URL) + is None + ) + + +def test_normalize_target_ignores_url_rejected_by_validator( + scanner_module: ModuleType, ) -> None: + scanner_module.is_http_url.return_value = False + assert ( scanner_module.normalize_target( - value, base_url=SOURCE_URL, source_url=SOURCE_URL + "https://example.com/target", base_url=SOURCE_URL, source_url=SOURCE_URL ) is None ) 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 |
