From 67eff0a854da010cb6ecd119d84238ec3e119272 Mon Sep 17 00:00:00 2001
From: Dennis Fink
Date: Sun, 9 Aug 2026 14:15:29 +0200
Subject: Implement received Webmention handling
Add the Flask application setup, database models and migrations,
authentication, and configuration for development and testing.
Implement asynchronous Webmention verification with Huey, including HTML
and plain-text source validation, retries, status tracking, and size
limits.
Add status, login, and paginated received-Webmention views together with
comprehensive tests for forms, views, and receiver tasks.
---
tests/conftest.py | 125 ++++++
tests/tasks/test_receiver.py | 886 +++++++++++++++++++++++++++++++++++++++++++
tests/test_forms.py | 130 +++++++
tests/test_views.py | 183 +++++++++
4 files changed, 1324 insertions(+)
create mode 100644 tests/conftest.py
create mode 100644 tests/tasks/test_receiver.py
create mode 100644 tests/test_forms.py
create mode 100644 tests/test_views.py
(limited to 'tests')
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..94ba650
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,125 @@
+import uuid
+from collections.abc import Callable, Iterator
+from types import ModuleType
+
+import httpx
+import pytest
+from flask import Flask
+from flask.testing import FlaskClient
+
+from webmentions_ssg import DATABASE as db
+from webmentions_ssg import create_app
+from webmentions_ssg.config import TestingConfig
+from webmentions_ssg.models import ReceivedWebmention
+
+HTTPHandler = Callable[
+ [httpx.Request],
+ httpx.Response,
+]
+
+
+@pytest.fixture
+def app() -> Iterator[Flask]:
+ """
+ Create a new Flask application and in-memory database
+ for every test.
+ """
+
+ application = create_app(TestingConfig)
+
+ with application.app_context():
+ db.create_all()
+
+ yield application
+
+ with application.app_context():
+ db.session.remove()
+ db.drop_all()
+ db.engine.dispose()
+
+
+@pytest.fixture
+def client(app: Flask) -> FlaskClient:
+ return app.test_client()
+
+
+@pytest.fixture
+def receiver_module(app: Flask) -> ModuleType:
+ """
+ Ensure the application and Huey extension are initialized
+ before retrieving the tasks module.
+ """
+
+ from webmentions_ssg.tasks import receiver
+
+ return receiver
+
+
+@pytest.fixture
+def views_module(app: Flask) -> ModuleType:
+ from webmentions_ssg import views
+
+ return views
+
+
+@pytest.fixture
+def make_webmention(
+ app: Flask,
+) -> Callable[..., uuid.UUID]:
+ def create(
+ *,
+ source: str = "https://source.example/post",
+ target: str = ("https://dennisfink.me/blog/example/"),
+ 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
+
+ return create
+
+
+@pytest.fixture
+def install_httpx_mock(
+ monkeypatch: pytest.MonkeyPatch,
+ receiver_module: ModuleType,
+) -> Callable[[HTTPHandler], None]:
+ """
+ Replace the HTTPX transport without replacing HTTPX itself.
+ """
+
+ real_client = httpx.Client
+
+ def install(handler: HTTPHandler) -> None:
+ transport = httpx.MockTransport(handler)
+
+ def create_client(
+ *args,
+ **kwargs,
+ ) -> httpx.Client:
+ return real_client(
+ *args,
+ transport=transport,
+ **kwargs,
+ )
+
+ monkeypatch.setattr(
+ receiver_module.httpx,
+ "Client",
+ create_client,
+ )
+
+ return install
diff --git a/tests/tasks/test_receiver.py b/tests/tasks/test_receiver.py
new file mode 100644
index 0000000..4545121
--- /dev/null
+++ b/tests/tasks/test_receiver.py
@@ -0,0 +1,886 @@
+import logging
+import uuid
+from collections.abc import Callable
+from types import ModuleType
+
+import httpx
+import pytest
+from flask import Flask
+
+from webmentions_ssg import DATABASE as db
+from webmentions_ssg.models import ReceivedWebmention
+
+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,
+]
+
+
+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,
+ )
+
+
+@pytest.mark.parametrize(
+ (
+ "body",
+ "source_url",
+ "target_url",
+ "expected",
+ ),
+ [
+ pytest.param(
+ f'Reply',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="a-href",
+ ),
+ pytest.param(
+ f'',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="area-href",
+ ),
+ pytest.param(
+ f'',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="link-href",
+ ),
+ pytest.param(
+ f'
',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="img-src",
+ ),
+ pytest.param(
+ f'',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="audio-src",
+ ),
+ pytest.param(
+ f'',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="video-src",
+ ),
+ pytest.param(
+ (f''),
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="audio-source-src",
+ ),
+ pytest.param(
+ (f''),
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="video-source-src",
+ ),
+ pytest.param(
+ f'',
+ SOURCE_URL,
+ TARGET_URL,
+ True,
+ id="iframe-src",
+ ),
+ pytest.param(
+ f'