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
**/data.json
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",
"Keycloak",
"tabindex",
"onetimelogin"
"onetimelogin",
"Ammeldelink"
],
"ignoreWords": [],
"import": []

View file

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

View file

@ -12,6 +12,9 @@ logger = logging.getLogger(__name__)
def generate_username(email):
"""
Generate a valid user name form the mail address.
"""
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
@ -19,19 +22,29 @@ def generate_username(email):
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()
if not organisator:
# no organisator, create or update reparateur
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
# create new reparateur
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
# update name of existing reparateur
reparateur.name = user.username
reparateur.save()
else:
# update name of organisator
organisator.name = user.username
organisator.save()
@ -74,15 +87,17 @@ class KeycloakOIDCAB(OIDCAuthenticationBackend):
def create_user(self, 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:
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.username = claims.get('preferred_username', fallback_name)
user.save()
except Exception as 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()
logger.debug(f'Updated username {user.username}')

View file

@ -7,5 +7,52 @@
"address": "Patersholzer Weg 19, Hilpoltstein",
"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.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(
{'style': 'display:none !important;', 'tabindex': '-1', 'autocomplete': 'off'})
is_hidden = True
def __init__(self, *args, **kwargs):
kwargs.setdefault('widget', HoneypotField.default_widget)
kwargs['required'] = False
super().__init__(*args, **kwargs)
def __init__(self, attrs=None, html_comment=False, *args, **kwargs):
self.html_comment = html_comment
super(HoneypotWidget, self).__init__(attrs, *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):
if cleaned_value := super().clean(value):
raise ValidationError('')
else:
return cleaned_value
if self.initial in EMPTY_VALUES and value in EMPTY_VALUES or value == self.initial:
return value
raise ValidationError('Anti-spam field changed in value.')
class RegisterDevice(forms.Form):
@ -48,10 +65,10 @@ class RegisterDevice(forms.Form):
widget=forms.Textarea
)
device_picture = forms.FileField(
label="Foto vom Gerät ", required=False
label="Foto vom Gerät", required=False
)
type_plate_picture = forms.FileField(
label="Foto vom Typenschild ", required=False
label="Foto vom Typenschild", required=False
)
follow_up = forms.BooleanField(
label="Folgetermin",

View file

@ -1,31 +1,71 @@
import time
import os
import datetime
import random
from pathlib import Path
from hashlib import sha256
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.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):
"""
Tests for workflows.
"""
fixtures = ["cafe-data.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.selenium = WebDriver()
cls.selenium.implicitly_wait(10)
cls.selenium.implicitly_wait(50)
cls.selenium.maximize_window()
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
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):
"""
Test device registration flows.
"""
# Open landing page
self.selenium.get(f"{self.live_server_url}/")
# Click on a register link
self.selenium.find_element(By.CLASS_NAME, "register_link").click()
time.sleep(1)
time.sleep(2)
# Enter new device
# Enter guest mail address
mail = self.selenium.find_element(By.NAME, "mail")
@ -39,6 +79,10 @@ class WorkflowTests(StaticLiveServerTestCase):
# Enter a error description
error = self.selenium.find_element(By.NAME, "error")
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
# No follow up repair
# Accept repair conditions
@ -84,6 +128,10 @@ class WorkflowTests(StaticLiveServerTestCase):
# Enter a error description
error = self.selenium.find_element(By.NAME, "error")
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
test_script_folder = Path(__file__).resolve().parent
fixtures_folder = os.path.join(test_script_folder, "fixtures")
@ -110,3 +158,55 @@ class WorkflowTests(StaticLiveServerTestCase):
# Guest is known, no guest registration needed.
self.assertTrue("confirm" in self.selenium.current_url)
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>
<html lang="de">
{% load static %}
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description"
@ -8,24 +9,66 @@
content="{% block keywords %} {% endblock keywords %}" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
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
<link rel="stylesheet" href="{% static 'repapp/css/bootstrap.min.css' %}" />
<title>RepApp -
{% block title %}
{% 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">
{% block page_title %}
{% endblock page_title %}
</h1>
</div>
</div>
<div class="container">
<div class="row justify-content-center">
<div class="col-8">
<div class="col-12">
{% if messages %}
{% for message in messages %}<div class="alert alert-primary" role="alert">{{ message }}</div>{% endfor %}
{% endif %}
@ -34,14 +77,18 @@
</div>
</div>
</div>
<footer>
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">Gast Logout</a>
<a href="{% url 'member_login' %}">Mitarbeiter Logout</a>
{% else %}
<a href="{% url 'login' %}">Gast Login</a>
<a href="{% url 'member_login' %}">Mitarbeiter Login</a>
{% endif %}
<div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<p class="col-md-4 mb-0 text-muted">© 2023 Repair-Café Hilpoltstein</p>
<ul class="nav col-md-4 justify-content-end">
<li class="nav-item">
<a href="https://www.repaircafe-hilpoltstein.de/impressum"
class="nav-link px-2 text-muted">Impressum & Datenschutz</a>
</li>
</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>
</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 %}
- {{ device.device }}
{% endblock title %}
{% block page_title %}
Gerät: {{ device.device }}
{% endblock page_title %}
{% block content %}
<h1 class="mt-2">Gerät: {{ device.device }}</h1>
<hr class="mt-0 mb-4">
<table class="table">
<tbody>
<tr>

