finish liveservertests

This commit is contained in:
Tom Irgang 2023-04-27 11:09:41 +02:00
parent f1d65e2bd6
commit 69aa578ad2
8 changed files with 272 additions and 24 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/**

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):

View file

@ -1,12 +1,21 @@
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.chrome.webdriver import WebDriver
from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.firefox.webdriver import WebDriver
from .models import CustomUser, Guest, OneTimeLogin
class WorkflowTests(StaticLiveServerTestCase): class WorkflowTests(StaticLiveServerTestCase):
"""
Tests for workflows.
"""
fixtures = ["cafe-data.json"] fixtures = ["cafe-data.json"]
@classmethod @classmethod
@ -20,7 +29,35 @@ class WorkflowTests(StaticLiveServerTestCase):
cls.selenium.quit() cls.selenium.quit()
super().tearDownClass() super().tearDownClass()
def setUp(self):
super().setUp()
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
@ -110,3 +147,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)

View file

@ -20,7 +20,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

@ -10,8 +10,8 @@ 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 .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)
class OneTimeLoginTest(TestCase): class OneTimeLoginTest(TestCase):
@ -100,11 +100,89 @@ 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):

View file

@ -22,10 +22,11 @@ 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("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"),
] ]