diff --git a/tests/test_admin.py b/tests/test_admin.py index 2957dbc..2c877ff 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,6 +1,13 @@ from django.test import Client from django.urls import reverse +import requests +import requests_mock +from pytest_django.asserts import assertContains + +from zgw_consumers.constants import AuthTypes +from zgw_consumers.test.factories import ServiceFactory + def test_oas_fields_enabled(admin_client: Client, settings): settings.ZGW_CONSUMERS_IGNORE_OAS_FIELDS = False @@ -32,3 +39,69 @@ def test_oas_fields_disabled(admin_client: Client, settings): assert "oas" not in form.fields assert "oas_file" not in form.fields + + +def test_get_connection_check_correct_status_code(admin_client: Client, settings): + service = ServiceFactory.create( + api_root="https://example.com/", + api_connection_check_path="foo", + auth_type=AuthTypes.zgw, + client_id="my-client-id", + secret="my-secret", + ) + with requests_mock.Mocker() as m: + m.get("https://example.com/foo", status_code=401) + url = reverse( + "admin:zgw_consumers_service_change", kwargs={"object_id": service.id} + ) + response = admin_client.get(url) + + connection_check_inner_html = '
401
' + assertContains(response, connection_check_inner_html, html=True) + + +def test_get_connection_check_encountering_error(admin_client: Client, settings): + service = ServiceFactory.create( + api_root="https://example.com/", + api_connection_check_path="foo", + auth_type=AuthTypes.zgw, + client_id="my-client-id", + secret="my-secret", + ) + with requests_mock.Mocker() as m: + m.get("https://example.com/foo", exc=requests.RequestException) + url = reverse( + "admin:zgw_consumers_service_change", kwargs={"object_id": service.id} + ) + response = admin_client.get(url) + + connection_check_inner_html = '
None
' + assertContains(response, connection_check_inner_html, html=True) + + +def test_get_connection_check_not_configured(admin_client: Client, settings): + service = ServiceFactory.create( + api_root="https://example.com/", + auth_type=AuthTypes.zgw, + client_id="my-client-id", + secret="my-secret", + ) + with requests_mock.Mocker() as m: + m.get("https://example.com/", status_code=200) + url = reverse( + "admin:zgw_consumers_service_change", kwargs={"object_id": service.id} + ) + response = admin_client.get(url) + + connection_check_inner_html = '
200
' + assertContains(response, connection_check_inner_html, html=True) + + +def test_get_connection_opening_add_page(admin_client: Client, settings): + url = reverse("admin:zgw_consumers_service_add") + response = admin_client.get(url) + + connection_check_inner_html = ( + '
n/a
' + ) + assertContains(response, connection_check_inner_html, html=True) diff --git a/tests/test_validation.py b/tests/test_validation.py index 23f4fd6..55021dc 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -2,37 +2,32 @@ import pytest -from zgw_consumers.models.validators import NonUrlValidator, PrefixValidator - - -def test_start_with_validator_starts_with_false(): - validator = PrefixValidator(prefix="/", starts_with=False) - - assert validator.__call__("no_leading_slash") is None - - with pytest.raises(ValidationError) as exc_context: - validator.__call__("/with_leading_slash") - - assert "The given value cannot start with '/'" in exc_context.value - - -def test_start_with_validator_starts_with_true(): - validator = PrefixValidator(prefix="/") - - with pytest.raises(ValidationError) as exc_context: - validator.__call__("no_leading_slash") - - assert "The given value must start with '/'" in exc_context.value - - assert validator.__call__("/with_leading_slash") is None +from zgw_consumers.models.validators import NonUrlValidator, validate_leading_slashes def test_is_not_url_validator(): validator = NonUrlValidator() - assert validator.__call__("some random text") is None - - with pytest.raises(ValidationError) as exc_context: - assert validator.__call__("http://www.example.com") - - assert "Value cannot be a URL" in exc_context.value + try: + validator("some random text") + except ValidationError: + pytest.fail("Expected validation to pass") + + with pytest.raises( + ValidationError, + match="Value cannot be a URL.", + ): + validator("http://www.example.com") + + +def test_validate_leading_slashes(): + try: + validate_leading_slashes("foo") + except ValidationError: + pytest.fail("Expected validation to pass") + + with pytest.raises( + ValidationError, + match="The value must be a relative path.", + ): + validate_leading_slashes("/foo") diff --git a/zgw_consumers/admin.py b/zgw_consumers/admin.py index 4331d09..3a1f28e 100644 --- a/zgw_consumers/admin.py +++ b/zgw_consumers/admin.py @@ -21,6 +21,9 @@ class ServiceAdmin(admin.ModelAdmin): @admin.display(description=_("Connection check status code")) def get_connection_check(self, obj): + if obj.pk is None: + return "n/a" + return obj.connection_check def get_fields(self, request: HttpRequest, obj: models.Model | None = None): diff --git a/zgw_consumers/migrations/0021_service_api_connection_check_path.py b/zgw_consumers/migrations/0021_service_api_connection_check_path.py index c17c4bf..bc5617e 100644 --- a/zgw_consumers/migrations/0021_service_api_connection_check_path.py +++ b/zgw_consumers/migrations/0021_service_api_connection_check_path.py @@ -1,5 +1,6 @@ # Generated by Django 4.2 on 2024-05-21 08:15 +import django.core.validators from django.db import migrations, models import zgw_consumers.models.validators @@ -17,11 +18,13 @@ class Migration(migrations.Migration): name="api_connection_check_path", field=models.CharField( blank=True, - help_text="An optional API endpoint which will be used to check if the API is configured correctly and is currently up or down. This field is only used for the admin's 'Connection check status code' field.", + help_text="A relative URL to perform a connection test. If left blank, the API root itself is used. This connection check is only performed in the admin when viewing the service configuration.", max_length=255, validators=[ - zgw_consumers.models.validators.PrefixValidator( - prefix="/", starts_with=False + django.core.validators.RegexValidator( + code="invalid", + message="The value must be a relative path.", + regex="^[^/#][^\\s]*", ), zgw_consumers.models.validators.NonUrlValidator(), ], diff --git a/zgw_consumers/models/services.py b/zgw_consumers/models/services.py index 12040b3..2849d3a 100644 --- a/zgw_consumers/models/services.py +++ b/zgw_consumers/models/services.py @@ -21,7 +21,7 @@ from ..constants import APITypes, AuthTypes, NLXDirectories from .abstract import RestAPIService -from .validators import NonUrlValidator, PrefixValidator +from .validators import NonUrlValidator, validate_leading_slashes logger = logging.getLogger(__name__) @@ -36,12 +36,13 @@ class Service(RestAPIService): api_connection_check_path = models.CharField( _("connection check endpoint"), help_text=_( - "An optional API endpoint which will be used to check if the API is configured correctly and " - "is currently up or down. This field is only used for the admin's 'Connection check status code' field." + "A relative URL to perform a connection test. If left blank, the API root itself is used. " + "This connection check is only performed in the admin when viewing the service " + "configuration." ), max_length=255, validators=[ - PrefixValidator(prefix="/", starts_with=False), + validate_leading_slashes, NonUrlValidator(), ], blank=True, @@ -132,7 +133,7 @@ def clean(self): ) @property - def connection_check(self) -> bool: + def connection_check(self) -> int | None: from zgw_consumers.client import build_client try: diff --git a/zgw_consumers/models/validators.py b/zgw_consumers/models/validators.py index b9724f2..5b344d5 100644 --- a/zgw_consumers/models/validators.py +++ b/zgw_consumers/models/validators.py @@ -1,51 +1,14 @@ from django.core.exceptions import ValidationError -from django.core.validators import URLValidator -from django.utils.deconstruct import deconstructible +from django.core.validators import RegexValidator, URLValidator from django.utils.translation import gettext_lazy as _ +validate_leading_slashes = RegexValidator( + regex=r"^[^/#][^\s]*", + message="The value must be a relative path.", + code="invalid", +) -@deconstructible -class PrefixValidator: - code = "invalid" - def __init__( - self, - prefix: str, - message: str = None, - code: str = None, - starts_with: bool = True, - ): - self.prefix = prefix - self.starts_with = starts_with - - if code is not None: - self.code = code - - if message is not None: - self.message = message - else: - self.message = _( - "The given value {must_or_cannot} start with '{prefix}'" - ).format( - must_or_cannot="must" if self.starts_with else "cannot", - prefix=self.prefix, - ) - - def __call__(self, value: str) -> bool: - if value.startswith(self.prefix) != self.starts_with: - raise ValidationError(self.message, code=self.code, params={"value": value}) - - def __eq__(self, other): - return ( - isinstance(other, self.__class__) - and self.prefix == other.prefix - and (self.message == other.message) - and (self.code == other.code) - and (self.starts_with == other.starts_with) - ) - - -@deconstructible class NonUrlValidator(URLValidator): message = _("Value cannot be a URL")