diff --git a/dune_processes/settings/settings.py b/dune_processes/settings/settings.py index 98764f5..169bace 100644 --- a/dune_processes/settings/settings.py +++ b/dune_processes/settings/settings.py @@ -131,5 +131,8 @@ AUTH_USER_MODEL = "main.User" +LOGIN_URL = "main:login" +LOGIN_REDIRECT_URL = "main:index" + INSTALLED_APPS += ["django_bootstrap5"] DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5.html" diff --git a/main/templates/registration/login.html b/main/templates/registration/login.html new file mode 100644 index 0000000..417c5cc --- /dev/null +++ b/main/templates/registration/login.html @@ -0,0 +1,37 @@ +{% extends "../main/base.html" %} + +{% block content %} + + {% if form.errors %} +

Your username and password didn't match. Please try again.

+ {% endif %} + + {% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} + {% endif %} + +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +
+ + {# Assumes you set up the password_reset view in your URLconf #} +

Lost password?

+ +{% endblock %} diff --git a/main/urls.py b/main/urls.py index 2703ba6..8579501 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,12 +1,13 @@ """Urls module for the main app.""" -from django.urls import path +from django.urls import include, path from . import views app_name = "main" urlpatterns = [ path("", views.index, name="index"), + path("accounts/", include("django.contrib.auth.urls")), path("restart/", views.restart_process, name="restart"), path("kill/", views.kill_process, name="kill"), path("flush/", views.flush_process, name="flush"), diff --git a/main/views.py b/main/views.py index 3f9c7ae..4ea9d56 100644 --- a/main/views.py +++ b/main/views.py @@ -5,6 +5,8 @@ from enum import Enum import django_tables2 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse, reverse_lazy @@ -36,6 +38,7 @@ async def get_session_info() -> ProcessInstanceList: return await pmd.ps(query) +@login_required def index(request: HttpRequest) -> HttpResponse: """View that renders the index/home page.""" val = asyncio.run(get_session_info()) @@ -96,6 +99,7 @@ async def _process_call(uuid: str, action: ProcessAction) -> None: await pmd.flush(query) +@login_required def restart_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: """Restart the process associated to the given UUID. @@ -111,6 +115,7 @@ def restart_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: return HttpResponseRedirect(reverse("main:index")) +@login_required def kill_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: """Kill the process associated to the given UUID. @@ -125,6 +130,7 @@ def kill_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: return HttpResponseRedirect(reverse("main:index")) +@login_required def flush_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: """Flush the process associated to the given UUID. @@ -154,6 +160,7 @@ async def _get_process_logs(uuid: str) -> list[DecodedResponse]: return [item async for item in pmd.logs(request)] +@login_required def logs(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse: """Display the logs of a process. @@ -181,7 +188,7 @@ async def _boot_process(user: str, data: dict[str, str | int]) -> None: pass -class BootProcessView(FormView): # type: ignore [type-arg] +class BootProcessView(LoginRequiredMixin, FormView): # type: ignore [type-arg] """View for the BootProcess form.""" template_name = "main/boot_process.html" diff --git a/tests/conftest.py b/tests/conftest.py index cb6cc72..9fbbe5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,16 @@ +"""Configuration for pytest.""" + import pytest +from django.test import Client + + +@pytest.fixture +def auth_client(django_user_model) -> Client: + """Return an authenticated client.""" + user = django_user_model.objects.create(username="testuser") + client = Client() + client.force_login(user) + return client @pytest.fixture diff --git a/tests/test_views.py b/tests/test_views.py index 8265ef9..7a8c874 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,42 +3,67 @@ import pytest from django.urls import reverse -from pytest_django.asserts import assertContains, assertTemplateUsed +from pytest_django.asserts import assertContains, assertRedirects, assertTemplateUsed from main.views import ProcessAction -def test_index(client, admin_client, mocker): +def test_index(client, auth_client, admin_client, mocker): """Test the index view.""" mocker.patch("main.views.get_session_info") + + # Test with an anonymous client. + response = client.get(reverse("main:index")) + assert response.status_code == HTTPStatus.FOUND + assertRedirects(response, "/accounts/login/?next=/") + + # Test with an authenticated client. with assertTemplateUsed(template_name="main/index.html"): - response = client.get(reverse("main:index")) + response = auth_client.get(reverse("main:index")) + assert response.status_code == HTTPStatus.OK + + # Test with an admin client. + with assertTemplateUsed(template_name="main/index.html"): + response = admin_client.get(reverse("main:index")) assert response.status_code == HTTPStatus.OK assert "table" in response.context assertContains(response, "Boot") -def test_logs(client, mocker): +def test_logs(client, auth_client, mocker): """Test the logs view.""" mock = mocker.patch("main.views._get_process_logs") uuid = uuid4() + + # Test with an anonymous client. + response = client.get(reverse("main:logs", kwargs=dict(uuid=uuid))) + assert response.status_code == HTTPStatus.FOUND + assertRedirects(response, f"/accounts/login/?next=/logs/{uuid}") + + # Test with an authenticated client. with assertTemplateUsed(template_name="main/logs.html"): - response = client.get(reverse("main:logs", kwargs=dict(uuid=uuid))) + response = auth_client.get(reverse("main:logs", kwargs=dict(uuid=uuid))) assert response.status_code == HTTPStatus.OK mock.assert_called_once_with(str(uuid)) assert "log_text" in response.context -def test_process_flush(client, mocker): +def test_process_flush(client, auth_client, mocker): """Test the process_flush view.""" mock = mocker.patch("main.views._process_call") uuid = uuid4() + + # Test with an anonymous client. response = client.get(reverse("main:flush", kwargs=dict(uuid=uuid))) + assert response.status_code == HTTPStatus.FOUND + assertRedirects(response, f"/accounts/login/?next=/flush/{uuid}") + # Test with an authenticated client. + response = auth_client.get(reverse("main:flush", kwargs=dict(uuid=uuid))) assert response.status_code == HTTPStatus.FOUND assert response.url == reverse("main:index") mock.assert_called_once_with(str(uuid), ProcessAction.FLUSH) @@ -49,27 +74,35 @@ class TestBootProcess: template_name = "main/boot_process.html" - def test_boot_process_get(self, client): + def test_boot_process_get(self, auth_client): """Test the GET request for the BootProcess view.""" with assertTemplateUsed(template_name=self.template_name): - response = client.get(reverse("main:boot_process")) + response = auth_client.get(reverse("main:boot_process")) assert response.status_code == HTTPStatus.OK assert "form" in response.context assertContains(response, f'form action="{reverse("main:boot_process")}"') - def test_boot_process_post_invalid(self, client): + def test_boot_process_get_anon(self, client): + """Test the GET request for the BootProcess view with an anonymous client.""" + response = client.get(reverse("main:boot_process")) + assert response.status_code == HTTPStatus.FOUND + assertRedirects(response, "/accounts/login/?next=/boot_process/") + + def test_boot_process_post_invalid(self, auth_client): """Test the POST request for the BootProcess view with invalid data.""" with assertTemplateUsed(template_name=self.template_name): - response = client.post(reverse("main:boot_process"), data=dict()) + response = auth_client.post(reverse("main:boot_process"), data=dict()) assert response.status_code == HTTPStatus.OK assert "form" in response.context - def test_boot_process_post_valid(self, client, mocker, dummy_session_data): + def test_boot_process_post_valid(self, auth_client, mocker, dummy_session_data): """Test the POST request for the BootProcess view.""" mock = mocker.patch("main.views._boot_process") - response = client.post(reverse("main:boot_process"), data=dummy_session_data) + response = auth_client.post( + reverse("main:boot_process"), data=dummy_session_data + ) assert response.status_code == HTTPStatus.FOUND assert response.url == reverse("main:index")