View file

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

View file

@ -282,8 +282,11 @@
}
</style>
{% endblock head %}
{% block page_title %}
Anmeldung
{% endblock page_title %}
{% 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">
<ul class="list-group">
{% for cafe in object_list %}

View file

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

View file

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

View file

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

View file

@ -3,9 +3,10 @@
{% block title %}
- Gast anmelden
{% endblock title %}
{% block page_title %}
Ihre Daten
{% endblock page_title %}
{% 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 %}"
method="post">
{% csrf_token %}

View file

@ -1,17 +1,25 @@
"""
Tests for RepApp.
"""
import time
from hashlib import sha256
from datetime import datetime, timedelta
import django.utils.timezone
from django.test import TestCase
from django.test import TestCase, RequestFactory
from django.test import Client
from django.urls import reverse
from django.core import mail
from django.contrib.messages.storage.fallback import FallbackStorage
from .backends import OneTimeLoginBackend, EmailBackend
from .models import OneTimeLogin, CustomUser, Cafe
from .backends import OneTimeLoginBackend, EmailBackend, create_repapp_user
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):
@ -100,19 +108,100 @@ class EmailLoginTest(TestCase):
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):
"""
Test for RepApp views.
"""
fixtures = ["cafe-data.json"]
def setUp(self):
self.factory = RequestFactory()
cafe = Cafe(location="neuer Ort", address="neue Adresse",
event_date=django.utils.timezone.now())
cafe.save()
@ -124,6 +213,21 @@ class ViewsTest(TestCase):
old_cafe.save()
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):
"""
Test index page.
@ -140,3 +244,161 @@ class ViewsTest(TestCase):
# ensure old cafe is not displayed
self.assertNotContains(response, self.old_cafe.location)
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"),
path("guest/profile/",
views.profile, name="guest_profile"),
path("onetimelogin/<str:secret>/",
views.one_time_login, name="one_time_login"),
path("member/login/",
views.member_login, name="member_login"),
path("cron", views.cron, name="cron"),
path("bootstrap", views.bootstrap, name="bootstrap"),
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):
"""
Send a mail with a one time login link.
"""
url = request.build_absolute_uri(
f'/onetimelogin/{secret}/')
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):
"""
Send confirmation mails for device registration.
"""
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
@ -110,6 +116,9 @@ def send_confirmation_mails(device, guest, cafe, request):
def send_guest_account_mail(guest, password, request):
"""
Send guest user account created mail.
"""
url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string(
'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):
"""
Test is a user is a Repair-Café member.
"""
organisator = Organisator.objects.filter(mail=user.email).first()
reparateur = Reparateur.objects.filter(mail=user.email).first()
return organisator or reparateur
@ -146,7 +158,7 @@ def is_member(user):
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(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
@ -435,3 +447,13 @@ def one_time_login(request, secret: str):
else:
messages.add_message(request, messages.ERROR, 'Login fehlgeschlagen.')
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
django-crispy-forms==2.0
crispy-bootstrap4==2022.1
crispy-bootstrap5==0.7
mozilla-django-oidc==3.0.0
djlint==1.23.3
django-import-export==3.2.0

View file

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