aboutsummaryrefslogtreecommitdiff
path: root/tests/tasks/test_sender.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/tasks/test_sender.py261
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