From 2a1a24ced71153b3710d28e557d703bf1b863d53 Mon Sep 17 00:00:00 2001 From: Tom Irgang Date: Mon, 24 Apr 2023 21:03:56 +0200 Subject: [PATCH] tests started, docs rework started --- docs/index.md | 13 +++- rc_hip/rc_hip/settings.py | 1 - rc_hip/repapp/admin.py | 11 +-- rc_hip/repapp/backends.py | 9 +-- rc_hip/repapp/forms.py | 12 +++- rc_hip/repapp/models.py | 3 + rc_hip/repapp/tests.py | 138 +++++++++++++++++++++++++++++++++++++- rc_hip/repapp/views.py | 28 +++++++- 8 files changed, 200 insertions(+), 15 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3f03358..4dca363 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,8 @@ RepApp ist eine Web App um Reparaturen im Rahmen eines Repair-Cafés zu organisi ## Benutzer +RepApp unterscheidet zwischen den folgenden Benutzer-Typen: + ### Gast Ein Gast ist ein Gerätebesitzer der ein oder mehrere Geräte zur Reparatur anmelden möchte. @@ -18,6 +20,9 @@ Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen o ## Anwendungsfälle +Die Anwendungsfälle sind in Meilensteine unterteilt. +Zu Meilenstein 1 (M1) gehören alle Anwendungsfälle die notwendig sind um RepApp "produktiv" für die Anmeldung von Geräten zu verwenden. + ### Gast - [x] M1: Als Gast möchte ich einen Termin für eine Reparatur vereinbaren. (A1) @@ -30,7 +35,9 @@ Ein Organisator ist Mitglied des Repair-Cafés das die Termine mit den Gästen o - [ ] Als Gast möchte ich meine Daten ansehen können. (A21) - [ ] Als Gast möchte ich meine Daten löschen können. (A22) - [ ] Als Gast möchte ich eine Geräteanmeldung löschen können. (A23) -- [ ] M1: Als Gast möchte ausschließlich per eMail mit dem System interagieren können. (A30) +- [ ] M1: Als Gast möchte ich ausschließlich per eMail mit dem System interagieren können. (A30) +- [ ] M1: Als Gast möchte per eMail Fragen ans Team stellen können. (A33) +- [ ] M1: Als Gast möchte ich per eMail auf Rückfragen antworten können. (A34) ### Reparateur @@ -272,7 +279,7 @@ Die Seite enthält persönliche Daten und ist nur nach Anmeldung erreichbar. ### Anmeldung eines defekten Gerätes -#### Variante: Neuer Gast +#### Variante: Neuer Gast (F1) Als neuer Gast der ein defekten Geräte anmelden möchte, @@ -282,7 +289,7 @@ Als neuer Gast der ein defekten Geräte anmelden möchte, - Die nächste Seite (S5) bestätigt mir die Anmeldung meines Gerätes. - In meinem eMail Posteingang finde ich ebenfalls eine Bestätigung der Geräteanmeldung. -#### Variante: Bekannter Gast (existierende eMail Adresse) +#### Variante: Bekannter Gast (existierende eMail Adresse) (F2) Als bekannter Gast der ein defekten Geräte anmelden möchte, diff --git a/rc_hip/rc_hip/settings.py b/rc_hip/rc_hip/settings.py index 61bbc36..5bd03be 100644 --- a/rc_hip/rc_hip/settings.py +++ b/rc_hip/rc_hip/settings.py @@ -163,7 +163,6 @@ AUTH_USER_MODEL = "repapp.CustomUser" AUTHENTICATION_BACKENDS = [ "repapp.backends.EmailBackend", "django.contrib.auth.backends.ModelBackend", - # "mozilla_django_oidc.auth.OIDCAuthenticationBackend", "repapp.backends.KeycloakOIDCAB", "repapp.backends.OneTimeLoginBackend", ] diff --git a/rc_hip/repapp/admin.py b/rc_hip/repapp/admin.py index 6f24f09..8580170 100644 --- a/rc_hip/repapp/admin.py +++ b/rc_hip/repapp/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin, ExportActionMixin from import_export import resources from .models import (Organisator, Cafe, Question, Device, CustomUser, - Reparateur, Appointment, Guest, Candidate, OneTimeLogin) + Reparateur, Appointment, Guest, Candidate, OneTimeLogin, Message) class GuestResource(resources.ModelResource): @@ -96,13 +96,16 @@ class ExportAdmin(ImportExportModelAdmin, ExportActionMixin): ] +# Register all models of RepApp for the admin interface. admin.site.register(Organisator) admin.site.register(Cafe) +admin.site.register(Reparateur) +admin.site.register(CustomUser) +admin.site.register(OneTimeLogin) +admin.site.register(Message) +# Make the "data" models exportable. admin.site.register(Question, ExportAdmin) admin.site.register(Guest, ExportAdmin) admin.site.register(Device, ExportAdmin) -admin.site.register(Reparateur) admin.site.register(Appointment, ExportAdmin) admin.site.register(Candidate, ExportAdmin) -admin.site.register(CustomUser) -admin.site.register(OneTimeLogin) diff --git a/rc_hip/repapp/backends.py b/rc_hip/repapp/backends.py index 126dcf8..732b6fb 100644 --- a/rc_hip/repapp/backends.py +++ b/rc_hip/repapp/backends.py @@ -28,10 +28,10 @@ class EmailBackend(ModelBackend): """ def authenticate(self, request, username=None, password=None, **kwargs): - userModel = get_user_model() + user_model = get_user_model() try: - user = userModel.objects.get(email=username) - except userModel.DoesNotExist: + user = user_model.objects.get(email=username) + except user_model.DoesNotExist: return None else: if user.check_password(password): @@ -41,7 +41,8 @@ class EmailBackend(ModelBackend): class KeycloakOIDCAB(OIDCAuthenticationBackend): """ - KeycloakOIDCAB allows a login using Open ID Connect (with the Repair-Café Keycloak Single Sign On server) + KeycloakOIDCAB allows a login using Open ID Connect + (with the Repair-Café Keycloak Single Sign On server) """ def create_user(self, claims): diff --git a/rc_hip/repapp/forms.py b/rc_hip/repapp/forms.py index 9193c73..c96e21f 100644 --- a/rc_hip/repapp/forms.py +++ b/rc_hip/repapp/forms.py @@ -6,6 +6,9 @@ from django.core.exceptions import ValidationError class HoneypotField(forms.BooleanField): + """ + Simple honeypot field. + """ default_widget = forms.CheckboxInput( {'style': 'display:none !important;', 'tabindex': '-1', 'autocomplete': 'off'}) @@ -22,6 +25,9 @@ class HoneypotField(forms.BooleanField): class RegisterDevice(forms.Form): + """ + Form for registering a device for a repair cafe. + """ mail = forms.EmailField( label="eMail Adresse", help_text="Diese eMail-Adresse wird für Aktualisierungen" @@ -49,7 +55,8 @@ class RegisterDevice(forms.Form): ) follow_up = forms.BooleanField( label="Folgetermin", - help_text="Kreuzen Sie diese Kästchen an wenn sie mit diesem Gerät bereits bei einem Repair-Café Termin waren.", + help_text="Kreuzen Sie diese Kästchen an wenn sie mit diesem Gerät " + "bereits bei einem Repair-Café Termin waren.", required=False ) confirm_repair = forms.BooleanField( @@ -72,6 +79,9 @@ class RegisterDevice(forms.Form): class RegisterGuest(forms.Form): + """ + Form for registering new guests. + """ name = forms.CharField( label="Name", max_length=200 diff --git a/rc_hip/repapp/models.py b/rc_hip/repapp/models.py index 833c08b..b058ff8 100644 --- a/rc_hip/repapp/models.py +++ b/rc_hip/repapp/models.py @@ -175,6 +175,9 @@ class Candidate(models.Model): class CustomUser(AbstractUser): + """ + Custom user object with unique email. + """ email = models.EmailField(unique=True, verbose_name=_("eMail Adresse")) class Meta: diff --git a/rc_hip/repapp/tests.py b/rc_hip/repapp/tests.py index a39b155..fde1243 100644 --- a/rc_hip/repapp/tests.py +++ b/rc_hip/repapp/tests.py @@ -1 +1,137 @@ -# Create your tests here. +""" +Tests for RepApp. +""" +from datetime import datetime, timedelta +import django.utils.timezone +from django.test import TestCase +from django.test import Client +from django.urls import reverse +from .backends import OneTimeLoginBackend, EmailBackend +from .models import OneTimeLogin, CustomUser, Cafe + + +class OneTimeLoginTest(TestCase): + """ + Tests for the one time login feature. + """ + + def setUp(self): + user = CustomUser( + email="user@example.com", + ) + user.save() + self.user = user + + otl = OneTimeLogin( + secret="LetMeIn", + user=user, + url="/test/url", + ) + otl.save() + self.otl = otl + + def test_allowed_one_time_login(self): + """ + Test that one time login is allowed. + """ + backend = OneTimeLoginBackend() + result = backend.authenticate(None, self.otl.secret, None) + + self.assertEqual(result, self.user) + + def test_unknown_one_time_login(self): + """ + Test that one time login is not allowed for unknown secret. + """ + + backend = OneTimeLoginBackend() + result = backend.authenticate(None, "other phrase", None) + + self.assertEqual(result, None) + + +class EmailLoginTest(TestCase): + """ + Tests for the one time login feature. + """ + + def setUp(self): + user = CustomUser( + email="user@example.com" + ) + user.set_password("aPassword") + user.save() + self.user = user + + def test_allowed_email_login(self): + """ + Test that email login is allowed. + """ + backend = EmailBackend() + result = backend.authenticate(None, self.user.email, "aPassword") + + self.assertEqual(result, self.user) + + def test_unknown_email_login(self): + """ + Test that email login is not allowed for unknown user. + """ + + backend = EmailBackend() + result = backend.authenticate( + None, "other@example.com", "somePassword") + + self.assertEqual(result, None) + + def test_wrong_password_login(self): + """ + Test that email login is not allowed for wrong password. + """ + + backend = EmailBackend() + result = backend.authenticate( + None, self.user.email, "wrongPassword") + + self.assertEqual(result, None) + + +class UtilsTest(TestCase): + """ + Test for utility functions. + """ + pass + + +class ViewsTest(TestCase): + """ + Test for RepApp views. + """ + + def setUp(self): + cafe = Cafe(location="neuer Ort", address="neue Adresse", + event_date=django.utils.timezone.now()) + cafe.save() + self.cafe = cafe + + date = datetime.now() - timedelta(days=7) + old_cafe = Cafe(location="alter Ort", address="alte Adresse", + event_date=date) + old_cafe.save() + self.old_cafe = old_cafe + + def test_index(self): + """ + Test index page. + """ + client = Client(enforce_csrf_checks=True) + response = client.get(reverse('index')) + + self.assertEqual(response.status_code, 200) + + # ensure future cafe is displayed + self.assertContains(response, self.cafe.location) + self.assertContains(response, self.cafe.address) + + # ensure old cafe is not displayed + self.assertNotContains(response, self.old_cafe.location) + self.assertNotContains(response, self.old_cafe.address) diff --git a/rc_hip/repapp/views.py b/rc_hip/repapp/views.py index 02cdf9d..c822e31 100644 --- a/rc_hip/repapp/views.py +++ b/rc_hip/repapp/views.py @@ -103,6 +103,9 @@ class RegisterDeviceFormView(generic.edit.FormView): class RegisterGuestFormView(generic.edit.FormView): + """ + View for registering a new guest. + """ template_name = "repapp/register_guest.html" form_class = RegisterGuest @@ -137,7 +140,8 @@ class RegisterGuestFormView(generic.edit.FormView): user = CustomUser.objects.create_user( username=name, email=mail, - password=password) + ) + user.set_password(password) user.save() send_guest_account_mail(guest, password, self.request) @@ -168,6 +172,9 @@ class RegisterGuestFormView(generic.edit.FormView): def register_device_final(request, cafe, device_identifier): + """ + View to confirm device registration. + """ cafe = get_object_or_404(Cafe, pk=cafe) device = get_object_or_404(Device, identifier=device_identifier) @@ -180,6 +187,9 @@ def register_device_final(request, cafe, device_identifier): @login_required def device_view(request, device_identifier): + """ + View for showing device details. + """ user = request.user if not user: raise PermissionDenied() @@ -200,6 +210,9 @@ def device_view(request, device_identifier): @login_required def profile(request): + """ + View for showing guest details. + """ user = request.user if not user: raise PermissionDenied() @@ -216,6 +229,9 @@ def profile(request): def member_login(request): + """ + Login page for repair cafe members, using OIDC. + """ return render( request, "repapp/member_login.html" @@ -223,14 +239,24 @@ def member_login(request): def cron(request): + """ + View to trigger automated regular tasks. + """ pass def process_mails(request): + """ + View to trigger processing of email in inbox. + """ pass def one_time_login(request, secret): + """ + View for one time login. + """ + # waste a little time as brute force protection time.sleep(1) otl = get_object_or_404(OneTimeLogin, secret=secret)