aboutsummaryrefslogtreecommitdiff
path: root/tests/forms/test_validators.py
blob: c63ee54f30f4c777589bd0dc38c8793953e0fb8a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
#
# 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)