vergabe-teilnahme/vergabe_teilnahme/apps/accounts/test_tenant_path.py
tegwick 9345a1bb1a
All checks were successful
Application acceptance / application-tests (push) Successful in 1m12s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 25s
feat: serve isolated companies below a fixed product path
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 21:29:26 +02:00

120 lines
5.1 KiB
Python

"""Regression for an isolated instance mounted below a fixed tenant path."""
from html.parser import HTMLParser
from urllib.parse import parse_qs, urlsplit
import pytest
from django.test import Client
from django.urls import get_script_prefix, set_script_prefix
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
pytestmark = pytest.mark.django_db
PREFIX = '/demo-company'
@pytest.fixture
def tenant_path(settings):
previous = get_script_prefix()
settings.FORCE_SCRIPT_NAME = PREFIX
settings.APP_BASE_PATH = PREFIX
settings.STATIC_URL = PREFIX + '/static/'
settings.MEDIA_URL = PREFIX + '/media/'
settings.SESSION_COOKIE_PATH = settings.CSRF_COOKIE_PATH = PREFIX + '/'
settings.SESSION_COOKIE_NAME = 'vergabe_demo_company_sessionid'
settings.CSRF_COOKIE_NAME = 'vergabe_demo_company_csrftoken'
set_script_prefix(PREFIX)
yield settings
set_script_prefix(previous)
class LocalLinks(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
for name, value in attrs:
if name in {'href', 'src', 'action', 'hx-get', 'hx-post', 'hx-delete'} and value:
if value.startswith('/'):
self.links.append(value)
def assert_prefixed(content):
parser = LocalLinks()
parser.feed(content.decode())
assert parser.links
assert all(link.startswith(PREFIX + '/') for link in parser.links), parser.links
def test_anonymous_and_expired_fragments_stay_in_tenant_path(client, tenant_path):
response = client.get('/ausschreibungen/', HTTP_X_FORWARDED_PREFIX='/other-company')
assert response.status_code == 302
assert urlsplit(response.url).path == PREFIX + '/accounts/login/'
assert parse_qs(urlsplit(response.url).query)['next'] == [PREFIX + '/ausschreibungen/']
response = client.get('/suche/?q=private', HTTP_HX_REQUEST='true')
assert response.status_code == 401
assert urlsplit(response['HX-Redirect']).path == PREFIX + '/accounts/login/'
assert parse_qs(urlsplit(response['HX-Redirect']).query)['next'] == [PREFIX + '/']
assert client.get('/health/').json() == {'status': 'ok'}
def test_login_cookie_csrf_logout_and_external_redirect(client, mitarbeiter, tenant_path):
client = Client(enforce_csrf_checks=True)
login = client.get('/accounts/login/')
assert_prefixed(login.content)
csrf = tenant_path.CSRF_COOKIE_NAME
assert client.cookies[csrf]['path'] == PREFIX + '/'
credentials = {'username': mitarbeiter.username, 'password': 'testpass'}
assert client.post('/accounts/login/', credentials).status_code == 403
credentials['csrfmiddlewaretoken'] = client.cookies[csrf].value
credentials['next'] = 'https://untrusted.example/private'
response = client.post('/accounts/login/', credentials)
assert response.status_code == 302
assert response.url == PREFIX + '/'
assert client.cookies[tenant_path.SESSION_COOKIE_NAME]['path'] == PREFIX + '/'
assert client.get('/accounts/logout/').status_code == 405
assert client.post('/accounts/logout/').status_code == 403
response = client.post('/accounts/logout/', {'csrfmiddlewaretoken': client.cookies[csrf].value})
assert response.url == PREFIX + '/accounts/login/'
assert client.cookies[tenant_path.SESSION_COOKIE_NAME]['path'] == PREFIX + '/'
assert client.get('/ausschreibungen/').status_code == 302
def test_member_navigation_fragments_and_password_change(client, mitarbeiter, tenant_path):
tender = Ausschreibung.objects.create(titel='Path test', ausschreiber='Demo')
client.force_login(mitarbeiter)
for path in [
'/ausschreibungen/', f'/ausschreibungen/{tender.pk}/',
'/aufgaben/', '/partner/subunternehmer/', '/bibliothek/nachweise/',
'/accounts/password-change/',
]:
response = client.get(path)
assert response.status_code == 200, path
assert_prefixed(response.content)
fragment = client.get('/suche/?q=Path', HTTP_HX_REQUEST='true')
assert fragment.status_code == 200
assert_prefixed(fragment.content)
response = client.post('/accounts/password-change/', {
'old_password': 'testpass',
'new_password1': 'Path-pilot-password-984!',
'new_password2': 'Path-pilot-password-984!',
})
assert response.status_code == 302
assert response.url.startswith(PREFIX + '/')
assert client.get('/ausschreibungen/').status_code == 200
def test_private_media_remains_authenticated_under_prefix(
client, mitarbeiter, tenant_path, tmp_path,
):
tenant_path.MEDIA_ROOT = tmp_path
(tmp_path / 'document.pdf').write_bytes(b'%PDF-private-demo')
anonymous = client.get('/media/document.pdf')
assert anonymous.status_code == 302
assert urlsplit(anonymous.url).path == PREFIX + '/accounts/login/'
client.force_login(mitarbeiter)
response = client.get('/media/document.pdf')
assert response.status_code == 200
assert b''.join(response.streaming_content) == b'%PDF-private-demo'
assert response['Content-Disposition'] == 'attachment; filename="document.pdf"'
assert 'no-store' in response['Cache-Control']