Merge pull request #24 from makes-hacks-hip/refactor_doc_test

Refactor doc test
This commit is contained in:
Thomas Irgang 2023-04-28 11:56:19 +02:00 committed by GitHub
commit 82d752c677
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 701 additions and 78 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.env .env
**/data.json
rc_hip/static rc_hip/static
rc_hip/static/** rc_hip/static/**

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "bootstrap"]
path = bootstrap
url = https://github.com/twbs/bootstrap.git

1
bootstrap Submodule

@ -0,0 +1 @@
Subproject commit 803617e9c595adebf8a43a560fcfe8d439424f94

View file

@ -17,7 +17,8 @@
"KeycloakOIDCAB", "KeycloakOIDCAB",
"Keycloak", "Keycloak",
"tabindex", "tabindex",
"onetimelogin" "onetimelogin",
"Ammeldelink"
], ],
"ignoreWords": [], "ignoreWords": [],
"import": [] "import": []

View file

@ -51,12 +51,12 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"crispy_forms", "crispy_forms",
"crispy_bootstrap4", "crispy_bootstrap5",
"import_export", "import_export",
] ]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4" CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap4" CRISPY_TEMPLATE_PACK = "bootstrap5"
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@ -88,7 +88,6 @@ TEMPLATES = [
WSGI_APPLICATION = "rc_hip.wsgi.application" WSGI_APPLICATION = "rc_hip.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # https://docs.djangoproject.com/en/4.2/ref/settings/#databases

View file

@ -12,6 +12,9 @@ logger = logging.getLogger(__name__)
def generate_username(email): def generate_username(email):
"""
Generate a valid user name form the mail address.
"""
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric # Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize # (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters. # it and slice at 150 characters.
@ -19,19 +22,29 @@ def generate_username(email):
def create_repapp_user(user): def create_repapp_user(user):
"""
Create the RepApp user objects for the new user.
If a user logs in using OIDC it is a member of the Repair-Café, so it can be
either a Reparateur or an Organisator. Organisators have access to the private
data form the guests and must be nominated by an admin.
"""
organisator = Organisator.objects.filter(mail=user.email).first() organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator: if not organisator:
# no organisator, create or update reparateur
reparateur = Reparateur.objects.filter(mail=user.email).first() reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur: if not reparateur:
# create new reparateur
reparateur = Reparateur( reparateur = Reparateur(
name=user.username, name=user.username,
mail=user.email, mail=user.email,
) )
reparateur.save() reparateur.save()
else: else:
# update name of existing reparateur
reparateur.name = user.username reparateur.name = user.username
reparateur.save() reparateur.save()
else: else:
# update name of organisator
organisator.name = user.username organisator.name = user.username
organisator.save() organisator.save()
@ -74,15 +87,17 @@ class KeycloakOIDCAB(OIDCAuthenticationBackend):
def create_user(self, claims): def create_user(self, claims):
user = super(KeycloakOIDCAB, self).create_user(claims) user = super(KeycloakOIDCAB, self).create_user(claims)
logger.debug(f'Create user {user.email}') logger.info('Create user %s' % user.email)
fallback_name = generate_username(user.email)
try: try:
user.username = claims.get( user.username = claims.get('preferred_username', fallback_name)
'preferred_username', generate_username(user.email))
user.save() user.save()
except Exception as exception: except Exception as exception:
logger.error(exception) logger.error(exception)
user.username = generate_username(user.email) logger.warning(
'Update username failed! Using fallback name %s.' % fallback_name)
user.username = fallback_name
user.save() user.save()
logger.debug(f'Updated username {user.username}') logger.debug(f'Updated username {user.username}')

View file

@ -7,5 +7,52 @@
"address": "Patersholzer Weg 19, Hilpoltstein", "address": "Patersholzer Weg 19, Hilpoltstein",
"event_date": "2023-05-13" "event_date": "2023-05-13"
} }
},
{
"model": "repapp.customuser",
"pk": 3,
"fields": {
"password": "pbkdf2_sha256$600000$O70jrsL2vmSssirJPvWBMw$lbvFy/OI7mUfz26Qvh+bxnsp19v+xsETJydvsgLxE4E=",
"last_login": "2023-04-25T18:47:07.685Z",
"is_superuser": false,
"username": "tom",
"first_name": "",
"last_name": "",
"is_staff": false,
"is_active": true,
"date_joined": "2023-04-25T07:10:40.972Z",
"email": "thomas@irgang.eu",
"groups": [],
"user_permissions": []
}
},
{
"model": "repapp.guest",
"pk": 4,
"fields": {
"identifier": "272623d8b634cef82ba1c8981593cd827a373226c492b328671d31418a19bcc1",
"name": "Thomas",
"phone": "012345678",
"residence": "Allersberg",
"mail": "thomas@irgang.eu",
"user": 3
}
},
{
"model": "repapp.device",
"pk": 2,
"fields": {
"identifier": "5f3d413409ca9c90c99540fd5677cc1147156f100d7bd29d3a1c1940e5f2c6ac",
"date": "2023-04-25",
"device": "Test Gerät",
"manufacturer": "Test Hersteller",
"error": "Fehler",
"follow_up": false,
"device_picture": "",
"type_plate_picture": "",
"confirmed": true,
"guest": 4,
"cafe": 1
}
} }
] ]

View file

@ -3,25 +3,42 @@ Django forms for RepApp.
""" """
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from crispy_forms.layout import Field
class HoneypotField(forms.BooleanField): EMPTY_VALUES = (None, '')
class HoneypotWidget(forms.TextInput):
""" """
Simple honeypot field. This widget shows a by CSS hidden input field. If this form gets manipulated
by a bot the form is rejected.
""" """
default_widget = forms.CheckboxInput( is_hidden = True
{'style': 'display:none !important;', 'tabindex': '-1', 'autocomplete': 'off'})
def __init__(self, *args, **kwargs): def __init__(self, attrs=None, html_comment=False, *args, **kwargs):
kwargs.setdefault('widget', HoneypotField.default_widget) self.html_comment = html_comment
kwargs['required'] = False super(HoneypotWidget, self).__init__(attrs, *args, **kwargs)
super().__init__(*args, **kwargs) if not 'class' in self.attrs:
self.attrs['style'] = 'display:none'
def render(self, *args, **kwargs):
value = super(HoneypotWidget, self).render(*args, **kwargs)
if self.html_comment:
value = '<!-- %s -->' % value
return value
class HoneypotField(forms.Field):
"""
This widget implements a simple honey pot field as spam protection.
"""
widget = HoneypotWidget
def clean(self, value): def clean(self, value):
if cleaned_value := super().clean(value): if self.initial in EMPTY_VALUES and value in EMPTY_VALUES or value == self.initial:
raise ValidationError('') return value
else: raise ValidationError('Anti-spam field changed in value.')
return cleaned_value
class RegisterDevice(forms.Form): class RegisterDevice(forms.Form):
@ -48,10 +65,10 @@ class RegisterDevice(forms.Form):
widget=forms.Textarea widget=forms.Textarea
) )
device_picture = forms.FileField( device_picture = forms.FileField(
label="Foto vom Gerät ", required=False label="Foto vom Gerät", required=False
) )
type_plate_picture = forms.FileField( type_plate_picture = forms.FileField(
label="Foto vom Typenschild ", required=False label="Foto vom Typenschild", required=False
) )
follow_up = forms.BooleanField( follow_up = forms.BooleanField(
label="Folgetermin", label="Folgetermin",

View file

@ -1,31 +1,71 @@
import time import time
import os import os
import datetime
import random
from pathlib import Path from pathlib import Path
from hashlib import sha256
from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.contrib.auth import get_user_model
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver import ActionChains, Keys
from .models import CustomUser, OneTimeLogin
class WorkflowTests(StaticLiveServerTestCase): class WorkflowTests(StaticLiveServerTestCase):
"""
Tests for workflows.
"""
fixtures = ["cafe-data.json"] fixtures = ["cafe-data.json"]
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.selenium = WebDriver() cls.selenium = WebDriver()
cls.selenium.implicitly_wait(10) cls.selenium.implicitly_wait(50)
cls.selenium.maximize_window()
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.selenium.quit() cls.selenium.quit()
super().tearDownClass() super().tearDownClass()
def setUp(self):
super().setUp()
self.action_chains = ActionChains(self.selenium)
self.password = "ATestPassword"
user = CustomUser.objects.get(pk=3)
user.set_password(self.password)
user.save()
self.user = user
print(f'Password of user {user.email} updated to {self.password}.')
url = "/guest/profile/"
secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
'utf-8')
).hexdigest()
self.secret = secret
secret_hash = sha256(secret.encode('utf-8')).hexdigest()
one_time_login = OneTimeLogin(
secret=secret_hash,
user=user,
url=url,
)
one_time_login.save()
self.one_time_login = one_time_login
def test_register_device(self): def test_register_device(self):
"""
Test device registration flows.
"""
# Open landing page # Open landing page
self.selenium.get(f"{self.live_server_url}/") self.selenium.get(f"{self.live_server_url}/")
# Click on a register link # Click on a register link
self.selenium.find_element(By.CLASS_NAME, "register_link").click() self.selenium.find_element(By.CLASS_NAME, "register_link").click()
time.sleep(1) time.sleep(2)
# Enter new device # Enter new device
# Enter guest mail address # Enter guest mail address
mail = self.selenium.find_element(By.NAME, "mail") mail = self.selenium.find_element(By.NAME, "mail")
@ -39,6 +79,10 @@ class WorkflowTests(StaticLiveServerTestCase):
# Enter a error description # Enter a error description
error = self.selenium.find_element(By.NAME, "error") error = self.selenium.find_element(By.NAME, "error")
error.send_keys("A description of the issue\nof the device.") error.send_keys("A description of the issue\nof the device.")
# Scroll to the bottom of the page
self.selenium.find_element(
By.TAG_NAME, "body").send_keys(Keys.CONTROL, Keys.END)
time.sleep(1)
# Skip device picture and device type plate # Skip device picture and device type plate
# No follow up repair # No follow up repair
# Accept repair conditions # Accept repair conditions
@ -84,6 +128,10 @@ class WorkflowTests(StaticLiveServerTestCase):
# Enter a error description # Enter a error description
error = self.selenium.find_element(By.NAME, "error") error = self.selenium.find_element(By.NAME, "error")
error.send_keys("Another issue description.") error.send_keys("Another issue description.")
# Scroll to the bottom of the page
self.selenium.find_element(
By.TAG_NAME, "body").send_keys(Keys.CONTROL, Keys.END)
time.sleep(1)
# Add device picture # Add device picture
test_script_folder = Path(__file__).resolve().parent test_script_folder = Path(__file__).resolve().parent
fixtures_folder = os.path.join(test_script_folder, "fixtures") fixtures_folder = os.path.join(test_script_folder, "fixtures")
@ -110,3 +158,55 @@ class WorkflowTests(StaticLiveServerTestCase):
# Guest is known, no guest registration needed. # Guest is known, no guest registration needed.
self.assertTrue("confirm" in self.selenium.current_url) self.assertTrue("confirm" in self.selenium.current_url)
self.assertTrue("Anmeldung erfolgreich!" in self.selenium.page_source) self.assertTrue("Anmeldung erfolgreich!" in self.selenium.page_source)
def test_login_and_view_profile(self):
"""
Test login and profile page.
"""
# Open login form
self.selenium.get(f"{self.live_server_url}/accounts/login/")
# Enter login data
# Enter guest mail address
username = self.selenium.find_element(By.NAME, "username")
username.send_keys(self.user.email)
# Enter password
password = self.selenium.find_element(By.NAME, "password")
password.send_keys(self.password)
time.sleep(1)
# Submit form
self.selenium.find_element(
By.XPATH, '//button[@type="submit"]').click()
time.sleep(1)
self.selenium.get(f"{self.live_server_url}/guest/profile/")
time.sleep(1)
self.assertTrue(self.user.email in self.selenium.page_source)
self.assertTrue("/guest/profile/" in self.selenium.current_url)
time.sleep(1)
def test_one_time_login_and_view_device(self):
"""
Test one time login and device page.
"""
# Open login form
self.selenium.get(
f"{self.live_server_url}/onetimelogin/{self.secret}/")
time.sleep(1)
self.assertTrue(self.user.email in self.selenium.page_source)
self.assertTrue("Login erfolgreich" in self.selenium.page_source)
self.assertTrue("/guest/profile/" in self.selenium.current_url)
time.sleep(1)
self.selenium.get(
f"{self.live_server_url}/device/5f3d413409ca9c90c99540fd5677cc1147156f100d7bd29d3a1c1940e5f2c6ac/")
time.sleep(1)
self.assertTrue("Test Gerät" in self.selenium.page_source)
self.assertTrue("Test Hersteller" in self.selenium.page_source)
self.assertTrue("/device/" in self.selenium.current_url)
time.sleep(1)
def test_member_login(self):
self.selenium.get(
f"{self.live_server_url}/member/login/")
time.sleep(1)
self.selenium.find_element(By.CLASS_NAME, 'login_link').click()
time.sleep(1)
self.assertTrue('sso.makes-hacks-hip.de' in self.selenium.current_url)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> {% load static %}
<html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" <meta name="description"
@ -8,24 +9,66 @@
content="{% block keywords %} {% endblock keywords %}" /> content="{% block keywords %} {% endblock keywords %}" />
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" /> content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" <link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" <title>RepApp -
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous" />
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<title>RepApp
{% block title %} {% block title %}
{% endblock title %} {% endblock title %}
</title> </title>
<link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
{% block head %} {% block head %}
{% endblock head %} {% endblock head %}
</head> </head>
<body> <body>
<div class="container">
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
<a href="{% url 'index' %}"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<img src="{% static 'repapp/images/logo.jpg' %}"
height="50px"
width="168px"
alt="Repair-Café Logo">
</a>
<ul class="nav nav-pills">
<li class="nav-item">
<a href="https://www.repaircafe-hilpoltstein.de/" class="nav-link">Repair-Café</a>
</li>
<li class="nav-item">
<a href="{% url 'index' %}" class="nav-link">Anmeldung</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link">Abmelden</a>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="nav-link">Gast Anmeldung</a>
</li>
<li class="nav-item">
<a href="{% url 'member_login' %}" class="nav-link">Mitglieder Anmeldung</a>
</li>
{% endif %}
</ul>
</header>
</div>
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3 text-center"
style="background-image: url({% static 'repapp/images/header_background.jpg' %});
background-repeat: no-repeat;
background-size: auto 100%;
background-position: center">
<div class="col-md-5 p-lg-5 mx-auto my-5">
<h1 class="display-4 font-weight-normal"
style="font-size: 60px;
font-weight: 700;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 1);
color: white">
{% block page_title %}
{% endblock page_title %}
</h1>
</div>
</div>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-8"> <div class="col-12">
{% if messages %} {% if messages %}
{% for message in messages %}<div class="alert alert-primary" role="alert">{{ message }}</div>{% endfor %} {% for message in messages %}<div class="alert alert-primary" role="alert">{{ message }}</div>{% endfor %}
{% endif %} {% endif %}
@ -34,14 +77,18 @@
</div> </div>
</div> </div>
</div> </div>
<footer> <div class="container">
{% if user.is_authenticated %} <footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<a href="{% url 'logout' %}">Gast Logout</a> <p class="col-md-4 mb-0 text-muted">© 2023 Repair-Café Hilpoltstein</p>
<a href="{% url 'member_login' %}">Mitarbeiter Logout</a> <ul class="nav col-md-4 justify-content-end">
{% else %} <li class="nav-item">
<a href="{% url 'login' %}">Gast Login</a> <a href="https://www.repaircafe-hilpoltstein.de/impressum"
<a href="{% url 'member_login' %}">Mitarbeiter Login</a> class="nav-link px-2 text-muted">Impressum & Datenschutz</a>
{% endif %} </li>
</footer> </ul>
</footer>
</div>
<script src="{% static 'repapp/js/jquery-3.6.4.min.js' %}"></script>
<script src="{% static 'repapp/js/bootstrap.min.js' %}"></script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,81 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description"
content="{% block description %} {% endblock description %}" />
<meta name="keywords"
content="{% block keywords %} {% endblock keywords %}" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
<title>RepApp -
{% block title %}
Bootstrap
{% endblock title %}
</title>
<link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
{% block head %}
{% endblock head %}
</head>
<body>
<div class="container">
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
<a href="{% url 'index' %}"
class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<img src="{% static 'repapp/images/logo.jpg' %}"
height="50px"
width="168px"
alt="Repair-Café Logo">
</a>
<ul class="nav nav-pills">
<li class="nav-item">
<a href="https://www.repaircafe-hilpoltstein.de/" class="nav-link">Repair-Café</a>
</li>
<li class="nav-item">
<a href="{% url 'index' %}" class="nav-link">Anmeldung</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link">Abmelden</a>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="nav-link">Gast Anmeldung</a>
</li>
<li class="nav-item">
<a href="{% url 'member_login' %}" class="nav-link">Mitglieder Anmeldung</a>
</li>
{% endif %}
</ul>
</header>
</div>
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3 text-center"
style="background-image: url({% static 'repapp/images/header_background.jpg' %});
background-repeat: no-repeat;
background-size: auto 100%;
background-position: center">
<div class="col-md-5 p-lg-5 mx-auto my-5">
<h1 class="display-4 font-weight-normal"
style="font-size: 60px;
font-weight: 700;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 1);
color: white">Anmeldung</h1>
</div>
</div>
<div class="container">
<div class="row justify-content-center">
<div class="col-8">
{% if messages %}
{% for message in messages %}<div class="alert alert-primary" role="alert">{{ message }}</div>{% endfor %}
{% endif %}
{% block content %}
{% endblock content %}
</div>
</div>
</div>
<script src="{% static 'repapp/js/jquery-3.6.4.min.js' %}"></script>
<script src="{% static 'repapp/js/bootstrap.min.js' %}"></script>
</body>
</html>

View file

@ -2,9 +2,10 @@
{% block title %} {% block title %}
- {{ device.device }} - {{ device.device }}
{% endblock title %} {% endblock title %}
{% block page_title %}
Gerät: {{ device.device }}
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Gerät: {{ device.device }}</h1>
<hr class="mt-0 mb-4">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>

View file

@ -1,10 +1,11 @@
{% extends "repapp/base.html" %} {% extends "repapp/base.html" %}
{% block title %} {% block title %}
- Gast Daten - Ihre Daten
{% endblock title %} {% endblock title %}
{% block page_title %}
Ihre Daten
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Ihre Daten</h1>
<hr class="mt-0 mb-4">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>

View file

@ -282,8 +282,11 @@
} }
</style> </style>
{% endblock head %} {% endblock head %}
{% block page_title %}
Anmeldung
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Repair-Cafés</h1> <h1 class="mt-2">Repair-Café auswählen</h1>
<hr class="mt-0 mb-4"> <hr class="mt-0 mb-4">
<ul class="list-group"> <ul class="list-group">
{% for cafe in object_list %} {% for cafe in object_list %}

View file

@ -1,17 +1,16 @@
{% extends "repapp/base.html" %} {% extends "repapp/base.html" %}
{% block title %} {% block title %}
- Mitarbeiter Login Mitarbeiter Anmeldung
{% endblock title %} {% endblock title %}
{% block page_title %}
Mitarbeiter
{% if user.is_authenticated %}
Anmelden
{% else %}
Abmelden
{% endif %}
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">
Mitarbeiter
{% if user.is_authenticated %}
Logout
{% else %}
Login
{% endif %}
</h1>
<hr class="mt-0 mb-4">
<div> <div>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p>Angemeldeter Benutzer: {{ user.email }}</p> <p>Angemeldeter Benutzer: {{ user.email }}</p>
@ -20,7 +19,7 @@
<button type="submit" class="btn btn-primary">Abmelden</button> <button type="submit" class="btn btn-primary">Abmelden</button>
</form> </form>
{% else %} {% else %}
<a href="{% url 'oidc_authentication_init' %}">Anmelden mit Makes-Hacks-Hip</a> <a class="login_link" href="{% url 'oidc_authentication_init' %}">Anmelden mit Makes-Hacks-Hip</a>
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -3,9 +3,10 @@
{% block title %} {% block title %}
- Gerät anmelden - Gerät anmelden
{% endblock title %} {% endblock title %}
{% block page_title %}
Gerät anmelden
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Gerät anmelden</h1>
<hr class="mt-0 mb-4">
<form action="{% url 'register_device' cafe.id %}" <form action="{% url 'register_device' cafe.id %}"
method="post" method="post"
enctype="multipart/form-data"> enctype="multipart/form-data">

View file

@ -1,9 +1,10 @@
{% extends "repapp/base.html" %} {% extends "repapp/base.html" %}
{% block title %} {% block title %}
{% endblock title %} {% endblock title %}
{% block page_title %}
Anmeldung erfolgreich!
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Anmeldung erfolgreich!</h1>
<hr class="mt-0 mb-4">
<p> <p>
Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}. Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}.
</p> </p>

View file

@ -3,9 +3,10 @@
{% block title %} {% block title %}
- Gast anmelden - Gast anmelden
{% endblock title %} {% endblock title %}
{% block page_title %}
Ihre Daten
{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="mt-2">Ihre Daten</h1>
<hr class="mt-0 mb-4">
<form action="{% url 'register_guest' cafe.id device.identifier mail %}" <form action="{% url 'register_guest' cafe.id device.identifier mail %}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}

View file

@ -1,17 +1,25 @@
""" """
Tests for RepApp. Tests for RepApp.
""" """
import time
from hashlib import sha256 from hashlib import sha256
from datetime import datetime, timedelta from datetime import datetime, timedelta
import django.utils.timezone import django.utils.timezone
from django.test import TestCase, RequestFactory
from django.test import TestCase
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.core import mail
from django.contrib.messages.storage.fallback import FallbackStorage
from .backends import OneTimeLoginBackend, EmailBackend from .backends import OneTimeLoginBackend, EmailBackend, create_repapp_user
from .models import OneTimeLogin, CustomUser, Cafe from .models import (OneTimeLogin, CustomUser, Cafe,
Organisator, Reparateur, Device, Guest)
from .views import (send_one_time_login_mail,
send_confirmation_mails,
send_guest_account_mail,
is_member,
create_one_time_login)
class OneTimeLoginTest(TestCase): class OneTimeLoginTest(TestCase):
@ -100,19 +108,100 @@ class EmailLoginTest(TestCase):
self.assertEqual(result, None) self.assertEqual(result, None)
class UtilsTest(TestCase): class BackendsTest(TestCase):
""" """
Test for utility functions. Test for backend utility functions.
""" """
pass
def setUp(self):
organisator = Organisator(
name="Organisator Name",
mail="orga@example.com",
)
organisator.save()
self.organisator = organisator
reparateur = Reparateur(
name="Reparateur Name",
mail="repa@example.com",
)
reparateur.save()
self.reparateur = reparateur
def test_create_reparateur(self):
"""
Ensure that a new reparateur is created.
"""
user = CustomUser(
username="Other Reparateur",
email="repa2@example.com",
)
user.save()
create_repapp_user(user)
reparateur = Reparateur.objects.filter(mail=user.email).first()
self.assertIsNotNone(reparateur)
self.assertEqual(reparateur.name, user.username)
def test_update_reparateur(self):
"""
Ensure that the reparateur name gets updated.
"""
user = CustomUser(
username="First Reparateur",
email="repa@example.com",
)
user.save()
create_repapp_user(user)
reparateur = Reparateur.objects.filter(mail=user.email).first()
self.assertIsNotNone(reparateur)
self.assertEqual(reparateur.name, user.username)
def test_update_organisator(self):
"""
Ensure that the organisator name gets updated.
"""
user = CustomUser(
username="The Organisator",
email="orga@example.com",
)
user.save()
create_repapp_user(user)
organisator = Organisator.objects.filter(mail=user.email).first()
self.assertIsNotNone(organisator)
self.assertEqual(organisator.name, user.username)
class FormsTest(TestCase):
"""
Test for RepApp form features.
"""
fixtures = ["cafe-data.json"]
def test_from_protection(self):
client = Client(enforce_csrf_checks=True)
response = client.get(reverse('register_device', kwargs={
'cafe': 1,
}))
# ensure honeypot field is available
self.assertContains(
response, 'type="text" name="accept_agb" style="display:none"')
# ensure csrf is available
self.assertContains(response, 'csrfmiddlewaretoken')
class ViewsTest(TestCase): class ViewsTest(TestCase):
""" """
Test for RepApp views. Test for RepApp views.
""" """
fixtures = ["cafe-data.json"]
def setUp(self): def setUp(self):
self.factory = RequestFactory()
cafe = Cafe(location="neuer Ort", address="neue Adresse", cafe = Cafe(location="neuer Ort", address="neue Adresse",
event_date=django.utils.timezone.now()) event_date=django.utils.timezone.now())
cafe.save() cafe.save()
@ -124,6 +213,21 @@ class ViewsTest(TestCase):
old_cafe.save() old_cafe.save()
self.old_cafe = old_cafe self.old_cafe = old_cafe
user = CustomUser(
username="ATestUser",
email="testuser@example.com",
)
user.set_password("aTestPassword")
user.save()
self.user = user
reparateur = Reparateur(
name="ATestUser",
mail="testuser@example.com"
)
reparateur.save()
self.reparateur = reparateur
def test_index(self): def test_index(self):
""" """
Test index page. Test index page.
@ -140,3 +244,161 @@ class ViewsTest(TestCase):
# ensure old cafe is not displayed # ensure old cafe is not displayed
self.assertNotContains(response, self.old_cafe.location) self.assertNotContains(response, self.old_cafe.location)
self.assertNotContains(response, self.old_cafe.address) self.assertNotContains(response, self.old_cafe.address)
def test_register_device(self):
"""
Test register device view
"""
client = Client(enforce_csrf_checks=True)
response = client.get(reverse('register_device', kwargs={
'cafe': 1,
}))
self.assertEqual(response.status_code, 200)
# ensure all required fields are displayed
self.assertContains(response, "eMail Adresse")
self.assertContains(response, "Art des Geräts")
self.assertContains(response, "Hersteller")
self.assertContains(response, "Fehlerbeschreibung")
self.assertContains(response, "Foto vom Gerät")
self.assertContains(response, "Foto vom Typenschild")
self.assertContains(response, "Folgetermin")
self.assertContains(response, "Informationen zur Reparaturabwicklung")
self.assertContains(response, "Datenschutz")
# ensure security fields are contained
self.assertContains(
response, 'type="text" name="accept_agb" style="display:none"')
self.assertContains(response, 'csrfmiddlewaretoken')
def test_member_login(self):
"""
Test member login view
"""
client = Client(enforce_csrf_checks=True)
response = client.get(reverse('member_login'))
self.assertEqual(response.status_code, 200)
# ensure OIDC link is displayed
self.assertContains(response, "Anmelden mit Makes-Hacks-Hip")
client.login(username="testuser@example.com", password="aTestPassword")
response = client.get(reverse('member_login'))
self.assertEqual(response.status_code, 200)
# ensure logout button is displayed
self.assertContains(response, "Abmelden")
# ensure user is displayed
self.assertContains(response, "testuser@example.com")
def test_send_one_time_login_mail(self):
"""
Test that a mail is sent with the right link included.
"""
secret = "A_SECRET"
mail_address = "guest@example.com"
request = self.factory.get('/')
setattr(request, 'session', 'session')
setattr(request, '_messages', FallbackStorage(request))
send_one_time_login_mail(secret, mail_address, request)
self.assertEqual(len(mail.outbox), 1)
self.assertTrue("Einmal-Ammeldelink" in mail.outbox[0].subject)
self.assertEqual(mail.outbox[0].to, [mail_address])
self.assertTrue(f'/onetimelogin/{secret}/' in mail.outbox[0].body)
def test_send_confirmation_mails(self):
"""
Test that the confirmation mails are sent.
"""
device = Device.objects.get(pk=2)
guest = Guest.objects.get(pk=4)
cafe = Cafe.objects.get(pk=1)
organisator = Organisator(
name="Orga",
mail="orga@example.com"
)
organisator.save()
request = self.factory.get('/')
setattr(request, 'session', 'session')
setattr(request, '_messages', FallbackStorage(request))
send_confirmation_mails(device, guest, cafe, request)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject,
f"Neues Gerät { device.device }")
self.assertEqual(mail.outbox[0].to, [organisator.mail])
self.assertTrue("Geräteanmeldung" in mail.outbox[1].subject)
self.assertEqual(mail.outbox[1].to, [guest.mail])
self.assertTrue("Geräteanmeldung" in mail.outbox[1].body)
def test_send_guest_account_mail(self):
"""
Test that an account creation mail was sent.
"""
password = "A_SECRET"
guest = Guest.objects.get(pk=4)
request = self.factory.get('/')
setattr(request, 'session', 'session')
setattr(request, '_messages', FallbackStorage(request))
send_guest_account_mail(guest, password, request)
self.assertEqual(len(mail.outbox), 1)
self.assertTrue("Benutzerkonto" in mail.outbox[0].subject)
self.assertEqual(mail.outbox[0].to, [guest.mail])
self.assertTrue(f'{password}' in mail.outbox[0].body)
self.assertTrue(f'{guest.mail}' in mail.outbox[0].body)
def test_is_member(self):
"""
Test that members are detected and not guests.
"""
organisator = Organisator(
name="Orga",
mail="orga@example.com"
)
organisator.save()
organisator_user = CustomUser(
username="orga",
email=organisator.mail
)
organisator_user.save()
reparateur = Reparateur(
name="Reparateur",
mail="repa@example.com"
)
reparateur.save()
reparateur_user = CustomUser(
username="repa",
email=reparateur.mail
)
reparateur_user.save()
guest = CustomUser.objects.get(pk=3)
self.assertTrue(is_member(organisator_user))
self.assertTrue(is_member(reparateur_user))
self.assertFalse(is_member(guest))
def test_create_one_time_login(self):
"""
Test one time login creation.
"""
user = CustomUser.objects.get(pk=3)
url = "/test/url/"
secret = create_one_time_login(user, url)
secret_hash = sha256(secret.encode('utf-8')).hexdigest()
otl = OneTimeLogin.objects.get(secret=secret_hash)
self.assertEqual(otl.url, url)

View file

@ -22,10 +22,12 @@ urlpatterns = [
views.device_view, name="view_device"), views.device_view, name="view_device"),
path("guest/profile/", path("guest/profile/",
views.profile, name="guest_profile"), views.profile, name="guest_profile"),
path("onetimelogin/<str:secret>/",
views.one_time_login, name="one_time_login"),
path("member/login/", path("member/login/",
views.member_login, name="member_login"), views.member_login, name="member_login"),
path("cron", views.cron, name="cron"), path("cron", views.cron, name="cron"),
path("bootstrap", views.bootstrap, name="bootstrap"),
path("process_mails", views.process_mails, name="process_mails"), path("process_mails", views.process_mails, name="process_mails"),
path("onetimelogin/<str:secret>/",
views.one_time_login, name="one_time_login"),
] ]

View file

@ -26,6 +26,9 @@ from .forms import RegisterDevice, RegisterGuest
def send_one_time_login_mail(secret, mail, request): def send_one_time_login_mail(secret, mail, request):
"""
Send a mail with a one time login link.
"""
url = request.build_absolute_uri( url = request.build_absolute_uri(
f'/onetimelogin/{secret}/') f'/onetimelogin/{secret}/')
subject = render_to_string( subject = render_to_string(
@ -52,6 +55,9 @@ def send_one_time_login_mail(secret, mail, request):
def send_confirmation_mails(device, guest, cafe, request): def send_confirmation_mails(device, guest, cafe, request):
"""
Send confirmation mails for device registration.
"""
organizers = [] organizers = []
for organizer in Organisator.objects.all(): for organizer in Organisator.objects.all():
organizers.append(organizer.mail) organizers.append(organizer.mail)
@ -110,6 +116,9 @@ def send_confirmation_mails(device, guest, cafe, request):
def send_guest_account_mail(guest, password, request): def send_guest_account_mail(guest, password, request):
"""
Send guest user account created mail.
"""
url = request.build_absolute_uri('/guest/profile/') url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string( subject = render_to_string(
'repapp/mail/mail_new_guest_subject.html').replace('\n', '') 'repapp/mail/mail_new_guest_subject.html').replace('\n', '')
@ -139,6 +148,9 @@ def send_guest_account_mail(guest, password, request):
def is_member(user): def is_member(user):
"""
Test is a user is a Repair-Café member.
"""
organisator = Organisator.objects.filter(mail=user.email).first() organisator = Organisator.objects.filter(mail=user.email).first()
reparateur = Reparateur.objects.filter(mail=user.email).first() reparateur = Reparateur.objects.filter(mail=user.email).first()
return organisator or reparateur return organisator or reparateur
@ -146,7 +158,7 @@ def is_member(user):
def create_one_time_login(user, url) -> str: def create_one_time_login(user, url) -> str:
""" """
create_one_time_login create a one time login object for user logins. create_one_time_login creates a one time login object for guest user logins.
""" """
secret = sha256( secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode( f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
@ -435,3 +447,13 @@ def one_time_login(request, secret: str):
else: else:
messages.add_message(request, messages.ERROR, 'Login fehlgeschlagen.') messages.add_message(request, messages.ERROR, 'Login fehlgeschlagen.')
return HttpResponseRedirect(reverse_lazy('index')) return HttpResponseRedirect(reverse_lazy('index'))
def bootstrap(request):
"""
Bootstrap design test page.
"""
return render(
request,
"repapp/bootstrap.html"
)

View file

@ -6,6 +6,7 @@ pylint-django==2.5.3
pylint-plugin-utils==0.7 pylint-plugin-utils==0.7
django-crispy-forms==2.0 django-crispy-forms==2.0
crispy-bootstrap4==2022.1 crispy-bootstrap4==2022.1
crispy-bootstrap5==0.7
mozilla-django-oidc==3.0.0 mozilla-django-oidc==3.0.0
djlint==1.23.3 djlint==1.23.3
django-import-export==3.2.0 django-import-export==3.2.0

View file

@ -2,5 +2,6 @@ Django==4.2
gunicorn==20.1.0 gunicorn==20.1.0
django-crispy-forms==2.0 django-crispy-forms==2.0
crispy-bootstrap4==2022.1 crispy-bootstrap4==2022.1
crispy-bootstrap5==0.7
mozilla-django-oidc==3.0.0 mozilla-django-oidc==3.0.0
django-import-export==3.2.0 django-import-export==3.2.0