import factory import pytest from django.urls import reverse from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory from vergabe_teilnahme.apps.lose.tests import AnforderungFactory from .models import Aufgabe, AufgabenVerknuepfung, Bieterfrage, ExternalIssue class AufgabeFactory(factory.django.DjangoModelFactory): class Meta: model = Aufgabe ausschreibung = factory.SubFactory(AusschreibungFactory) titel = factory.Sequence(lambda n: f"Aufgabe {n}") typ = 'fachlich' status = 'offen' prioritaet = 2 class BieterfragenFactory(factory.django.DjangoModelFactory): class Meta: model = Bieterfrage ausschreibung = factory.SubFactory(AusschreibungFactory) fragentext = factory.Sequence(lambda n: f"Frage {n}: Bitte klären Sie...") status = 'entwurf' prioritaet = 2 # ─── Aufgaben ────────────────────────────────────────────────────────────── @pytest.mark.django_db def test_aufgaben_liste_get(client): a = AusschreibungFactory() url = reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': a.pk}) response = client.get(url) assert response.status_code == 200 @pytest.mark.django_db def test_aufgabe_neu_post(client): a = AusschreibungFactory() url = reverse('ausschreibungen:aufgaben:neu', kwargs={'ausschreibung_id': a.pk}) response = 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): 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') assert response.status_code == 200 aufgabe.refresh_from_db() assert aufgabe.status == 'erledigt' @pytest.mark.django_db def test_ueberfaellige_aufgabe_auto_update(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) aufgabe.refresh_from_db() assert aufgabe.status == 'ueberfaellig' # ─── Bieterfragen ───────────────────────────────────────────────────────── @pytest.mark.django_db def test_bieterfrage_neu_prefill_anforderung(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}) assert response.status_code == 200 assert str(anf.pk).encode() in response.content @pytest.mark.django_db def test_bieterfrage_antwort_speichern(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': ''}) assert response.status_code == 302 bf.refresh_from_db() assert bf.antwort == 'Die Antwort lautet 42.' assert bf.status == 'beantwortet' # ─── Implizite Fälligkeit ───────────────────────────────────────────────────── @pytest.mark.django_db def test_frist_effektiv_mit_frist(): from datetime import date aufgabe = AufgabeFactory(frist=date(2026, 6, 1)) assert aufgabe.frist_effektiv == date(2026, 6, 1) @pytest.mark.django_db def test_frist_effektiv_ohne_frist(): from datetime import timedelta from django.utils import timezone aufgabe = AufgabeFactory(frist=None) expected = (aufgabe.erstellt_am + timedelta(days=7)).date() assert aufgabe.frist_effektiv == expected @pytest.mark.django_db def test_ueberfaellig_ohne_frist_nach_7_tagen(client): from datetime import timedelta from django.utils import timezone a = AusschreibungFactory() alte_erstellung = timezone.now() - timedelta(days=8) 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) aufgabe.refresh_from_db() assert aufgabe.status == 'ueberfaellig' # ─── Aufgaben-Verknüpfungen ─────────────────────────────────────────────────── @pytest.mark.django_db def test_aufgaben_verknuepfung_erstellen(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, { 'ziel_typ': ct.pk, 'ziel_id': anf.pk, 'kommentar': 'Testverknüpfung', }, HTTP_HX_REQUEST='true') assert response.status_code == 200 assert AufgabenVerknuepfung.objects.filter(aufgabe=aufgabe, object_id=anf.pk).exists() @pytest.mark.django_db def test_aufgaben_verknuepfung_loeschen(client): from django.contrib.contenttypes.models import ContentType aufgabe = AufgabeFactory() anf = AnforderungFactory(ausschreibung=aufgabe.ausschreibung) ct = ContentType.objects.get_for_model(anf) vk = AufgabenVerknuepfung.objects.create( aufgabe=aufgabe, content_type=ct, object_id=anf.pk ) 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') assert response.status_code == 200 assert not AufgabenVerknuepfung.objects.filter(pk=vk.pk).exists() # ─── ExternalIssue ──────────────────────────────────────────────────────────── @pytest.fixture def tmp_issue_db(tmp_path, settings): settings.ISSUE_FACADE_LOCAL_DB = tmp_path / 'test_issues.db' settings.ISSUE_FACADE_GITEA = None return settings.ISSUE_FACADE_LOCAL_DB @pytest.mark.django_db def test_external_issue_erstellen(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') 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): 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') assert response.status_code == 200 assert not ExternalIssue.objects.filter(pk=ei.pk).exists() @pytest.mark.django_db def test_issue_adapter_interface(): from .issue_facade import get_adapter assert get_adapter('github') is None assert get_adapter('nichtexistent') is None # ─── Issue-Facade Integration ───────────────────────────────────────────────── @pytest.mark.django_db def test_aufgabe_zu_issue_mapping(tmp_issue_db): from .issue_facade import aufgabe_zu_issue aufgabe = AufgabeFactory(titel='Test-Aufgabe', beschreibung='Beschreibung', prioritaet=1) issue = aufgabe_zu_issue(aufgabe) assert issue.title == 'Test-Aufgabe' assert issue.description == 'Beschreibung' label_names = [l.name for l in issue.labels] assert 'task' in label_names assert 'priority:high' in label_names @pytest.mark.django_db def test_lokales_issue_erstellen(tmp_issue_db): from .issue_facade import lokales_issue_erstellen aufgabe = AufgabeFactory() daten = lokales_issue_erstellen(aufgabe) assert daten['issue_facade_id'] assert daten['issue_key'].startswith('#') assert daten['sync_status'] == 'open' @pytest.mark.django_db def test_status_synchronisieren(tmp_issue_db): from .issue_facade import lokales_issue_erstellen, status_synchronisieren aufgabe = AufgabeFactory() daten = lokales_issue_erstellen(aufgabe) ei = ExternalIssue.objects.create( aufgabe=aufgabe, issue_facade_backend='local', issue_facade_id=daten['issue_facade_id'], issue_key=daten['issue_key'], sync_status=daten['sync_status'], ) neuer_status = status_synchronisieren(ei) assert neuer_status == 'open' ei.refresh_from_db() assert ei.sync_status == 'open' assert ei.letzter_sync is not None @pytest.mark.django_db def test_external_issue_bearbeiten_view_erstellt_issue(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') assert response.status_code == 200 ei = ExternalIssue.objects.get(aufgabe=aufgabe) assert ei.issue_facade_backend == 'local' assert ei.issue_facade_id assert ei.notizen == 'Test-Notiz' @pytest.mark.django_db def test_external_issue_sync_view(client, tmp_issue_db): from .issue_facade import lokales_issue_erstellen aufgabe = AufgabeFactory() daten = lokales_issue_erstellen(aufgabe) ei = ExternalIssue.objects.create( aufgabe=aufgabe, issue_facade_backend='local', issue_facade_id=daten['issue_facade_id'], issue_key=daten['issue_key'], sync_status=daten['sync_status'], ) url = reverse('ausschreibungen:aufgaben:external_issue_sync', kwargs={'ausschreibung_id': aufgabe.ausschreibung_id, 'pk': aufgabe.pk}) response = client.post(url, {}, HTTP_HX_REQUEST='true') assert response.status_code == 200 ei.refresh_from_db() assert ei.sync_status == 'open' @pytest.mark.django_db def test_get_adapter_stub(): from .issue_facade import get_adapter assert get_adapter('github') is None assert get_adapter('gitea') is None