diff --git a/Dockerfile b/Dockerfile index d9ec8f61..18d541c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ pkg-config \ build-essential \ libpq-dev \ + git \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -54,7 +55,9 @@ COPY --from=build /usr/local/bin/uwsgi /usr/local/bin/uwsgi # Stage 3.2 - Copy source code WORKDIR /app +COPY ./bin/wait_for_db.sh /wait_for_db.sh COPY ./bin/docker_start.sh /start.sh +COPY ./bin/setup_configuration.sh /setup_configuration.sh RUN mkdir /app/log /app/config COPY --from=frontend-build /app/src/objecttypes/static /app/src/objecttypes/static diff --git a/bin/docker_start.sh b/bin/docker_start.sh index 2516c4dd..0328cec3 100755 --- a/bin/docker_start.sh +++ b/bin/docker_start.sh @@ -15,12 +15,8 @@ uwsgi_threads=${UWSGI_THREADS:-2} mountpoint=${SUBPATH:-/} -until pg_isready; do - >&2 echo "Waiting for database connection..." - sleep 1 -done - ->&2 echo "Database is up." +# wait for required services +${SCRIPTPATH}/wait_for_db.sh # Apply database migrations >&2 echo "Apply database migrations" diff --git a/bin/setup_configuration.sh b/bin/setup_configuration.sh new file mode 100755 index 00000000..216f8697 --- /dev/null +++ b/bin/setup_configuration.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# setup initial configuration using environment variables +# Run this script from the root of the repository + +#set -e +${SCRIPTPATH}/wait_for_db.sh + +src/manage.py migrate + +src/manage.py setup_configuration --no-selftest diff --git a/bin/wait_for_db.sh b/bin/wait_for_db.sh new file mode 100755 index 00000000..89e15e6a --- /dev/null +++ b/bin/wait_for_db.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +# Wait for the database container +# See: https://docs.docker.com/compose/startup-order/ +export PGHOST=${DB_HOST:-db} +export PGPORT=${DB_PORT:-5432} + +until pg_isready; do + >&2 echo "Waiting for database connection..." + sleep 1 +done + +>&2 echo "Database is up." diff --git a/docker-compose.yml b/docker-compose.yml index b08354f0..90561482 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,20 +2,37 @@ version: '3' services: db: - # NOTE: No persistance storage configured. - # See: https://hub.docker.com/_/postgres/ - image: postgres + image: postgres:11-alpine environment: - - POSTGRES_USER=${DB_USER:-objecttypes} - - POSTGRES_PASSWORD=${DB_PASSWORD:-objecttypes} + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./docker-init-db.sql:/docker-entrypoint-initdb.d/init_db.sql +# - db:/var/lib/postgresql/data + command: postgres -c max_connections=300 -c log_min_messages=LOG web: build: . - environment: + environment: &app-env - DJANGO_SETTINGS_MODULE=objecttypes.conf.docker - SECRET_KEY=${SECRET_KEY:-fgv=c0hz&tl*8*3m3893@m+1pstrvidc9e^5@fpspmg%cy$15d} - ALLOWED_HOSTS=* + - TWO_FACTOR_FORCE_OTP_ADMIN=no + - TWO_FACTOR_PATCH_ADMIN=no + # setup_configuration env vars + - OBJECTTYPES_DOMAIN=web:8000 + - OBJECTTYPES_ORGANIZATION=ObjectTypes + - OBJECTS_OBJECTTYPES_TOKEN=some-random-string + - OBJECTS_OBJECTTYPES_PERSON=Some Person + - OBJECTS_OBJECTTYPES_EMAIL=objects@objects.local ports: - 8000:8000 depends_on: - - db + web-init: + condition: service_completed_successfully + + web-init: + build: . + environment: *app-env + command: /setup_configuration.sh + depends_on: + - db \ No newline at end of file diff --git a/docker-init-db.sql b/docker-init-db.sql new file mode 100644 index 00000000..7e5eb4df --- /dev/null +++ b/docker-init-db.sql @@ -0,0 +1,3 @@ +CREATE USER objecttypes; +CREATE DATABASE objecttypes; +GRANT ALL PRIVILEGES ON DATABASE objecttypes TO objecttypes; diff --git a/requirements/base.in b/requirements/base.in index d9481840..7bd81885 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,10 +2,12 @@ open-api-framework # Core python libraries jsonschema +furl # Framework libraries django-jsonsuit sharing-configs +django-setup-configuration # API libraries drf-nested-routers diff --git a/requirements/base.txt b/requirements/base.txt index e1f556a5..49d33aca 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -87,6 +87,7 @@ django==4.2.11 # django-relativedelta # django-rest-framework-condition # django-sendfile2 + # django-setup-configuration # django-simple-certmanager # django-solo # django-two-factor-auth @@ -138,6 +139,8 @@ django-rest-framework-condition==0.1.1 # via commonground-api-common django-sendfile2==0.7.0 # via django-privates +django-setup-configuration==0.1.0 + # via -r requirements/base.in django-simple-certmanager==2.0.0 # via zgw-consumers django-solo==2.0.0 @@ -184,7 +187,9 @@ face==20.1.1 flower==2.0.1 # via open-api-framework furl==2.1.3 - # via ape-pie + # via + # -r requirements/base.in + # ape-pie gemma-zds-client==1.0.1 # via # commonground-api-common diff --git a/requirements/ci.txt b/requirements/ci.txt index 9093a08a..9ce9678b 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -132,6 +132,7 @@ django==4.2.11 # django-relativedelta # django-rest-framework-condition # django-sendfile2 + # django-setup-configuration # django-simple-certmanager # django-solo # django-two-factor-auth @@ -213,6 +214,8 @@ django-sendfile2==0.7.0 # via # -r requirements/base.txt # django-privates +django-setup-configuration==0.1.0 + # via -r requirements/base.txt django-simple-certmanager==2.0.0 # via # -r requirements/base.txt diff --git a/requirements/dev.txt b/requirements/dev.txt index ad466ae0..a1868223 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -152,6 +152,7 @@ django==4.2.11 # django-relativedelta # django-rest-framework-condition # django-sendfile2 + # django-setup-configuration # django-simple-certmanager # django-solo # django-two-factor-auth @@ -237,6 +238,8 @@ django-sendfile2==0.7.0 # via # -r requirements/ci.txt # django-privates +django-setup-configuration==0.1.0 + # via -r requirements/ci.txt django-simple-certmanager==2.0.0 # via # -r requirements/ci.txt diff --git a/src/objecttypes/conf/base.py b/src/objecttypes/conf/base.py index 07861d38..2192d521 100644 --- a/src/objecttypes/conf/base.py +++ b/src/objecttypes/conf/base.py @@ -34,6 +34,7 @@ DEBUG = config("DEBUG", default=False) ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="", split=True) +IS_HTTPS = config("IS_HTTPS", default=not DEBUG) USE_X_FORWARDED_HOST = config("USE_X_FORWARDED_HOST", default=False) @@ -77,6 +78,7 @@ "solo", "drf_spectacular", "vng_api_common", + "django_setup_configuration", # Two-factor authentication in the Django admin, enforced. "django_otp", "django_otp.plugins.otp_static", @@ -87,6 +89,7 @@ # Project applications. "objecttypes.accounts", "objecttypes.api", + "objecttypes.config", "objecttypes.core", "objecttypes.token", "objecttypes.utils", @@ -423,3 +426,35 @@ if config("DISABLE_2FA", default=False): # pragma: no cover MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS + +# +# Django setup configuration +# +SETUP_CONFIGURATION_STEPS = [ + "objecttypes.config.site.SiteConfigurationStep", + "objecttypes.config.objects.ObjectsAuthStep", + "objecttypes.config.demo.DemoUserStep", +] + + +# +# Objecttypes settings +# + +# setup_configuration command +# sites config +SITES_CONFIG_ENABLE = config("SITES_CONFIG_ENABLE", default=True) +OBJECTTYPES_DOMAIN = config("OBJECTTYPES_DOMAIN", "") +OBJECTTYPES_ORGANIZATION = config("OBJECTTYPES_ORGANIZATION", "") +# objects auth config +OBJECTS_OBJECTTYPES_CONFIG_ENABLE = config( + "OBJECTS_OBJECTTYPES_CONFIG_ENABLE", default=True +) +OBJECTS_OBJECTTYPES_TOKEN = config("OBJECTS_OBJECTTYPES_TOKEN", "") +OBJECTS_OBJECTTYPES_PERSON = config("OBJECTS_OBJECTTYPES_PERSON", "") +OBJECTS_OBJECTTYPES_EMAIL = config("OBJECTS_OBJECTTYPES_EMAIL", "") +# Demo User Configuration +DEMO_CONFIG_ENABLE = config("DEMO_CONFIG_ENABLE", default=DEBUG) +DEMO_TOKEN = config("DEMO_TOKEN", "") +DEMO_PERSON = config("DEMO_PERSON", "") +DEMO_EMAIL = config("DEMO_EMAIL", "") diff --git a/src/objecttypes/conf/ci.py b/src/objecttypes/conf/ci.py index e8c8c3ff..c5214e76 100644 --- a/src/objecttypes/conf/ci.py +++ b/src/objecttypes/conf/ci.py @@ -6,6 +6,7 @@ os.environ.setdefault("SECRET_KEY", "dummy") os.environ.setdefault("ENVIRONMENT", "ci") +os.environ.setdefault("IS_HTTPS", "no") from .base import * # noqa isort:skip diff --git a/src/objecttypes/conf/dev.py b/src/objecttypes/conf/dev.py index a467ddab..c69b51cc 100644 --- a/src/objecttypes/conf/dev.py +++ b/src/objecttypes/conf/dev.py @@ -3,6 +3,7 @@ import warnings os.environ.setdefault("DEBUG", "yes") +os.environ.setdefault("IS_HTTPS", "no") os.environ.setdefault("ALLOWED_HOSTS", "*") os.environ.setdefault( "SECRET_KEY", "fgv=c0hz&tl*8*3m3893@m+1pstrvidc9e^5@fpspmg%cy$15d" diff --git a/src/objecttypes/config/__init__.py b/src/objecttypes/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objecttypes/config/demo.py b/src/objecttypes/config/demo.py new file mode 100644 index 00000000..215d4c7e --- /dev/null +++ b/src/objecttypes/config/demo.py @@ -0,0 +1,53 @@ +from django.conf import settings +from django.urls import reverse + +import requests +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.token.models import TokenAuth +from objecttypes.utils import build_absolute_url + + +class DemoUserStep(BaseConfigurationStep): + """ + Create demo user to request Objectypes API + """ + + verbose_name = "Demo User Configuration" + required_settings = [ + "DEMO_TOKEN", + "DEMO_PERSON", + "DEMO_EMAIL", + ] + enable_setting = "DEMO_CONFIG_ENABLE" + + def is_configured(self) -> bool: + return TokenAuth.objects.filter(token=settings.DEMO_TOKEN).exists() + + def configure(self): + TokenAuth.objects.update_or_create( + token=settings.DEMO_TOKEN, + defaults={ + "contact_person": settings.DEMO_PERSON, + "email": settings.DEMO_EMAIL, + }, + ) + + def test_configuration(self): + endpoint = reverse("v2:objecttype-list") + full_url = build_absolute_url(endpoint, request=None) + + try: + response = requests.get( + full_url, + headers={ + "Authorization": f"Token {settings.DEMO_TOKEN}", + "Accept": "application/json", + }, + ) + response.raise_for_status() + except requests.RequestException as exc: + raise SelfTestFailed( + "Could not list objecttypes for the configured token" + ) from exc diff --git a/src/objecttypes/config/objects.py b/src/objecttypes/config/objects.py new file mode 100644 index 00000000..a24d5cf5 --- /dev/null +++ b/src/objecttypes/config/objects.py @@ -0,0 +1,55 @@ +from django.conf import settings +from django.urls import reverse + +import requests +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.token.models import TokenAuth +from objecttypes.utils import build_absolute_url + + +class ObjectsAuthStep(BaseConfigurationStep): + """ + Configure credentials for Objects API to request Objecttypes API + """ + + verbose_name = "Objects API Authentication Configuration" + required_settings = [ + "OBJECTS_OBJECTTYPES_TOKEN", + "OBJECTS_OBJECTTYPES_PERSON", + "OBJECTS_OBJECTTYPES_EMAIL", + ] + enable_setting = "OBJECTS_OBJECTTYPES_CONFIG_ENABLE" + + def is_configured(self) -> bool: + return TokenAuth.objects.filter( + token=settings.OBJECTS_OBJECTTYPES_TOKEN + ).exists() + + def configure(self): + TokenAuth.objects.update_or_create( + token=settings.OBJECTS_OBJECTTYPES_TOKEN, + defaults={ + "contact_person": settings.OBJECTS_OBJECTTYPES_PERSON, + "email": settings.OBJECTS_OBJECTTYPES_EMAIL, + }, + ) + + def test_configuration(self): + endpoint = reverse("v2:objecttype-list") + full_url = build_absolute_url(endpoint, request=None) + + try: + response = requests.get( + full_url, + headers={ + "Authorization": f"Token {settings.OBJECTS_OBJECTTYPES_TOKEN}", + "Accept": "application/json", + }, + ) + response.raise_for_status() + except requests.RequestException as exc: + raise SelfTestFailed( + "Could not list objecttypes for the configured token" + ) from exc diff --git a/src/objecttypes/config/site.py b/src/objecttypes/config/site.py new file mode 100644 index 00000000..3d00b878 --- /dev/null +++ b/src/objecttypes/config/site.py @@ -0,0 +1,37 @@ +from django.conf import settings +from django.contrib.sites.models import Site +from django.urls import reverse + +import requests +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.utils import build_absolute_url + + +class SiteConfigurationStep(BaseConfigurationStep): + """ + Configure the application site/domain. + """ + + verbose_name = "Site Configuration" + required_settings = ["OBJECTTYPES_DOMAIN", "OBJECTTYPES_ORGANIZATION"] + enable_setting = "SITES_CONFIG_ENABLE" + + def is_configured(self) -> bool: + site = Site.objects.get_current() + return site.domain == settings.OBJECTTYPES_DOMAIN + + def configure(self): + site = Site.objects.get_current() + site.domain = settings.OBJECTTYPES_DOMAIN + site.name = f"Objecttypes {settings.OBJECTTYPES_ORGANIZATION}".strip() + site.save() + + def test_configuration(self): + full_url = build_absolute_url(reverse("home")) + try: + response = requests.get(full_url) + response.raise_for_status() + except requests.RequestException as exc: + raise SelfTestFailed(f"Could not access home page at '{full_url}'") from exc diff --git a/src/objecttypes/tests/commands/__init__.py b/src/objecttypes/tests/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objecttypes/tests/commands/test_setup_configuration.py b/src/objecttypes/tests/commands/test_setup_configuration.py new file mode 100644 index 00000000..3e65d74d --- /dev/null +++ b/src/objecttypes/tests/commands/test_setup_configuration.py @@ -0,0 +1,100 @@ +from io import StringIO + +from django.contrib.sites.models import Site +from django.core.management import CommandError, call_command +from django.test import TestCase, override_settings +from django.urls import reverse + +import requests_mock +from rest_framework import status + +from objecttypes.config.demo import DemoUserStep +from objecttypes.config.objects import ObjectsAuthStep +from objecttypes.config.site import SiteConfigurationStep + + +@override_settings( + OBJECTTYPES_DOMAIN="objecttypes.example.com", + OBJECTTYPES_ORGANIZATION="ACME", + OBJECTS_OBJECTTYPES_TOKEN="some-random-string", + OBJECTS_OBJECTTYPES_PERSON="Some Person", + OBJECTS_OBJECTTYPES_EMAIL="objects@objects.local", + DEMO_CONFIG_ENABLE=True, + DEMO_TOKEN="demo-random-string", + DEMO_PERSON="Demo", + DEMO_EMAIL="demo@demo.local", +) +class SetupConfigurationTests(TestCase): + def setUp(self): + super().setUp() + + self.addCleanup(Site.objects.clear_cache) + + @requests_mock.Mocker() + def test_setup_configuration(self, m): + stdout = StringIO() + # mocks + m.get("http://objecttypes.example.com/", status_code=200) + m.get("http://objecttypes.example.com/api/v2/objecttypes", json=[]) + + call_command("setup_configuration", stdout=stdout) + + with self.subTest("Command output"): + command_output = stdout.getvalue().splitlines() + expected_output = [ + f"Configuration will be set up with following steps: [{SiteConfigurationStep()}, " + f"{ObjectsAuthStep()}, {DemoUserStep()}]", + f"Configuring {SiteConfigurationStep()}...", + f"{SiteConfigurationStep()} is successfully configured", + f"Configuring {ObjectsAuthStep()}...", + f"{ObjectsAuthStep()} is successfully configured", + f"Configuring {DemoUserStep()}...", + f"{DemoUserStep()} is successfully configured", + "Instance configuration completed.", + ] + + self.assertEqual(command_output, expected_output) + + with self.subTest("Site configured correctly"): + site = Site.objects.get_current() + self.assertEqual(site.domain, "objecttypes.example.com") + self.assertEqual(site.name, "Objecttypes ACME") + + with self.subTest("Objects can query Objecttypes API"): + response = self.client.get( + reverse("v2:objecttype-list"), + HTTP_AUTHORIZATION="Token some-random-string", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + with self.subTest("Demo user configured correctly"): + response = self.client.get( + reverse("v2:objecttype-list"), + HTTP_AUTHORIZATION="Token demo-random-string", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @requests_mock.Mocker() + def test_setup_configuration_selftest_fails(self, m): + m.get("http://objecttypes.example.com/", status_code=200) + m.get("http://objecttypes.example.com/api/v2/objecttypes", status_code=500) + + with self.assertRaisesMessage( + CommandError, + "Configuration test failed with errors: " + "Objects API Authentication Configuration: " + "Could not list objecttypes for the configured token", + ): + call_command("setup_configuration") + + @requests_mock.Mocker() + def test_setup_configuration_without_selftest(self, m): + stdout = StringIO() + + call_command("setup_configuration", no_selftest=True, stdout=stdout) + command_output = stdout.getvalue() + + self.assertEqual(len(m.request_history), 0) + self.assertTrue("Selftest is skipped" in command_output) diff --git a/src/objecttypes/tests/config/__init__.py b/src/objecttypes/tests/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objecttypes/tests/config/test_demo_configuration.py b/src/objecttypes/tests/config/test_demo_configuration.py new file mode 100644 index 00000000..952cfdb5 --- /dev/null +++ b/src/objecttypes/tests/config/test_demo_configuration.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings + +import requests +import requests_mock +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.config.demo import DemoUserStep +from objecttypes.token.models import TokenAuth + + +@override_settings( + DEMO_TOKEN="demo-random-string", DEMO_PERSON="Demo", DEMO_EMAIL="demo@demo.local" +) +class DemoConfigurationTests(TestCase): + def test_configure(self): + configuration = DemoUserStep() + + configuration.configure() + + token_auth = TokenAuth.objects.get() + self.assertEqual(token_auth.token, "demo-random-string") + self.assertEqual(token_auth.contact_person, "Demo") + self.assertEqual(token_auth.email, "demo@demo.local") + + @requests_mock.Mocker() + @patch( + "objecttypes.config.demo.build_absolute_url", + return_value="http://testserver/objecttypes", + ) + def test_configuration_check_ok(self, m, *mocks): + configuration = DemoUserStep() + configuration.configure() + m.get("http://testserver/objecttypes", json=[]) + + configuration.test_configuration() + + self.assertEqual(m.last_request.url, "http://testserver/objecttypes") + self.assertEqual(m.last_request.method, "GET") + + @requests_mock.Mocker() + @patch( + "objecttypes.config.demo.build_absolute_url", + return_value="http://testserver/objecttypes", + ) + def test_configuration_check_failures(self, m, *mocks): + configuration = DemoUserStep() + configuration.configure() + + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get("http://testserver/objecttypes", **mock_config) + + with self.assertRaises(SelfTestFailed): + configuration.test_configuration() + + def test_is_configured(self): + configuration = DemoUserStep() + + self.assertFalse(configuration.is_configured()) + + configuration.configure() + + self.assertTrue(configuration.is_configured()) diff --git a/src/objecttypes/tests/config/test_objects_configuration.py b/src/objecttypes/tests/config/test_objects_configuration.py new file mode 100644 index 00000000..044680c7 --- /dev/null +++ b/src/objecttypes/tests/config/test_objects_configuration.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings + +import requests +import requests_mock +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.config.objects import ObjectsAuthStep +from objecttypes.token.models import TokenAuth + + +@override_settings( + OBJECTS_OBJECTTYPES_TOKEN="some-random-string", + OBJECTS_OBJECTTYPES_PERSON="Some Person", + OBJECTS_OBJECTTYPES_EMAIL="objects@objects.local", +) +class ObjectsConfigurationTests(TestCase): + def test_configure(self): + configuration = ObjectsAuthStep() + + configuration.configure() + + token_auth = TokenAuth.objects.get(token="some-random-string") + self.assertEqual(token_auth.contact_person, "Some Person") + self.assertEqual(token_auth.email, "objects@objects.local") + + @requests_mock.Mocker() + @patch( + "objecttypes.config.objects.build_absolute_url", + return_value="http://testserver/objecttypes", + ) + def test_selftest_ok(self, m, *mocks): + configuration = ObjectsAuthStep() + configuration.configure() + m.get("http://testserver/objecttypes", json=[]) + + configuration.test_configuration() + + self.assertEqual(m.last_request.url, "http://testserver/objecttypes") + self.assertEqual(m.last_request.method, "GET") + + @requests_mock.Mocker() + @patch( + "objecttypes.config.objects.build_absolute_url", + return_value="http://testserver/objecttypes", + ) + def test_selftest_fail(self, m, *mocks): + configuration = ObjectsAuthStep() + configuration.configure() + + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get("http://testserver/objecttypes", **mock_config) + + with self.assertRaises(SelfTestFailed): + configuration.test_configuration() + + def test_is_configured(self): + configuration = ObjectsAuthStep() + self.assertFalse(configuration.is_configured()) + + configuration.configure() + + self.assertTrue(configuration.is_configured()) diff --git a/src/objecttypes/tests/config/test_site_configuration.py b/src/objecttypes/tests/config/test_site_configuration.py new file mode 100644 index 00000000..4dccee19 --- /dev/null +++ b/src/objecttypes/tests/config/test_site_configuration.py @@ -0,0 +1,66 @@ +from django.contrib.sites.models import Site +from django.test import TestCase, override_settings + +import requests +import requests_mock +from django_setup_configuration.exceptions import SelfTestFailed + +from objecttypes.config.site import SiteConfigurationStep + + +@override_settings( + OBJECTTYPES_DOMAIN="localhost:8000", + OBJECTTYPES_ORGANIZATION="ACME", +) +class SiteConfigurationTests(TestCase): + def setUp(self): + super().setUp() + + self.addCleanup(Site.objects.clear_cache) + + def test_set_domain(self): + configuration = SiteConfigurationStep() + configuration.configure() + + site = Site.objects.get_current() + self.assertEqual(site.domain, "localhost:8000") + self.assertEqual(site.name, "Objecttypes ACME") + + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + m.get("http://localhost:8000/", status_code=200) + configuration = SiteConfigurationStep() + configuration.configure() + + configuration.test_configuration() + + self.assertEqual(m.last_request.url, "http://localhost:8000/") + self.assertEqual(m.last_request.method, "GET") + + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + configuration = SiteConfigurationStep() + configuration.configure() + + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get("http://localhost:8000/", **mock_config) + + with self.assertRaises(SelfTestFailed): + configuration.test_configuration() + + def test_is_configured(self): + configuration = SiteConfigurationStep() + + self.assertFalse(configuration.is_configured()) + + configuration.configure() + + self.assertTrue(configuration.is_configured()) diff --git a/src/objecttypes/urls.py b/src/objecttypes/urls.py index a9c70b5f..8e256729 100644 --- a/src/objecttypes/urls.py +++ b/src/objecttypes/urls.py @@ -48,6 +48,7 @@ template_name="index.html", extra_context={"version": api_settings.DEFAULT_VERSION}, ), + name="home", ), path("oidc/", include("mozilla_django_oidc.urls")), path("api/", include("objecttypes.api.urls")), diff --git a/src/objecttypes/utils/__init__.py b/src/objecttypes/utils/__init__.py index e69de29b..9eeca335 100644 --- a/src/objecttypes/utils/__init__.py +++ b/src/objecttypes/utils/__init__.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.http import HttpRequest + +from furl import furl + + +def get_domain() -> str: + """ + Obtain the domain/netloc according to settings or configuration. + """ + from django.contrib.sites.models import Site + + if settings.OBJECTTYPES_DOMAIN: + return settings.OBJECTTYPES_DOMAIN + + return Site.objects.get_current().domain + + +def build_absolute_url(path: str, request: HttpRequest | None = None) -> str: + if request is not None: + return request.build_absolute_uri(path) + + domain = get_domain() + _furl = furl( + scheme="https" if settings.IS_HTTPS else "http", + netloc=domain, + path=path, + ) + return _furl.url