ungleich-user/dal/tests/base.py
Sajeyks 13b1d9c23a Add end-to-end tests and pin production baseline dependencies
Restore root requirements.txt pinned to what production runs
(Django 2.2, last patch 2.2.28) so the upgrade starts from a
reproducible baseline. Restore logs/.keep; the logging config fails
on a fresh clone without the directory.

The tests drive every view over HTTP against the real test LDAP:
login, registration and activation, data and password changes,
password reset, account deletion, the create API and OTP seeds.
Known bugs are recorded as expected failures:
- password reset POST changes any user's password without a token
- LDAP filter injection in reset (user='*' hits the first entry)
- activation link carries the base64-encoded password
- a username from ou=users can be registered again under ou=customers
2026-11-06 14:46:24 +03:00

134 lines
4.8 KiB
Python

"""
Shared helpers for the end-to-end tests.
These tests drive the real views over HTTP (Django test client) against a
real OpenLDAP server, started with:
docker compose -f docker-compose.dev.yml up -d --build
They are deliberately black-box: they assert on what a user sees (templates,
emails, status codes) and on what ends up in LDAP, so they keep passing while
the internals are rewritten during the upgrade.
"""
import os
import re
import tempfile
import uuid
from unittest import mock
import ldap3
from django.conf import settings
from django.core import mail
from django.test import TestCase, override_settings
from dal.ungleich_ldap import LdapManager
_ldap_checked = False
def admin_conn():
server = ldap3.Server(settings.AUTH_LDAP_SERVER)
return ldap3.Connection(
server, settings.LDAP_ADMIN_DN, settings.LDAP_ADMIN_PASSWORD,
auto_bind=True, raise_exceptions=False,
)
def ensure_ldap_up():
global _ldap_checked
if _ldap_checked:
return
try:
admin_conn().unbind()
except Exception as exc: # pragma: no cover - environment problem
raise RuntimeError(
"Test LDAP not reachable at {}. Start it with: docker compose -f "
"docker-compose.dev.yml up -d --build ({})".format(
settings.AUTH_LDAP_SERVER, exc)
)
_ldap_checked = True
def unique_uid(prefix="t"):
# Matches the app's username rule: lowercase letters, digits, - and _
return "{}-{}".format(prefix, uuid.uuid4().hex[:10])
class LdapTestCase(TestCase):
"""Base class: unique users per test, cleaned up afterwards."""
def setUp(self):
ensure_ldap_up()
self._created_dns = []
fd, self.uid_file = tempfile.mkstemp(prefix="ldap_max_uid_")
os.close(fd)
os.unlink(self.uid_file) # app must cope with a missing file
self._settings = override_settings(LDAP_MAX_UID_FILE_PATH=self.uid_file)
self._settings.enable()
# OTP server is external; never hit the network from tests.
self.otp_post = mock.patch("dal.views.requests.post").start()
self.otp_post.return_value = mock.Mock(status_code=201, text="{}")
self.otp_get = mock.patch("dal.views.requests.get").start()
self.otp_get.return_value = mock.Mock(status_code=200, text="[]")
self.addCleanup(mock.patch.stopall)
def tearDown(self):
conn = admin_conn()
for dn in self._created_dns:
conn.delete(dn)
conn.unbind()
self._settings.disable()
if os.path.exists(self.uid_file):
os.unlink(self.uid_file)
# LDAP helpers ---------------------------------------------------------
def make_ldap_user(self, uid=None, password="initial-pass-123",
ou="customers", email=None):
"""Create a user directly in LDAP, the same way the app does."""
uid = uid or unique_uid()
dn = "uid={},ou={},dc=ungleich,dc=ch".format(uid, ou)
conn = admin_conn()
ok = conn.add(dn, ["inetOrgPerson", "posixAccount", "ldapPublicKey"], {
"uid": uid, "cn": uid, "sn": "Tester", "givenName": "Tina",
"mail": email or "{}@example.com".format(uid),
"uidNumber": str(30000 + int(uuid.uuid4().int % 100000)),
"gidNumber": "2000", "homeDirectory": "/home/" + uid,
"userPassword": LdapManager()._ssha_password(password.encode()),
})
conn.unbind()
self.assertTrue(ok, "could not seed LDAP user {}".format(dn))
self._created_dns.append(dn)
return uid
def track(self, uid, ou="customers"):
"""Register a user created by the app for cleanup."""
self._created_dns.append("uid={},ou={},dc=ungleich,dc=ch".format(uid, ou))
def ldap_entry(self, uid, attributes=("*",)):
conn = admin_conn()
conn.search(settings.ENTIRE_SEARCH_BASE, "(uid={})".format(
ldap3.utils.conv.escape_filter_chars(uid)), attributes=list(attributes))
entries = list(conn.entries)
conn.unbind()
return entries[0] if entries else None
def can_bind(self, uid, password, ou="customers"):
server = ldap3.Server(settings.AUTH_LDAP_SERVER)
conn = ldap3.Connection(
server, "uid={},ou={},dc=ungleich,dc=ch".format(uid, ou), password)
ok = conn.bind()
conn.unbind()
return ok
# HTTP helpers ---------------------------------------------------------
def login(self, uid, password):
self.assertTrue(self.client.login(username=uid, password=password),
"login failed for {}".format(uid))
def last_mail_link(self):
self.assertTrue(mail.outbox, "no email was sent")
urls = re.findall(r"https?://testserver(/[^\s\"'<>]+)", mail.outbox[-1].body)
self.assertTrue(urls, "no link in email: " + mail.outbox[-1].body)
return urls[-1]