# SPDX-FileCopyrightText: 2026 Dennis Fink # # SPDX-License-Identifier: BSD-3-Clause from unittest.mock import Mock import pytest from flask import Flask from werkzeug.datastructures import MultiDict from wtforms import Form, StringField, ValidationError from webmentions_ssg.forms.validators import AllowedHostname, NotEqualTo, PublicURL from webmentions_ssg.url_security import AddressResolutionError class ComparisonForm(Form): source = StringField("Source") target = StringField("Target") class URLForm(Form): url = StringField("URL") def test_not_equal_to_accepts_different_values() -> None: form = ComparisonForm(MultiDict({"source": "source", "target": "target"})) NotEqualTo("target")(form, form.source) def test_not_equal_to_rejects_equal_values() -> None: form = ComparisonForm(MultiDict({"source": "same", "target": "same"})) with pytest.raises(ValidationError, match="Field must not be equal to target"): NotEqualTo("target")(form, form.source) def test_not_equal_to_uses_custom_message() -> None: form = ComparisonForm(MultiDict({"source": "same", "target": "same"})) with pytest.raises(ValidationError, match="Must differ from Target"): NotEqualTo("target", "Must differ from %(other_label)s")(form, form.source) def test_not_equal_to_rejects_unknown_field() -> None: form = ComparisonForm(MultiDict({"source": "source", "target": "target"})) with pytest.raises(ValidationError, match="Invalid field name 'missing'"): NotEqualTo("missing")(form, form.source) def test_allowed_hostname_accepts_configured_hostname(app: Flask) -> None: app.config["WEBMENTIONS_SSG_ALLOWED_HOSTNAMES"] = ["dennisfink.me"] form = URLForm(MultiDict({"url": "https://dennisfink.me/blog/example/"})) with app.app_context(): AllowedHostname()(form, form.url) def test_allowed_hostname_rejects_unconfigured_hostname(app: Flask) -> None: app.config["WEBMENTIONS_SSG_ALLOWED_HOSTNAMES"] = ["dennisfink.me"] form = URLForm(MultiDict({"url": "https://example.com/post"})) with app.app_context(), pytest.raises(ValidationError, match="Invalid input"): AllowedHostname()(form, form.url) def test_allowed_hostname_uses_custom_message(app: Flask) -> None: app.config["WEBMENTIONS_SSG_ALLOWED_HOSTNAMES"] = ["dennisfink.me"] form = URLForm(MultiDict({"url": "https://example.com/post"})) with ( app.app_context(), pytest.raises(ValidationError, match="Hostname is not allowed"), ): AllowedHostname("Hostname is not allowed")(form, form.url) def test_public_url_accepts_public_url(monkeypatch: pytest.MonkeyPatch) -> None: is_public_url = Mock(return_value=True) monkeypatch.setattr("webmentions_ssg.forms.validators.is_public_url", is_public_url) form = URLForm(MultiDict({"url": "https://example.com/post"})) PublicURL()(form, form.url) is_public_url.assert_called_once_with("https://example.com/post") @pytest.mark.parametrize( "outcome", [ pytest.param(False, id="non-public-address"), pytest.param( AddressResolutionError("Could not resolve hostname"), id="resolution-error" ), pytest.param(ValueError("No hostname was specified"), id="missing-hostname"), ], ) def test_public_url_rejects_invalid_url( monkeypatch: pytest.MonkeyPatch, outcome: bool | Exception ) -> None: is_public_url = Mock() if isinstance(outcome, Exception): is_public_url.side_effect = outcome else: is_public_url.return_value = outcome monkeypatch.setattr("webmentions_ssg.forms.validators.is_public_url", is_public_url) form = URLForm(MultiDict({"url": "https://example.com/post"})) with pytest.raises(ValidationError, match="URL must resolve to a public address"): PublicURL()(form, form.url) def test_public_url_uses_custom_message(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( "webmentions_ssg.forms.validators.is_public_url", Mock(return_value=False) ) form = URLForm(MultiDict({"url": "https://example.com/post"})) with pytest.raises(ValidationError, match="Public URL required"): PublicURL("Public URL required")(form, form.url)