Require invited login and private downloads for the company pilot

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-11 16:35:13 +02:00
parent 56bf193193
commit b7d7828f30
27 changed files with 584 additions and 111 deletions

View file

@ -0,0 +1,16 @@
from django.contrib.auth import forms
class _AccountFieldStyle:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-input'
class AuthenticationForm(_AccountFieldStyle, forms.AuthenticationForm):
pass
class PasswordChangeForm(_AccountFieldStyle, forms.PasswordChangeForm):
pass

View file

@ -0,0 +1,21 @@
from django.contrib.auth.middleware import LoginRequiredMiddleware
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponse
from django.utils.cache import add_never_cache_headers
class PilotLoginRequiredMiddleware(LoginRequiredMiddleware):
"""Use Django's default-deny gate, including full-page HTMX reauthentication."""
def handle_no_permission(self, request, view_func):
if request.headers.get('HX-Request') == 'true':
# The original URL may render only a fragment or accept only POST.
login = redirect_to_login('/', self.get_login_url(view_func))
return HttpResponse(status=401, headers={'HX-Redirect': login.url})
return super().handle_no_permission(request, view_func)
def process_response(self, request, response):
# Company records and account pages must not survive in shared caches.
if request.path != '/health/':
add_never_cache_headers(response)
return response

View file

@ -1,3 +1,138 @@
from django.test import TestCase
from urllib.parse import parse_qs, urlsplit
# Create your tests here.
import pytest
from django.test import Client
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
pytestmark = pytest.mark.django_db
def test_anonymous_cannot_read_tenders(client):
Ausschreibung.objects.create(titel='Confidential tender', ausschreiber='Pilot')
response = client.get('/ausschreibungen/')
assert response.status_code == 302
assert urlsplit(response.url).path == '/accounts/login/'
assert b'Confidential tender' not in response.content
def test_anonymous_cannot_create_tender(client):
response = client.post('/ausschreibungen/neu/', {
'titel': 'Unauthorised tender', 'ausschreiber': 'Pilot',
})
assert response.status_code == 302
assert not Ausschreibung.objects.exists()
def test_expired_htmx_session_requires_full_login(client):
response = client.get('/suche/?q=private', HTTP_HX_REQUEST='true')
assert response.status_code == 401
assert urlsplit(response['HX-Redirect']).path == '/accounts/login/'
# A fragment URL must never become the full page after login.
assert parse_qs(urlsplit(response['HX-Redirect']).query)['next'] == ['/']
def test_health_is_public_and_contains_no_tenant_data(client):
assert client.get('/health/').json() == {'status': 'ok'}
def test_invited_login_redirect_and_logout(mitarbeiter):
client = Client(enforce_csrf_checks=True)
response = client.get('/accounts/login/?next=/ausschreibungen/')
assert response.status_code == 200
assert b'Benutzername' in response.content
assert b'search-results' not in response.content
credentials = {'username': mitarbeiter.username, 'password': 'testpass',
'next': '/ausschreibungen/'}
assert client.post('/accounts/login/', credentials).status_code == 403
credentials['csrfmiddlewaretoken'] = client.cookies['csrftoken'].value
response = client.post('/accounts/login/', credentials)
assert response.status_code == 302
assert response.url == '/ausschreibungen/'
assert client.get(response.url).status_code == 200
assert client.get('/accounts/logout/').status_code == 405
assert client.post('/accounts/logout/').status_code == 403
response = client.post('/accounts/logout/', {
'csrfmiddlewaretoken': client.cookies['csrftoken'].value,
})
assert response.url == '/accounts/login/'
assert client.get('/ausschreibungen/').status_code == 302
def test_login_rejects_external_next(client, mitarbeiter):
response = client.post('/accounts/login/', {
'username': mitarbeiter.username, 'password': 'testpass',
'next': 'https://untrusted.example/private',
})
assert response.status_code == 302
assert response.url == '/'
def test_deactivated_member_loses_existing_session_and_cannot_login(client, mitarbeiter):
client.force_login(mitarbeiter)
mitarbeiter.is_active = False
mitarbeiter.save(update_fields=['is_active'])
assert client.get('/ausschreibungen/').status_code == 302
response = client.post('/accounts/login/', {
'username': mitarbeiter.username, 'password': 'testpass',
})
assert response.status_code == 200
assert response.context['form'].errors
assert client.get('/ausschreibungen/').status_code == 302
def test_member_password_change_keeps_session_and_replaces_password(client, mitarbeiter):
client.force_login(mitarbeiter)
assert client.get('/accounts/password-change/').status_code == 200
response = client.post('/accounts/password-change/', {
'old_password': 'testpass',
'new_password1': 'A-unique-pilot-password-914!',
'new_password2': 'A-unique-pilot-password-914!',
})
assert response.status_code == 302
assert client.get(response.url).status_code == 200
assert client.get('/ausschreibungen/').status_code == 200
mitarbeiter.refresh_from_db()
assert not mitarbeiter.check_password('testpass')
assert mitarbeiter.check_password('A-unique-pilot-password-914!')
def test_company_member_does_not_gain_django_admin_access(client, mitarbeiter):
client.force_login(mitarbeiter)
response = client.get('/admin/')
assert response.status_code == 302
assert urlsplit(response.url).path == '/admin/login/'
@pytest.mark.parametrize('debug', [False, True])
def test_media_requires_login_and_downloads_as_private_attachment(
client, mitarbeiter, tmp_path, settings, debug,
):
settings.DEBUG = debug
settings.MEDIA_ROOT = tmp_path / 'media'
settings.MEDIA_ROOT.mkdir()
(settings.MEDIA_ROOT / 'tender.pdf').write_bytes(b'%PDF-pilot')
assert client.get('/media/tender.pdf').status_code == 302
client.force_login(mitarbeiter)
response = client.get('/media/tender.pdf')
assert response.status_code == 200
assert b''.join(response.streaming_content) == b'%PDF-pilot'
assert response['Content-Disposition'] == 'attachment; filename="tender.pdf"'
assert response['X-Content-Type-Options'] == 'nosniff'
assert 'no-store' in response['Cache-Control']
def test_media_rejects_traversal_symlink_directory_and_missing_file(
client, mitarbeiter, tmp_path, settings,
):
settings.MEDIA_ROOT = tmp_path / 'media'
settings.MEDIA_ROOT.mkdir()
private = tmp_path / 'issues.db'
private.write_bytes(b'private issue state')
(settings.MEDIA_ROOT / 'escape.pdf').symlink_to(private)
(settings.MEDIA_ROOT / 'folder').mkdir()
client.force_login(mitarbeiter)
for path in ('../issues.db', '%2e%2e/issues.db', 'escape.pdf', 'folder', 'absent.pdf'):
response = client.get('/media/' + path)
assert response.status_code == 404
assert b'private issue state' not in response.content

