import pytest from django.conf import settings from django.core.cache import cache from django.test import override_settings from django.urls import reverse from django.utils import timezone from apps.jobs.models import JobPosting, ScoreRun @pytest.mark.integration @pytest.mark.django_db def test_today_requires_login(client): response = client.get(reverse("dashboard:today")) assert response.status_code == 302 assert reverse("login") in response.url @pytest.mark.integration @pytest.mark.django_db def test_browser_favicon_and_brand_assets_are_local_pngs(client): response = client.get("/favicon.ico") assert response.status_code == 301 assert response.url.endswith("/static/img/itworx-app-icon.png") favicon = settings.BASE_DIR / "static" / "img" / "itworx-app-icon.png" wordmark = settings.BASE_DIR / "static" / "img" / "itworx-wordmark.png" assert favicon.read_bytes().startswith(b"\x89PNG\r\n\x1a\n") assert wordmark.read_bytes().startswith(b"\x89PNG\r\n\x1a\n") assert favicon.stat().st_size < 150_000 assert wordmark.stat().st_size < 100_000 @pytest.mark.integration @pytest.mark.django_db def test_today_loads_for_user(client, user, profile): client.force_login(user) response = client.get(reverse("dashboard:today")) assert response.status_code == 200 assert "Vandaag" in response.content.decode() body = response.content.decode() assert 'alt="ITWorx.tech"' in body assert "VacatureRadar · intelligence cockpit" in body assert "ITWorx.tech" in body assert "VacatureRadar · Dagoverzicht" in body assert "Dagoverzicht" in body assert 'content="VacatureRadar · ITWorx.tech"' in body def test_premium_layout_uses_one_left_aligned_ultrawide_grid(): css = (settings.BASE_DIR / "static" / "css" / "premium.css").read_text(encoding="utf-8") assert "max-width: 2800px; margin-right: auto; margin-left: 0" in css assert "@media (min-width: 1800px)" in css assert "minmax(650px, 880px) minmax(760px, 1fr)" in css @pytest.mark.integration @pytest.mark.django_db def test_job_list_defaults_to_ranked_it_focus_but_keeps_explicit_archive( client, user, profile, job ): non_it = JobPosting.objects.create( employer=job.employer, original_title="Spontane sollicitatie - logopedist", normalized_title="spontane sollicitatie logopedist", canonical_url="https://jobs.example.org/vacatures/logopedist", canonical_key="9" * 64, content_hash="8" * 64, description_text="Zorgfunctie met toegang tot digitale patiëntendossiers.", status=JobPosting.Status.ACTIVE, ) misleading_it_title = JobPosting.objects.create( employer=job.employer, original_title="IT Recruitment Specialist", normalized_title="it recruitment specialist", canonical_url="https://jobs.example.org/vacatures/it-recruitment", canonical_key="3" * 64, content_hash="2" * 64, status=JobPosting.Status.ACTIVE, ) ScoreRun.objects.create( job=job, profile=profile, profile_version=profile.version, score=60, confidence=0.9, recommendation=ScoreRun.Recommendation.WEAK, positives=["IT-focus bevestigd via de functietitel."], ) ScoreRun.objects.create( job=non_it, profile=profile, profile_version=profile.version, score=99, confidence=0.9, recommendation=ScoreRun.Recommendation.POSSIBLE, ) hidden_it = JobPosting.objects.create( employer=job.employer, original_title="Cloud Engineer buiten de zoekradius", normalized_title="cloud engineer buiten de zoekradius", canonical_url="https://jobs.example.org/vacatures/cloud-ver", canonical_key="5" * 64, content_hash="4" * 64, status=JobPosting.Status.ACTIVE, ) ScoreRun.objects.create( job=hidden_it, profile=profile, profile_version=profile.version, score=98, confidence=0.9, recommendation=ScoreRun.Recommendation.HIDDEN, hard_exclusions=["Buiten de zoekradius."], ) client.force_login(user) focused = client.get(reverse("jobs:list")) focused_body = focused.content.decode() assert job.original_title in focused_body assert non_it.original_title not in focused_body assert misleading_it_title.original_title not in focused_body assert hidden_it.original_title not in focused_body assert focused.context["it_job_count"] == 2 assert focused.context["filtered_non_it_count"] == 2 archive = client.get(reverse("jobs:list"), {"focus": "all", "match": "all"}) archive_body = archive.content.decode() assert non_it.original_title in archive_body assert misleading_it_title.original_title in archive_body assert hidden_it.original_title in archive_body @pytest.mark.integration @pytest.mark.django_db def test_today_uses_latest_user_score_and_excludes_non_it(client, user, profile, job): non_it = JobPosting.objects.create( employer=job.employer, original_title="Technieker Facilitaire Diensten", normalized_title="technieker facilitaire diensten", canonical_url="https://jobs.example.org/vacatures/facilitair", canonical_key="7" * 64, content_hash="6" * 64, status=JobPosting.Status.ACTIVE, first_seen=timezone.now(), ) ScoreRun.objects.create( job=job, profile=profile, profile_version=profile.version, score=70, confidence=0.9, recommendation=ScoreRun.Recommendation.POSSIBLE, ) ScoreRun.objects.create( job=job, profile=profile, profile_version=profile.version, score=66, confidence=0.9, recommendation=ScoreRun.Recommendation.POSSIBLE, ) ScoreRun.objects.create( job=non_it, profile=profile, profile_version=profile.version, score=95, confidence=0.9, recommendation=ScoreRun.Recommendation.STRONG, ) client.force_login(user) response = client.get(reverse("dashboard:today")) cards = list(response.context["score_cards"]) assert [card.job_id for card in cards] == [job.id] assert cards[0].score == 66 assert response.context["filtered_non_it_jobs"] == 1 @pytest.mark.integration @pytest.mark.django_db @override_settings( AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS=2, AUTH_LOGIN_RATE_LIMIT_WINDOW_SECONDS=120, AUTH_LOGIN_RATE_LIMIT_BLOCK_SECONDS=300, ) def test_login_view_rate_limits_failed_attempts(client, user): cache.clear() response = client.post( reverse("login"), {"username": user.username, "password": "wrong"}, ) assert response.status_code == 200 response = client.post( reverse("login"), {"username": user.username, "password": "wrong"}, ) body = response.content.decode("utf-8") assert "Te veel inlogpogingen" in body response = client.post( reverse("login"), {"username": user.username, "password": "wrong"}, ) assert "Te veel inlogpogingen" in response.content.decode("utf-8")