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