Skip to content

Commit

Permalink
Merge pull request #64 from ImperialCollegeLondon/basic_auth
Browse files Browse the repository at this point in the history
Basic auth
  • Loading branch information
jamesturner246 authored Sep 13, 2024
2 parents c0d7508 + 8432c45 commit 28641eb
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 14 deletions.
3 changes: 3 additions & 0 deletions dune_processes/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
37 changes: 37 additions & 0 deletions main/templates/registration/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "../main/base.html" %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}

<form method="post" action="{% url 'main:login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>

{# Assumes you set up the password_reset view in your URLconf #}
<p><a href="{% url 'main:password_reset' %}">Lost password?</a></p>

{% endblock %}
3 changes: 2 additions & 1 deletion main/urls.py
Original file line number Diff line number Diff line change
@@ -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/<uuid:uuid>", views.restart_process, name="restart"),
path("kill/<uuid:uuid>", views.kill_process, name="kill"),
path("flush/<uuid:uuid>", views.flush_process, name="flush"),
Expand Down
9 changes: 8 additions & 1 deletion main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
57 changes: 45 additions & 12 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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</a>")


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)
Expand All @@ -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")
Expand Down

0 comments on commit 28641eb

Please sign in to comment.