aboutsummaryrefslogtreecommitdiff
path: root/tests/tasks/test_receiver.py
diff options
context:
space:
mode:
authorDennis Fink2026-08-10 20:00:28 +0200
committerDennis Fink2026-08-10 20:00:28 +0200
commit5b39f70aa14cac635b6084170e9d0bd3f548b911 (patch)
treedaef3fddff0679efb23a42a58743e7c2a3597213 /tests/tasks/test_receiver.py
parent8350bef3e3baea6042ed3780a054cc65707e9f8d (diff)
downloadwebmentions-ssg-5b39f70aa14cac635b6084170e9d0bd3f548b911.tar.gz
webmentions-ssg-5b39f70aa14cac635b6084170e9d0bd3f548b911.zip
test: simplify mocks and localize test fixtures
Replace shared HTTPX and DNS fixtures with targeted unittest.mock patches close to the tests that need them. Keep conftest focused on application and client setup, while moving receiver-specific helpers into the receiver test module.
Diffstat (limited to '')
-rw-r--r--tests/tasks/test_receiver.py625
1 files changed, 241 insertions, 384 deletions
diff --git a/tests/tasks/test_receiver.py b/tests/tasks/test_receiver.py
index 4545121..19630c3 100644
--- a/tests/tasks/test_receiver.py
+++ b/tests/tasks/test_receiver.py
@@ -1,7 +1,7 @@
import logging
import uuid
-from collections.abc import Callable
from types import ModuleType
+from unittest.mock import Mock, patch
import httpx
import pytest
@@ -9,15 +9,45 @@ from flask import Flask
from webmentions_ssg import DATABASE as db
from webmentions_ssg.models import ReceivedWebmention
+from webmentions_ssg.url_security import (
+ AddressResolutionError,
+ NonPublicAddressError,
+)
SOURCE_URL = "https://source.example/article"
TARGET_URL = "https://dennisfink.me/blog/example/"
-ReceivedWebmentionFactory = Callable[..., uuid.UUID]
-HTTPXMockInstaller = Callable[
- [Callable[[httpx.Request], httpx.Response]],
- None,
-]
+
+@pytest.fixture
+def receiver(app: Flask) -> ModuleType:
+ from webmentions_ssg.tasks import receiver
+
+ return receiver
+
+
+def create_webmention(
+ app: Flask,
+ *,
+ source: str = SOURCE_URL,
+ target: str = TARGET_URL,
+ status: str = "received",
+ failure_reason: str | None = None,
+) -> uuid.UUID:
+ identifier = uuid.uuid7()
+
+ with app.app_context():
+ webmention = ReceivedWebmention(
+ uuid=identifier,
+ source=source,
+ target=target,
+ status=status,
+ failure_reason=failure_reason,
+ )
+
+ db.session.add(webmention)
+ db.session.commit()
+
+ return identifier
def get_webmention_state(
@@ -38,257 +68,200 @@ def get_webmention_state(
)
+def set_stream_response(
+ httpx_client: Mock,
+ response: httpx.Response,
+) -> Mock:
+ client = httpx_client.return_value.__enter__.return_value
+ client.stream.return_value.__enter__.return_value = response
+ return client
+
+
@pytest.mark.parametrize(
- (
- "body",
- "source_url",
- "target_url",
- "expected",
- ),
+ ("body", "expected"),
[
pytest.param(
f'<a href="{TARGET_URL}">Reply</a>',
- SOURCE_URL,
- TARGET_URL,
True,
id="a-href",
),
pytest.param(
f'<area href="{TARGET_URL}" alt="Target">',
- SOURCE_URL,
- TARGET_URL,
True,
id="area-href",
),
pytest.param(
f'<link href="{TARGET_URL}" rel="alternate">',
- SOURCE_URL,
- TARGET_URL,
True,
id="link-href",
),
pytest.param(
f'<img src="{TARGET_URL}" alt="">',
- SOURCE_URL,
- TARGET_URL,
True,
id="img-src",
),
pytest.param(
f'<audio src="{TARGET_URL}"></audio>',
- SOURCE_URL,
- TARGET_URL,
True,
id="audio-src",
),
pytest.param(
f'<video src="{TARGET_URL}"></video>',
- SOURCE_URL,
- TARGET_URL,
True,
id="video-src",
),
pytest.param(
- (f'<audio><source src="{TARGET_URL}"></audio>'),
- SOURCE_URL,
- TARGET_URL,
+ f'<audio><source src="{TARGET_URL}"></audio>',
True,
id="audio-source-src",
),
pytest.param(
- (f'<video><source src="{TARGET_URL}"></video>'),
- SOURCE_URL,
- TARGET_URL,
+ f'<video><source src="{TARGET_URL}"></video>',
True,
id="video-source-src",
),
pytest.param(
f'<iframe src="{TARGET_URL}"></iframe>',
- SOURCE_URL,
- TARGET_URL,
True,
id="iframe-src",
),
pytest.param(
f'<embed src="{TARGET_URL}">',
- SOURCE_URL,
- TARGET_URL,
True,
id="embed-src",
),
pytest.param(
f'<script src="{TARGET_URL}"></script>',
- SOURCE_URL,
- TARGET_URL,
True,
id="script-src",
),
pytest.param(
- (f'<video><track src="{TARGET_URL}"></video>'),
- SOURCE_URL,
- TARGET_URL,
+ f'<video><track src="{TARGET_URL}"></video>',
True,
id="track-src",
),
pytest.param(
f'<input type="image" src="{TARGET_URL}" alt="">',
- SOURCE_URL,
- TARGET_URL,
True,
id="image-input-src",
),
pytest.param(
f'<input type="IMAGE" src="{TARGET_URL}" alt="">',
- SOURCE_URL,
- TARGET_URL,
True,
id="image-input-case-insensitive",
),
pytest.param(
f'<blockquote cite="{TARGET_URL}">Quotation</blockquote>',
- SOURCE_URL,
- TARGET_URL,
True,
id="blockquote-cite",
),
pytest.param(
f'<q cite="{TARGET_URL}">Quotation</q>',
- SOURCE_URL,
- TARGET_URL,
True,
id="q-cite",
),
pytest.param(
f'<ins cite="{TARGET_URL}">Addition</ins>',
- SOURCE_URL,
- TARGET_URL,
True,
id="ins-cite",
),
pytest.param(
f'<del cite="{TARGET_URL}">Removal</del>',
- SOURCE_URL,
- TARGET_URL,
True,
id="del-cite",
),
pytest.param(
- '<a href="../target/">Reply</a>',
- "https://source.example/posts/article/",
- "https://source.example/posts/target/",
- True,
- id="relative-href",
- ),
- pytest.param(
'<base href="https://dennisfink.me/blog/"><a href="example/">Reply</a>',
- SOURCE_URL,
- TARGET_URL,
True,
id="base-url",
),
pytest.param(
f'<img cite="{TARGET_URL}" alt="">',
- SOURCE_URL,
- TARGET_URL,
False,
id="img-cite-invalid",
),
pytest.param(
f'<blockquote src="{TARGET_URL}">Quote</blockquote>',
- SOURCE_URL,
- TARGET_URL,
False,
id="blockquote-src-invalid",
),
pytest.param(
f'<a src="{TARGET_URL}">Reply</a>',
- SOURCE_URL,
- TARGET_URL,
False,
id="a-src-invalid",
),
pytest.param(
f'<div href="{TARGET_URL}"></div>',
- SOURCE_URL,
- TARGET_URL,
False,
id="div-href-invalid",
),
pytest.param(
f'<link src="{TARGET_URL}">',
- SOURCE_URL,
- TARGET_URL,
False,
id="link-src-invalid",
),
pytest.param(
f'<input type="text" src="{TARGET_URL}">',
- SOURCE_URL,
- TARGET_URL,
False,
id="text-input-src-invalid",
),
pytest.param(
- (f'<picture><source src="{TARGET_URL}"></picture>'),
- SOURCE_URL,
- TARGET_URL,
+ f'<picture><source src="{TARGET_URL}"></picture>',
False,
id="picture-source-src-invalid",
),
pytest.param(
f'<base href="{TARGET_URL}">',
- SOURCE_URL,
- TARGET_URL,
False,
id="base-is-not-mention",
),
pytest.param(
- (f'<a href="{TARGET_URL}more">Different page</a>'),
- SOURCE_URL,
- TARGET_URL,
+ f'<a href="{TARGET_URL}more">Different page</a>',
False,
id="longer-url",
),
pytest.param(
- (f'<img cite="{TARGET_URL}" src="https://example.com/image.jpg" alt="">'),
- SOURCE_URL,
- TARGET_URL,
+ f'<img cite="{TARGET_URL}" src="https://example.com/image.jpg" alt="">',
False,
id="invalid-cite-does-not-override-valid-src",
),
pytest.param(
f"<p>{TARGET_URL}</p>",
- SOURCE_URL,
- TARGET_URL,
False,
id="text-content",
),
pytest.param(
'<a href="https://example.com/">Other site</a>',
- SOURCE_URL,
- TARGET_URL,
False,
id="missing-target",
),
],
)
def test_html_mentions_target(
- receiver_module: ModuleType,
+ receiver: ModuleType,
body: str,
- source_url: str,
- target_url: str,
expected: bool,
) -> None:
assert (
- receiver_module.html_mentions_target(
+ receiver.html_mentions_target(
body.encode(),
- source_url,
- target_url,
+ SOURCE_URL,
+ TARGET_URL,
)
is expected
)
+def test_html_mentions_relative_target(
+ receiver: ModuleType,
+) -> None:
+ assert receiver.html_mentions_target(
+ b'<a href="../target/">Reply</a>',
+ "https://source.example/posts/article/",
+ "https://source.example/posts/target/",
+ )
+
+
@pytest.mark.parametrize(
("body", "target_url", "expected"),
[
@@ -306,63 +279,97 @@ def test_html_mentions_target(
],
)
def test_text_mentions_target(
- receiver_module: ModuleType,
+ receiver: ModuleType,
body: str,
target_url: str,
expected: bool,
) -> None:
- assert (
- receiver_module.text_mentions_target(
- body,
- target_url,
- )
- is expected
- )
+ assert receiver.text_mentions_target(body, target_url) is expected
-def test_fetch_source_returns_response_and_body(
- app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
+@patch("webmentions_ssg.tasks.receiver.ensure_public_url")
+def test_ensure_public_request_accepts_public_url(
+ ensure_public_url: Mock,
+ receiver: ModuleType,
) -> None:
- captured_request: httpx.Request | None = None
+ receiver.ensure_public_request(httpx.Request("GET", SOURCE_URL))
- def handler(
- request: httpx.Request,
- ) -> httpx.Response:
- nonlocal captured_request
- captured_request = request
+ ensure_public_url.assert_called_once_with(SOURCE_URL)
- return httpx.Response(
- 200,
- headers={
- "Content-Type": "text/html",
- },
- content=b"<p>Document</p>",
- )
- install_httpx_mock(handler)
+@patch(
+ "webmentions_ssg.tasks.receiver.ensure_public_url",
+ side_effect=NonPublicAddressError("Non-public address"),
+)
+def test_ensure_public_request_rejects_non_public_address(
+ ensure_public_url: Mock,
+ receiver: ModuleType,
+) -> None:
+ with pytest.raises(
+ receiver.VerificationError,
+ match="Source resolves to a non-public address",
+ ):
+ receiver.ensure_public_request(httpx.Request("GET", SOURCE_URL))
- with app.app_context():
- response, body = receiver_module.fetch_source(SOURCE_URL)
+ ensure_public_url.assert_called_once_with(SOURCE_URL)
- assert response.status_code == 200
- assert body == b"<p>Document</p>"
- assert captured_request is not None
- assert captured_request.url == SOURCE_URL
+@patch(
+ "webmentions_ssg.tasks.receiver.ensure_public_url",
+ side_effect=AddressResolutionError("Could not resolve hostname"),
+)
+def test_ensure_public_request_maps_dns_failure_to_temporary_error(
+ ensure_public_url: Mock,
+ receiver: ModuleType,
+) -> None:
+ with pytest.raises(
+ receiver.TemporaryFetchError,
+ match="Source hostname could not be resolved",
+ ):
+ receiver.ensure_public_request(httpx.Request("GET", SOURCE_URL))
+
+ ensure_public_url.assert_called_once_with(SOURCE_URL)
+
+
+@patch("webmentions_ssg.tasks.receiver.httpx.Client")
+def test_fetch_source_returns_response_and_body(
+ httpx_client: Mock,
+ app: Flask,
+ receiver: ModuleType,
+) -> None:
+ response = httpx.Response(
+ 200,
+ headers={"Content-Type": "text/html"},
+ content=b"<p>Document</p>",
+ request=httpx.Request("GET", SOURCE_URL),
+ )
+ client = set_stream_response(httpx_client, response)
+
+ with app.app_context():
+ fetched_response, body = receiver.fetch_source(SOURCE_URL)
- accept = captured_request.headers["Accept"]
+ assert fetched_response is response
+ assert body == b"<p>Document</p>"
+ client.stream.assert_called_once_with("GET", SOURCE_URL)
- assert "text/html" in accept
- assert "application/xhtml+xml" in accept
- assert "text/plain" in accept
+ options = httpx_client.call_args.kwargs
- assert captured_request.headers["User-Agent"] == (
- f"{receiver_module.APP_NAME}/{receiver_module.VERSION} ReceivedWebmention"
+ assert options["headers"] == {
+ "Accept": "text/html, application/xhtml+xml;q=0.9, text/plain;q=0.8",
+ "User-Agent": f"{receiver.APP_NAME}/{receiver.VERSION} ReceivedWebmention",
+ }
+ assert options["follow_redirects"] is True
+ assert options["max_redirects"] == app.config.get(
+ "WEBMENTIONS_SSG_MAX_REDIRECTS",
+ 20,
)
+ assert options["trust_env"] is False
+ assert options["event_hooks"] == {
+ "request": [receiver.ensure_public_request],
+ }
+@patch("webmentions_ssg.tasks.receiver.httpx.Client")
@pytest.mark.parametrize(
("status_code", "exception_name"),
[
@@ -377,19 +384,22 @@ def test_fetch_source_returns_response_and_body(
],
)
def test_fetch_source_maps_http_status_to_exception(
+ httpx_client: Mock,
app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
+ receiver: ModuleType,
status_code: int,
exception_name: str,
) -> None:
- install_httpx_mock(lambda request: httpx.Response(status_code))
-
- exception_type = getattr(
- receiver_module,
- exception_name,
+ set_stream_response(
+ httpx_client,
+ httpx.Response(
+ status_code,
+ request=httpx.Request("GET", SOURCE_URL),
+ ),
)
+ exception_type = getattr(receiver, exception_name)
+
with (
app.app_context(),
pytest.raises(
@@ -397,23 +407,20 @@ def test_fetch_source_maps_http_status_to_exception(
match=f"HTTP {status_code}",
),
):
- receiver_module.fetch_source(SOURCE_URL)
+ receiver.fetch_source(SOURCE_URL)
+@patch("webmentions_ssg.tasks.receiver.httpx.Client")
def test_fetch_source_propagates_network_error(
+ httpx_client: Mock,
app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
+ receiver: ModuleType,
) -> None:
- def handler(
- request: httpx.Request,
- ) -> httpx.Response:
- raise httpx.ConnectError(
- "Connection refused",
- request=request,
- )
-
- install_httpx_mock(handler)
+ client = httpx_client.return_value.__enter__.return_value
+ client.stream.side_effect = httpx.ConnectError(
+ "Connection refused",
+ request=httpx.Request("GET", SOURCE_URL),
+ )
with (
app.app_context(),
@@ -422,153 +429,74 @@ def test_fetch_source_propagates_network_error(
match="Connection refused",
),
):
- receiver_module.fetch_source(SOURCE_URL)
-
-
-def test_fetch_source_follows_redirect(
- app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
-) -> None:
- requested_paths: list[str] = []
-
- def handler(
- request: httpx.Request,
- ) -> httpx.Response:
- requested_paths.append(request.url.path)
-
- match request.url.path:
- case "/start":
- return httpx.Response(
- 302,
- headers={
- "Location": "/final",
- },
- )
-
- case "/final":
- return httpx.Response(
- 200,
- headers={
- "Content-Type": "text/html",
- },
- content=b"Final document",
- )
-
- case _:
- raise AssertionError(f"Unexpected URL: {request.url}")
-
- install_httpx_mock(handler)
-
- with app.app_context():
- response, body = receiver_module.fetch_source("https://source.example/start")
-
- assert requested_paths == [
- "/start",
- "/final",
- ]
- assert response.url.path == "/final"
- assert body == b"Final document"
-
-
-def test_fetch_source_enforces_redirect_limit(
- app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
- monkeypatch: pytest.MonkeyPatch,
-) -> None:
- monkeypatch.setitem(
- app.config,
- "WEBMENTIONS_SSG_MAX_REDIRECTS",
- 1,
- )
-
- install_httpx_mock(
- lambda request: httpx.Response(
- 302,
- headers={
- "Location": "/another",
- },
- )
- )
-
- with (
- app.app_context(),
- pytest.raises(httpx.TooManyRedirects),
- ):
- receiver_module.fetch_source("https://source.example/start")
+ receiver.fetch_source(SOURCE_URL)
+@patch("webmentions_ssg.tasks.receiver.httpx.Client")
def test_fetch_source_rejects_declared_oversized_body(
+ httpx_client: Mock,
app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
- monkeypatch: pytest.MonkeyPatch,
+ receiver: ModuleType,
) -> None:
- monkeypatch.setitem(
- app.config,
- "WEBMENTIONS_SSG_MAX_SOURCE_BYTES",
- 10,
- )
+ app.config["WEBMENTIONS_SSG_MAX_SOURCE_BYTES"] = 10
- install_httpx_mock(
- lambda request: httpx.Response(
+ set_stream_response(
+ httpx_client,
+ httpx.Response(
200,
headers={
"Content-Type": "text/html",
"Content-Length": "11",
},
content=b"x" * 11,
- )
+ request=httpx.Request("GET", SOURCE_URL),
+ ),
)
with (
app.app_context(),
pytest.raises(
- receiver_module.VerificationError,
+ receiver.VerificationError,
match="Source document is too large",
),
):
- receiver_module.fetch_source(SOURCE_URL)
+ receiver.fetch_source(SOURCE_URL)
+@patch("webmentions_ssg.tasks.receiver.httpx.Client")
def test_fetch_source_rejects_streamed_oversized_body(
+ httpx_client: Mock,
app: Flask,
- receiver_module: ModuleType,
- install_httpx_mock: HTTPXMockInstaller,
- monkeypatch: pytest.MonkeyPatch,
+ receiver: ModuleType,
) -> None:
- monkeypatch.setitem(
- app.config,
- "WEBMENTIONS_SSG_MAX_SOURCE_BYTES",
- 10,
- )
+ app.config["WEBMENTIONS_SSG_MAX_SOURCE_BYTES"] = 10
class BodyStream(httpx.SyncByteStream):
def __iter__(self):
yield b"x" * 6
yield b"x" * 6
- install_httpx_mock(
- lambda request: httpx.Response(
+ set_stream_response(
+ httpx_client,
+ httpx.Response(
200,
- headers={
- "Content-Type": "text/html",
- },
+ headers={"Content-Type": "text/html"},
stream=BodyStream(),
- )
+ request=httpx.Request("GET", SOURCE_URL),
+ ),
)
with (
app.app_context(),
pytest.raises(
- receiver_module.VerificationError,
+ receiver.VerificationError,
match="Source document is too large",
),
):
- receiver_module.fetch_source(SOURCE_URL)
+ receiver.fetch_source(SOURCE_URL)
+@patch("webmentions_ssg.tasks.receiver.fetch_source")
@pytest.mark.parametrize(
("content_type", "body", "expected"),
[
@@ -617,42 +545,25 @@ def test_fetch_source_rejects_streamed_oversized_body(
],
)
def test_source_mentions_target_by_media_type(
- receiver_module: ModuleType,
- monkeypatch: pytest.MonkeyPatch,
+ fetch_source: Mock,
+ receiver: ModuleType,
content_type: str,
body: bytes,
expected: bool,
) -> None:
response = httpx.Response(
200,
- headers={
- "Content-Type": content_type,
- },
+ headers={"Content-Type": content_type},
content=body,
- request=httpx.Request(
- "GET",
- SOURCE_URL,
- ),
- )
-
- monkeypatch.setattr(
- receiver_module,
- "fetch_source",
- lambda source_url: (
- response,
- body,
- ),
+ request=httpx.Request("GET", SOURCE_URL),
)
+ fetch_source.return_value = response, body
- assert (
- receiver_module.source_mentions_target(
- SOURCE_URL,
- TARGET_URL,
- )
- is expected
- )
+ assert receiver.source_mentions_target(SOURCE_URL, TARGET_URL) is expected
+ fetch_source.assert_called_once_with(SOURCE_URL)
+@patch("webmentions_ssg.tasks.receiver.fetch_source")
@pytest.mark.parametrize(
("content_type", "expected_media_type"),
[
@@ -662,8 +573,8 @@ def test_source_mentions_target_by_media_type(
],
)
def test_source_mentions_target_rejects_unsupported_media_type(
- receiver_module: ModuleType,
- monkeypatch: pytest.MonkeyPatch,
+ fetch_source: Mock,
+ receiver: ModuleType,
content_type: str,
expected_media_type: str,
) -> None:
@@ -676,38 +587,25 @@ def test_source_mentions_target_rejects_unsupported_media_type(
200,
headers=headers,
content=b"Document",
- request=httpx.Request(
- "GET",
- SOURCE_URL,
- ),
- )
-
- monkeypatch.setattr(
- receiver_module,
- "fetch_source",
- lambda source_url: (
- response,
- b"Document",
- ),
+ request=httpx.Request("GET", SOURCE_URL),
)
+ fetch_source.return_value = response, b"Document"
with pytest.raises(
- receiver_module.VerificationError,
- match=(f"Unsupported source content type: {expected_media_type}"),
+ receiver.VerificationError,
+ match=f"Unsupported source content type: {expected_media_type}",
):
- receiver_module.source_mentions_target(
- SOURCE_URL,
- TARGET_URL,
- )
+ receiver.source_mentions_target(SOURCE_URL, TARGET_URL)
+@patch("webmentions_ssg.tasks.receiver.source_mentions_target")
def test_verify_webmention_marks_row_verifying_before_check(
+ source_mentions_target: Mock,
app: Flask,
- receiver_module: ModuleType,
- make_webmention: ReceivedWebmentionFactory,
- monkeypatch: pytest.MonkeyPatch,
+ receiver: ModuleType,
) -> None:
- identifier = make_webmention(
+ identifier = create_webmention(
+ app,
status="failed",
failure_reason="Earlier failure",
)
@@ -729,13 +627,9 @@ def test_verify_webmention_marks_row_verifying_before_check(
return True
- monkeypatch.setattr(
- receiver_module,
- "source_mentions_target",
- verify_source,
- )
+ source_mentions_target.side_effect = verify_source
- receiver_module.verify_webmention.call_local(identifier)
+ receiver.verify_webmention.call_local(identifier)
assert get_webmention_state(
app,
@@ -746,6 +640,7 @@ def test_verify_webmention_marks_row_verifying_before_check(
)
+@patch("webmentions_ssg.tasks.receiver.source_mentions_target")
@pytest.mark.parametrize(
("outcome", "expected_status", "expected_reason"),
[
@@ -756,45 +651,32 @@ def test_verify_webmention_marks_row_verifying_before_check(
],
)
def test_verify_webmention_persists_final_state(
+ source_mentions_target: Mock,
app: Flask,
- receiver_module: ModuleType,
- make_webmention: ReceivedWebmentionFactory,
- monkeypatch: pytest.MonkeyPatch,
+ receiver: ModuleType,
outcome: str,
expected_status: str,
expected_reason: str | None,
) -> None:
- identifier = make_webmention(
- status="received",
- )
-
- def verify_source(
- source_url: str,
- target_url: str,
- ) -> bool:
- match outcome:
- case "verified":
- return True
+ identifier = create_webmention(app)
- case "missing":
- return False
-
- case "gone":
- raise receiver_module.SourceGoneError("Source returned HTTP 410")
-
- case "permanent-failure":
- raise receiver_module.VerificationError("Source returned HTTP 404")
-
- case _:
- raise AssertionError(f"Unexpected outcome: {outcome}")
-
- monkeypatch.setattr(
- receiver_module,
- "source_mentions_target",
- verify_source,
- )
+ match outcome:
+ case "verified":
+ source_mentions_target.return_value = True
+ case "missing":
+ source_mentions_target.return_value = False
+ case "gone":
+ source_mentions_target.side_effect = receiver.SourceGoneError(
+ "Source returned HTTP 410"
+ )
+ case "permanent-failure":
+ source_mentions_target.side_effect = receiver.VerificationError(
+ "Source returned HTTP 404"
+ )
+ case _:
+ raise AssertionError(f"Unexpected outcome: {outcome}")
- receiver_module.verify_webmention.call_local(identifier)
+ receiver.verify_webmention.call_local(identifier)
assert get_webmention_state(
app,
@@ -805,6 +687,7 @@ def test_verify_webmention_persists_final_state(
)
+@patch("webmentions_ssg.tasks.receiver.source_mentions_target")
@pytest.mark.parametrize(
"failure",
[
@@ -813,74 +696,48 @@ def test_verify_webmention_persists_final_state(
],
)
def test_verify_webmention_persists_retryable_failure_and_reraises(
+ source_mentions_target: Mock,
app: Flask,
- receiver_module: ModuleType,
- make_webmention: ReceivedWebmentionFactory,
- monkeypatch: pytest.MonkeyPatch,
+ receiver: ModuleType,
failure: str,
) -> None:
- identifier = make_webmention()
-
- def verify_source(
- source_url: str,
- target_url: str,
- ) -> bool:
- match failure:
- case "temporary-http":
- raise receiver_module.TemporaryFetchError("Source returned HTTP 503")
-
- case "network":
- raise httpx.ConnectError(
- "Connection refused",
- request=httpx.Request(
- "GET",
- source_url,
- ),
- )
-
- case _:
- raise AssertionError(f"Unexpected failure: {failure}")
-
- monkeypatch.setattr(
- receiver_module,
- "source_mentions_target",
- verify_source,
- )
+ identifier = create_webmention(app)
match failure:
case "temporary-http":
- expected_exception = receiver_module.TemporaryFetchError
- expected_reason = "Source returned HTTP 503"
-
+ exception = receiver.TemporaryFetchError("Source returned HTTP 503")
case "network":
- expected_exception = httpx.ConnectError
- expected_reason = "Connection refused"
-
+ exception = httpx.ConnectError(
+ "Connection refused",
+ request=httpx.Request("GET", SOURCE_URL),
+ )
case _:
raise AssertionError(f"Unexpected failure: {failure}")
+ source_mentions_target.side_effect = exception
+
with pytest.raises(
- expected_exception,
- match=expected_reason,
+ type(exception),
+ match=str(exception),
):
- receiver_module.verify_webmention.call_local(identifier)
+ receiver.verify_webmention.call_local(identifier)
assert get_webmention_state(
app,
identifier,
) == (
"failed",
- expected_reason,
+ str(exception),
)
def test_verify_webmention_ignores_unknown_identifier(
- receiver_module: ModuleType,
+ receiver: ModuleType,
caplog: pytest.LogCaptureFixture,
) -> None:
identifier = uuid.uuid7()
with caplog.at_level(logging.WARNING):
- receiver_module.verify_webmention.call_local(identifier)
+ receiver.verify_webmention.call_local(identifier)
assert f"Cannot verify unknown ReceivedWebmention {identifier}" in caplog.text