View file

@ -0,0 +1,21 @@
from django.contrib.auth import views
from django.urls import path, reverse_lazy
from .forms import AuthenticationForm, PasswordChangeForm
app_name = 'accounts'
urlpatterns = [
path('login/', views.LoginView.as_view(
template_name='accounts/login.html', authentication_form=AuthenticationForm,
), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('password-change/', views.PasswordChangeView.as_view(
template_name='accounts/password_change.html',
form_class=PasswordChangeForm,
success_url=reverse_lazy('accounts:password_change_done'),
), name='password_change'),
path('password-change/done/', views.PasswordChangeDoneView.as_view(
template_name='accounts/password_change_done.html',
), name='password_change_done'),
]

View file

@ -33,40 +33,40 @@ class BieterfragenFactory(factory.django.DjangoModelFactory):
@pytest.mark.django_db
def test_aufgaben_liste_get(client):
def test_aufgaben_liste_get(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_aufgabe_neu_post(client):
def test_aufgabe_neu_post(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:aufgaben:neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {'titel': 'Neue Aufgabe', 'typ': 'fachlich', 'prioritaet': 2})
response = admitted_client.post(url, {'titel': 'Neue Aufgabe', 'typ': 'fachlich', 'prioritaet': 2})
assert response.status_code == 302
assert Aufgabe.objects.filter(ausschreibung=a, titel='Neue Aufgabe').exists()
@pytest.mark.django_db
def test_aufgabe_status_htmx(client):
def test_aufgabe_status_htmx(admitted_client):
aufgabe = AufgabeFactory()
url = reverse('ausschreibungen:aufgaben:status',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {'status': 'erledigt'}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {'status': 'erledigt'}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
aufgabe.refresh_from_db()
assert aufgabe.status == 'erledigt'
@pytest.mark.django_db
def test_ueberfaellige_aufgabe_auto_update(client):
def test_ueberfaellige_aufgabe_auto_update(admitted_client):
from datetime import date, timedelta
a = AusschreibungFactory()
aufgabe = AufgabeFactory(ausschreibung=a, frist=date.today() - timedelta(days=1), status='offen')
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
client.get(url)
admitted_client.get(url)
aufgabe.refresh_from_db()
assert aufgabe.status == 'ueberfaellig'
@ -75,21 +75,21 @@ def test_ueberfaellige_aufgabe_auto_update(client):
@pytest.mark.django_db
def test_bieterfrage_neu_prefill_anforderung(client):
def test_bieterfrage_neu_prefill_anforderung(admitted_client):
a = AusschreibungFactory()
anf = AnforderungFactory(ausschreibung=a)
url = reverse('ausschreibungen:bieterfragen:neu', kwargs={'ausschreibung_id': a.pk})
response = client.get(url, {'anforderung_id': anf.pk})
response = admitted_client.get(url, {'anforderung_id': anf.pk})
assert response.status_code == 200
assert str(anf.pk).encode() in response.content
@pytest.mark.django_db
def test_bieterfrage_antwort_speichern(client):
def test_bieterfrage_antwort_speichern(admitted_client):
bf = BieterfragenFactory(status='eingereicht')
url = reverse('ausschreibungen:bieterfragen:antwort',
kwargs={'ausschreibung_id': bf.ausschreibung_id, 'pk': bf.pk})
response = client.post(url, {'antwort': 'Die Antwort lautet 42.', 'auswirkung_angebot': ''})
response = admitted_client.post(url, {'antwort': 'Die Antwort lautet 42.', 'auswirkung_angebot': ''})
assert response.status_code == 302
bf.refresh_from_db()
assert bf.antwort == 'Die Antwort lautet 42.'
@ -116,7 +116,7 @@ def test_frist_effektiv_ohne_frist():
@pytest.mark.django_db
def test_ueberfaellig_ohne_frist_nach_7_tagen(client):
def test_ueberfaellig_ohne_frist_nach_7_tagen(admitted_client):
from datetime import timedelta
from django.utils import timezone
a = AusschreibungFactory()
@ -124,7 +124,7 @@ def test_ueberfaellig_ohne_frist_nach_7_tagen(client):
aufgabe = AufgabeFactory(ausschreibung=a, frist=None, status='offen')
Aufgabe.objects.filter(pk=aufgabe.pk).update(erstellt_am=alte_erstellung)
url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk})
client.get(url)
admitted_client.get(url)
aufgabe.refresh_from_db()
assert aufgabe.status == 'ueberfaellig'
@ -133,14 +133,14 @@ def test_ueberfaellig_ohne_frist_nach_7_tagen(client):
@pytest.mark.django_db
def test_aufgaben_verknuepfung_erstellen(client):
def test_aufgaben_verknuepfung_erstellen(admitted_client):
from django.contrib.contenttypes.models import ContentType
aufgabe = AufgabeFactory()
anf = AnforderungFactory(ausschreibung=aufgabe.ausschreibung)
ct = ContentType.objects.get_for_model(anf)
url = reverse('ausschreibungen:aufgaben:verknuepfung_neu',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {
response = admitted_client.post(url, {
'ziel_typ': ct.pk,
'ziel_id': anf.pk,
'kommentar': 'Testverknüpfung',
@ -150,7 +150,7 @@ def test_aufgaben_verknuepfung_erstellen(client):
@pytest.mark.django_db
def test_aufgaben_verknuepfung_loeschen(client):
def test_aufgaben_verknuepfung_loeschen(admitted_client):
from django.contrib.contenttypes.models import ContentType
aufgabe = AufgabeFactory()
anf = AnforderungFactory(ausschreibung=aufgabe.ausschreibung)
@ -160,7 +160,7 @@ def test_aufgaben_verknuepfung_loeschen(client):
)
url = reverse('ausschreibungen:aufgaben:verknuepfung_loeschen',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk, 'vk_pk': vk.pk})
response = client.post(url, {}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert not AufgabenVerknuepfung.objects.filter(pk=vk.pk).exists()
@ -176,24 +176,24 @@ def tmp_issue_db(tmp_path, settings):
@pytest.mark.django_db
def test_external_issue_erstellen(client, tmp_issue_db):
def test_external_issue_erstellen(admitted_client, tmp_issue_db):
aufgabe = AufgabeFactory()
url = reverse('ausschreibungen:aufgaben:external_issue',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {'notizen': ''}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {'notizen': ''}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert ExternalIssue.objects.filter(aufgabe=aufgabe, issue_facade_backend='local').exists()
@pytest.mark.django_db
def test_external_issue_loeschen(client):
def test_external_issue_loeschen(admitted_client):
aufgabe = AufgabeFactory()
ei = ExternalIssue.objects.create(
aufgabe=aufgabe, issue_facade_backend='local', issue_key='#1'
)
url = reverse('ausschreibungen:aufgaben:external_issue_loeschen',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert not ExternalIssue.objects.filter(pk=ei.pk).exists()
@ -250,11 +250,11 @@ def test_status_synchronisieren(tmp_issue_db):
@pytest.mark.django_db
def test_external_issue_bearbeiten_view_erstellt_issue(client, tmp_issue_db):
def test_external_issue_bearbeiten_view_erstellt_issue(admitted_client, tmp_issue_db):
aufgabe = AufgabeFactory()
url = reverse('ausschreibungen:aufgaben:external_issue',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {'notizen': 'Test-Notiz'}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {'notizen': 'Test-Notiz'}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
ei = ExternalIssue.objects.get(aufgabe=aufgabe)
assert ei.issue_facade_backend == 'local'
@ -263,7 +263,7 @@ def test_external_issue_bearbeiten_view_erstellt_issue(client, tmp_issue_db):
@pytest.mark.django_db
def test_external_issue_sync_view(client, tmp_issue_db):
def test_external_issue_sync_view(admitted_client, tmp_issue_db):
from .issue_facade import lokales_issue_erstellen
aufgabe = AufgabeFactory()
daten = lokales_issue_erstellen(aufgabe)
@ -276,7 +276,7 @@ def test_external_issue_sync_view(client, tmp_issue_db):
)
url = reverse('ausschreibungen:aufgaben:external_issue_sync',
kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk})
response = client.post(url, {}, HTTP_HX_REQUEST='true')
response = admitted_client.post(url, {}, HTTP_HX_REQUEST='true')
assert response.status_code == 200
ei.refresh_from_db()
assert ei.sync_status == 'open'

View file

@ -54,47 +54,47 @@ def test_naechste_frist_none_when_past():
# --- View tests ---
@pytest.mark.django_db
def test_liste_get(client):
response = client.get(reverse("ausschreibungen:liste"))
def test_liste_get(admitted_client):
response = admitted_client.get(reverse("ausschreibungen:liste"))
assert response.status_code == 200
@pytest.mark.django_db
def test_neu_get(client):
response = client.get(reverse("ausschreibungen:neu"))
def test_neu_get(admitted_client):
response = admitted_client.get(reverse("ausschreibungen:neu"))
assert response.status_code == 200
@pytest.mark.django_db
def test_neu_post_valid(client):
def test_neu_post_valid(admitted_client):
data = {"titel": "Neue Ausschreibung", "ausschreiber": "Stadt XY"}
response = client.post(reverse("ausschreibungen:neu"), data)
response = admitted_client.post(reverse("ausschreibungen:neu"), data)
assert response.status_code == 302
a = Ausschreibung.objects.get(titel="Neue Ausschreibung")
assert response.url == reverse("ausschreibungen:detail", kwargs={"pk": a.pk})
@pytest.mark.django_db
def test_detail_get(client):
def test_detail_get(admitted_client):
a = AusschreibungFactory()
response = client.get(reverse("ausschreibungen:detail", kwargs={"pk": a.pk}))
response = admitted_client.get(reverse("ausschreibungen:detail", kwargs={"pk": a.pk}))
assert response.status_code == 200
@pytest.mark.django_db
def test_status_post(client):
def test_status_post(admitted_client):
a = AusschreibungFactory(status=1)
url = reverse("ausschreibungen:status", kwargs={"pk": a.pk})
response = client.post(url, {"status": "4"})
response = admitted_client.post(url, {"status": "4"})
assert response.status_code == 200
a.refresh_from_db()
assert a.status == 4
@pytest.mark.django_db
def test_status_htmx_returns_partial(client):
def test_status_htmx_returns_partial(admitted_client):
a = AusschreibungFactory(status=1)
url = reverse("ausschreibungen:status", kwargs={"pk": a.pk})
response = client.post(url, {"status": "3"}, HTTP_HX_REQUEST="true")
response = admitted_client.post(url, {"status": "3"}, HTTP_HX_REQUEST="true")
assert response.status_code == 200
assert b"status-widget" in response.content

View file

@ -19,12 +19,12 @@ def test_nachweis_ist_abgelaufen_false_without_date():
@pytest.mark.django_db
def test_nachweis_liste_filter_abgelaufen(client):
def test_nachweis_liste_filter_abgelaufen(admitted_client):
heute = date.today()
Nachweis.objects.create(titel='AbgelaufenerNachweis', gueltig_bis=heute - timedelta(days=5))
Nachweis.objects.create(titel='NochAktuellerNachweis', gueltig_bis=heute + timedelta(days=100))
url = reverse('bibliothek:nachweise_liste')
response = client.get(url + '?tab=abgelaufen')
response = admitted_client.get(url + '?tab=abgelaufen')
assert response.status_code == 200
content = response.content.decode()
assert 'AbgelaufenerNachweis' in content
@ -32,7 +32,7 @@ def test_nachweis_liste_filter_abgelaufen(client):
@pytest.mark.django_db
def test_entscheidungsregel_inaktiv_nicht_in_liste(client):
def test_entscheidungsregel_inaktiv_nicht_in_liste(admitted_client):
Entscheidungsregel.objects.create(
regelname='Aktive Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
)
@ -40,7 +40,7 @@ def test_entscheidungsregel_inaktiv_nicht_in_liste(client):
regelname='Inaktive Regel', kategorie='frist', empfehlung='pruefen', aktiv=False
)
url = reverse('bibliothek:entscheidungsregeln_liste')
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
content = response.content.decode()
assert 'Aktive Regel' in content
@ -48,21 +48,21 @@ def test_entscheidungsregel_inaktiv_nicht_in_liste(client):
@pytest.mark.django_db
def test_entscheidungsregel_toggle(client):
def test_entscheidungsregel_toggle(admitted_client):
r = Entscheidungsregel.objects.create(
regelname='Toggle-Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
)
url = reverse('bibliothek:er_toggle', kwargs={'pk': r.pk})
client.post(url)
admitted_client.post(url)
r.refresh_from_db()
assert r.aktiv is False
@pytest.mark.django_db
def test_nachweis_neue_version_ersetzt_alten(client):
def test_nachweis_neue_version_ersetzt_alten(admitted_client):
alt = Nachweis.objects.create(titel='Zertifikat ISO', version='1.0')
url = reverse('bibliothek:nachweis_version', kwargs={'pk': alt.pk})
client.post(url, {
admitted_client.post(url, {
'titel': 'Zertifikat ISO',
'version': '1.0',
'sprache': 'de',

View file

@ -0,0 +1,23 @@
from pathlib import Path
from django.conf import settings
from django.http import FileResponse, Http404
from django.views.decorators.http import require_safe
@require_safe
def protected_media(request, path):
"""Download company uploads through the application authentication gate.
The invited pilot has one company per deployment. MEDIA_ROOT must contain
uploads only; operational databases and credentials live outside this root.
"""
root = Path(settings.MEDIA_ROOT).resolve()
try:
target = (root / path).resolve()
if not target.is_relative_to(root) or not target.is_file():
raise Http404
file = target.open('rb')
except (OSError, RuntimeError, ValueError) as exc:
raise Http404 from exc
return FileResponse(file, as_attachment=True, filename=target.name)

View file

@ -26,46 +26,46 @@ def _pdf_file(name='test.pdf'):
@pytest.mark.django_db
def test_dokument_upload_valid(client, tmp_path, settings):
def test_dokument_upload_valid(admitted_client, tmp_path, settings):
settings.MEDIA_ROOT = tmp_path
a = AusschreibungFactory()
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {'datei': _pdf_file(), 'kategorie': 'intern', 'version': '1.0'})
response = admitted_client.post(url, {'datei': _pdf_file(), 'kategorie': 'intern', 'version': '1.0'})
assert response.status_code == 302
assert Dokument.objects.filter(ausschreibung=a).exists()
@pytest.mark.django_db
def test_dokument_upload_invalid_extension(client, tmp_path, settings):
def test_dokument_upload_invalid_extension(admitted_client, tmp_path, settings):
settings.MEDIA_ROOT = tmp_path
a = AusschreibungFactory()
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
bad_file = SimpleUploadedFile('malware.exe', b'MZ bad', content_type='application/octet-stream')
response = client.post(url, {'datei': bad_file, 'kategorie': 'intern', 'version': '1.0'})
response = admitted_client.post(url, {'datei': bad_file, 'kategorie': 'intern', 'version': '1.0'})
assert response.status_code == 200
assert not Dokument.objects.filter(ausschreibung=a).exists()
@pytest.mark.django_db
def test_dokument_upload_too_large(client, tmp_path, settings):
def test_dokument_upload_too_large(admitted_client, tmp_path, settings):
settings.MEDIA_ROOT = tmp_path
settings.MAX_UPLOAD_SIZE = 10
a = AusschreibungFactory()
url = reverse('ausschreibungen:dokumente:upload', kwargs={'ausschreibung_id': a.pk})
big_file = SimpleUploadedFile('big.pdf', b'%PDF' + b'x' * 100, content_type='application/pdf')
response = client.post(url, {'datei': big_file, 'kategorie': 'intern', 'version': '1.0'})
response = admitted_client.post(url, {'datei': big_file, 'kategorie': 'intern', 'version': '1.0'})
assert response.status_code == 200
assert not Dokument.objects.filter(ausschreibung=a).exists()
@pytest.mark.django_db
def test_dokument_neue_version(client, tmp_path, settings):
def test_dokument_neue_version(admitted_client, tmp_path, settings):
settings.MEDIA_ROOT = tmp_path
altes_dok = DokumentFactory(version='1.0')
a = altes_dok.ausschreibung
url = reverse('ausschreibungen:dokumente:neue_version',
kwargs={'ausschreibung_id': a.pk, 'pk': altes_dok.pk})
response = client.post(url, {'datei': _pdf_file('v2.pdf'), 'version': '2.0'})
response = admitted_client.post(url, {'datei': _pdf_file('v2.pdf'), 'version': '2.0'})
assert response.status_code == 302
altes_dok.refresh_from_db()
assert altes_dok.status == 'ersetzt'
@ -73,12 +73,12 @@ def test_dokument_neue_version(client, tmp_path, settings):
@pytest.mark.django_db
def test_dokument_finale_version(client):
def test_dokument_finale_version(admitted_client):
dok = DokumentFactory(status='freigegeben')
a = dok.ausschreibung
url = reverse('ausschreibungen:dokumente:finale_version',
kwargs={'ausschreibung_id': a.pk, 'pk': dok.pk})
response = client.post(url)
response = admitted_client.post(url)
assert response.status_code == 200
dok.refresh_from_db()
assert dok.finale_abgabeversion is True

View file

@ -39,28 +39,28 @@ class NachweisFactory(factory.django.DjangoModelFactory):
@pytest.mark.django_db
def test_lose_liste_get(client):
def test_lose_liste_get(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:lose:liste', kwargs={'ausschreibung_id': a.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_los_neu_post(client):
def test_los_neu_post(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:lose:neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {'losnummer': 'L01', 'lostitel': 'Testlos'})
response = admitted_client.post(url, {'losnummer': 'L01', 'lostitel': 'Testlos'})
assert response.status_code == 302
assert Los.objects.filter(ausschreibung=a, losnummer='L01').exists()
@pytest.mark.django_db
def test_los_detail_get(client):
def test_los_detail_get(admitted_client):
los = LosFactory()
url = reverse('ausschreibungen:lose:detail',
kwargs={'ausschreibung_id': los.ausschreibung_id, 'los_pk': los.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
@ -68,21 +68,21 @@ def test_los_detail_get(client):
@pytest.mark.django_db
def test_anforderung_neu_post_muss(client):
def test_anforderung_neu_post_muss(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:lose:anforderung_neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {'titel': 'Neue Anforderung', 'verbindlichkeit': 'muss',
response = admitted_client.post(url, {'titel': 'Neue Anforderung', 'verbindlichkeit': 'muss',
'erfuellungsstatus': 'offen'})
assert response.status_code == 302
assert Anforderung.objects.filter(ausschreibung=a, titel='Neue Anforderung').exists()
@pytest.mark.django_db
def test_anforderung_status_htmx(client):
def test_anforderung_status_htmx(admitted_client):
anf = AnforderungFactory()
url = reverse('ausschreibungen:lose:anforderung_status',
kwargs={'ausschreibung_id': anf.ausschreibung_id, 'pk': anf.pk})
response = client.post(url, {'erfuellungsstatus': 'nicht_erfuellbar'},
response = admitted_client.post(url, {'erfuellungsstatus': 'nicht_erfuellbar'},
HTTP_HX_REQUEST='true')
assert response.status_code == 200
anf.refresh_from_db()
@ -90,7 +90,7 @@ def test_anforderung_status_htmx(client):
@pytest.mark.django_db
def test_ausschlusskriterium_banner_shown(client):
def test_ausschlusskriterium_banner_shown(admitted_client):
a = AusschreibungFactory()
AnforderungFactory(
ausschreibung=a,
@ -98,17 +98,17 @@ def test_ausschlusskriterium_banner_shown(client):
erfuellungsstatus='nicht_erfuellbar',
)
url = reverse('ausschreibungen:entscheidung', kwargs={'pk': a.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
assert b'Nicht erf\xc3\xbcllbare Ausschlusskriterien' in response.content
@pytest.mark.django_db
def test_nachweis_zuordnen(client):
def test_nachweis_zuordnen(admitted_client):
anf = AnforderungFactory()
n = NachweisFactory()
url = reverse('ausschreibungen:lose:nachweis_zuordnen',
kwargs={'ausschreibung_id': anf.ausschreibung_id, 'pk': anf.pk})
response = client.post(url, {'nachweis_pk': n.pk})
response = admitted_client.post(url, {'nachweis_pk': n.pk})
assert response.status_code == 200
assert anf.nachweise.filter(pk=n.pk).exists()

View file

@ -12,11 +12,11 @@ def make_mb(name='TestBegleiter', **kwargs):
@pytest.mark.django_db
def test_passage_anlegen_mit_score_10(client):
def test_passage_anlegen_mit_score_10(admitted_client):
a = AusschreibungFactory()
mb = make_mb()
url = reverse('marktbegleiter:passagen:neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {
response = admitted_client.post(url, {
'passage': 'Musterpassage aus dem Dokument',
'marktbegleiter': mb.pk,
'verlaesslichkeitsscore': 10,
@ -40,7 +40,7 @@ def test_passage_score_zu_hoch_validierungsfehler():
@pytest.mark.django_db
def test_auswertung_score_durchschnitt(client):
def test_auswertung_score_durchschnitt(admitted_client):
a = AusschreibungFactory()
mb = make_mb()
Ausschreibungspassage.objects.create(
@ -50,14 +50,14 @@ def test_auswertung_score_durchschnitt(client):
ausschreibung=a, marktbegleiter=mb, passage='P2', verlaesslichkeitsscore=6
)
url = reverse('marktbegleiter:auswertung', kwargs={'pk': mb.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
content = response.content.decode()
assert '7' in content
@pytest.mark.django_db
def test_marktbegleiter_detail_zeigt_passagen(client):
def test_marktbegleiter_detail_zeigt_passagen(admitted_client):
a = AusschreibungFactory()
mb = make_mb(name='DetailBegleiter')
Ausschreibungspassage.objects.create(
@ -65,6 +65,6 @@ def test_marktbegleiter_detail_zeigt_passagen(client):
passage='Sichtbare Passage', verlaesslichkeitsscore=5
)
url = reverse('marktbegleiter:detail', kwargs={'pk': mb.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
assert b'Sichtbare Passage' in response.content

View file

@ -38,11 +38,11 @@ def test_abgabe_vollstaendigkeit_mit_freigabe():
@pytest.mark.django_db
def test_ergebnis_gewonnen_erstellt_kickoff_aufgabe(client):
def test_ergebnis_gewonnen_erstellt_kickoff_aufgabe(admitted_client):
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
a = AusschreibungFactory(status=9)
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {
response = admitted_client.post(url, {
'ergebnis': 'gewonnen',
'verlustgruende': '[]',
})
@ -53,21 +53,21 @@ def test_ergebnis_gewonnen_erstellt_kickoff_aufgabe(client):
@pytest.mark.django_db
def test_ergebnis_verloren_setzt_status_11(client):
def test_ergebnis_verloren_setzt_status_11(admitted_client):
a = AusschreibungFactory(status=9)
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
client.post(url, {'ergebnis': 'verloren', 'verlustgruende': '[]'})
admitted_client.post(url, {'ergebnis': 'verloren', 'verlustgruende': '[]'})
a.refresh_from_db()
assert a.status == 11
@pytest.mark.django_db
def test_verlustgruende_json_gespeichert(client):
def test_verlustgruende_json_gespeichert(admitted_client):
import json
a = AusschreibungFactory(status=9)
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
gruende = [{'grund': 'Zu teuer', 'kategorie': 'preis', 'verlaesslichkeit': 4}]
client.post(url, {'ergebnis': 'verloren', 'verlustgruende': json.dumps(gruende)})
admitted_client.post(url, {'ergebnis': 'verloren', 'verlustgruende': json.dumps(gruende)})
nb = Nachbetrachtung.objects.get(ausschreibung=a)
assert nb.verlustgruende[0]['grund'] == 'Zu teuer'
assert nb.verlustgruende[0]['kategorie'] == 'preis'

View file

@ -12,43 +12,43 @@ def make_sub(praeferenz='zugelassen', name='TestSub', **kwargs):
@pytest.mark.django_db
def test_subunternehmer_zuordnung_zu_los(client):
def test_subunternehmer_zuordnung_zu_los(admitted_client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
sub = make_sub()
url = reverse('partner:su_zuordnen', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
client.post(url, {'subunternehmer_id': sub.pk, 'konkrete_leistung': 'IT-Support'})
admitted_client.post(url, {'subunternehmer_id': sub.pk, 'konkrete_leistung': 'IT-Support'})
assert SubunternehmerZuordnung.objects.filter(subunternehmer=sub, ausschreibung=a, los=los).exists()
@pytest.mark.django_db
def test_gesperrter_subunternehmer_im_suchmodal(client):
def test_gesperrter_subunternehmer_im_suchmodal(admitted_client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
make_sub(praeferenz='gesperrt', name='GesperrterSub')
url = reverse('partner:su_suche_modal', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
response = client.get(url + '?q=GesperrterSub')
response = admitted_client.get(url + '?q=GesperrterSub')
assert response.status_code == 200
assert b'gesperrt' in response.content.lower()
@pytest.mark.django_db
def test_subunternehmer_praeferenz_update(client):
def test_subunternehmer_praeferenz_update(admitted_client):
sub = make_sub(praeferenz='zugelassen')
url = reverse('partner:su_praeferenz', kwargs={'pk': sub.pk})
client.post(url, {'praeferenz': 'bevorzugt'})
admitted_client.post(url, {'praeferenz': 'bevorzugt'})
sub.refresh_from_db()
assert sub.praeferenz == 'bevorzugt'
@pytest.mark.django_db
def test_zuordnung_toggle_zusage(client):
def test_zuordnung_toggle_zusage(admitted_client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
sub = make_sub()
z = SubunternehmerZuordnung.objects.create(subunternehmer=sub, ausschreibung=a, los=los)
assert z.zusage_vorhanden is False
url = reverse('partner:zuordnung_toggle', kwargs={'pk': z.pk})
client.post(url, {'feld': 'zusage_vorhanden'})
admitted_client.post(url, {'feld': 'zusage_vorhanden'})
z.refresh_from_db()
assert z.zusage_vorhanden is True

View file

@ -23,10 +23,10 @@ class PreispunktFactory(factory.django.DjangoModelFactory):
@pytest.mark.django_db
def test_vergleichsgewicht_null_gespeichert(client):
def test_vergleichsgewicht_null_gespeichert(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:preise:neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {
response = admitted_client.post(url, {
'leistungstyp': 'Test',
'konkrete_leistung': 'Leistung mit Gewicht 0',
'mengeneinheit': 'Stück',
@ -39,10 +39,10 @@ def test_vergleichsgewicht_null_gespeichert(client):
@pytest.mark.django_db
def test_vergleichsgewicht_zu_gross_validation_error(client):
def test_vergleichsgewicht_zu_gross_validation_error(admitted_client):
a = AusschreibungFactory()
url = reverse('ausschreibungen:preise:neu', kwargs={'ausschreibung_id': a.pk})
response = client.post(url, {
response = admitted_client.post(url, {
'leistungstyp': 'Test',
'konkrete_leistung': 'Leistung',
'mengeneinheit': 'Stück',
@ -71,10 +71,10 @@ def test_gewichteter_durchschnitt_berechnung():
@pytest.mark.django_db
def test_auswertung_view_200(client):
def test_auswertung_view_200(admitted_client):
a = AusschreibungFactory()
PreispunktFactory(ausschreibung=a, einzelpreis=Decimal('150.00'))
url = reverse('ausschreibungen:preise:auswertung', kwargs={'ausschreibung_id': a.pk})
response = client.get(url)
response = admitted_client.get(url)
assert response.status_code == 200
assert 'ergebnis' in response.context