# SPDX-FileCopyrightText: 2026 Dennis Fink # # SPDX-License-Identifier: BSD-3-Clause import uuid from datetime import UTC, datetime from unittest.mock import Mock, call, patch import pytest import sqlalchemy as sa from flask import Flask from flask.testing import FlaskClient from webmentions_ssg import DATABASE as db from webmentions_ssg.models import ( ReceivedWebmention, SentWebmention, SentWebmentionStatus, Source, User, ) def log_in(app: Flask, client: FlaskClient) -> None: with app.app_context(): user = User(username="admin", password="test") db.session.add(user) db.session.commit() user_id = user.id with client.session_transaction() as session: session["_user_id"] = str(user_id) session["_fresh"] = True def create_sent_source(app: Flask) -> uuid.UUID: now = datetime.now(UTC) with app.app_context(): source = Source( path="blog/example/index.html", url="https://dennisfink.me/blog/example/", content_hash="0" * 32, revision=2, last_seen_at=now, revised_at=now, ) source.sent_webmentions.extend( [ SentWebmention( target="https://example.com/b", active=True, desired_revision=2, processed_revision=2, sent_revision=2, status=SentWebmentionStatus.SENT, endpoint="https://example.com/webmention", response_status=202, ), SentWebmention( target="https://example.com/a", active=True, desired_revision=2, processed_revision=2, sent_revision=1, status=SentWebmentionStatus.FAILED, failure_reason="Webmention endpoint returned HTTP 400", endpoint="https://example.com/webmention", response_status=400, ), ] ) db.session.add(source) db.session.commit() return source.uuid def test_endpoint_only_accepts_post(client: FlaskClient) -> None: response = client.get("/endpoint") assert response.status_code == 405 @patch("webmentions_ssg.forms.validators.is_public_url", new=lambda url: True) def test_endpoint_returns_form_errors(client: FlaskClient) -> None: response = client.post( "/endpoint", data={ "source": "https://source.example/post", "target": "https://example.com/post", }, ) assert response.status_code == 400 errors = response.get_json() assert errors is not None assert "target" in errors @patch("webmentions_ssg.forms.validators.is_public_url", new=lambda url: True) @patch("webmentions_ssg.views.verify_webmention") def test_endpoint_creates_webmention( verify_webmention: Mock, app: Flask, client: FlaskClient ) -> None: source = "https://source.example/post" target = "https://dennisfink.me/blog/example/" response = client.post("/endpoint", data={"source": source, "target": target}) assert response.status_code == 201 with app.app_context(): webmention = db.session.scalar(sa.select(ReceivedWebmention)) assert webmention is not None assert webmention.source == source assert webmention.target == target assert webmention.status == "received" assert webmention.failure_reason is None identifier = webmention.uuid assert response.headers["Location"].endswith(str(identifier)) verify_webmention.assert_called_once_with(identifier) @patch("webmentions_ssg.forms.validators.is_public_url", new=lambda url: True) @patch("webmentions_ssg.views.verify_webmention") def test_endpoint_is_idempotent( verify_webmention: Mock, app: Flask, client: FlaskClient ) -> None: data = { "source": "https://source.example/post", "target": "https://dennisfink.me/blog/example/", } first_response = client.post("/endpoint", data=data) second_response = client.post("/endpoint", data=data) assert first_response.status_code == 201 assert second_response.status_code == 201 assert first_response.headers["Location"] == second_response.headers["Location"] with app.app_context(): webmentions = db.session.scalars(sa.select(ReceivedWebmention)).all() assert len(webmentions) == 1 webmention = webmentions[0] assert webmention.source == data["source"] assert webmention.target == data["target"] assert webmention.status == "received" assert webmention.failure_reason is None identifier = webmention.uuid assert verify_webmention.call_args_list == [call(identifier), call(identifier)] @patch("webmentions_ssg.forms.validators.is_public_url", new=lambda url: True) @patch("webmentions_ssg.views.verify_webmention") def test_resending_resets_failure_state( verify_webmention: Mock, app: Flask, client: FlaskClient ) -> None: source = "https://source.example/post" target = "https://dennisfink.me/blog/example/" with app.app_context(): existing = ReceivedWebmention( uuid=uuid.uuid7(), source=source, target=target, status="failed", failure_reason="Previous failure", ) db.session.add(existing) db.session.commit() identifier = existing.uuid response = client.post("/endpoint", data={"source": source, "target": target}) assert response.status_code == 201 with app.app_context(): webmention = db.session.get(ReceivedWebmention, identifier) assert webmention is not None assert webmention.status == "received" assert webmention.failure_reason is None verify_webmention.assert_called_once_with(identifier) @pytest.mark.parametrize("path", ["/sent", f"/sent/{uuid.uuid7()}"]) def test_sent_views_require_login(client: FlaskClient, path: str) -> None: response = client.get(path) assert response.status_code == 302 assert "/login" in response.headers["Location"] def test_sent_lists_sources(app: Flask, client: FlaskClient) -> None: log_in(app, client) identifier = create_sent_source(app) response = client.get("/sent") assert response.status_code == 200 html = response.get_data(as_text=True) assert "blog/example/index.html" in html assert "https://dennisfink.me/blog/example/" in html assert f"/sent/{identifier}" in html def test_sent_source_lists_webmentions(app: Flask, client: FlaskClient) -> None: log_in(app, client) identifier = create_sent_source(app) response = client.get(f"/sent/{identifier}") assert response.status_code == 200 html = response.get_data(as_text=True) assert "blog/example/index.html" in html assert "https://dennisfink.me/blog/example/" in html first_target = "https://example.com/a" second_target = "https://example.com/b" assert first_target in html assert second_target in html assert html.index(first_target) < html.index(second_target) assert "Webmention endpoint returned HTTP 400" in html assert "https://example.com/webmention" in html def test_sent_source_returns_404_for_unknown_source( app: Flask, client: FlaskClient ) -> None: log_in(app, client) response = client.get(f"/sent/{uuid.uuid7()}") assert response.status_code == 404