249 lines
9.5 KiB
Python
249 lines
9.5 KiB
Python
"""Tests voor het herwerkte loginscherm en de bijbehorende authenticatie-flows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.core import mail
|
|
from django.test import override_settings
|
|
from django.urls import reverse
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.django_db]
|
|
|
|
|
|
def test_login_page_renders_stitch_design(client):
|
|
response = client.get(reverse("login"))
|
|
assert response.status_code == 200
|
|
body = response.content.decode("utf-8")
|
|
assert "<h1>Inloggen</h1>" in body
|
|
assert 'name="username"' in body and 'type="email"' in body
|
|
assert "Wachtwoord vergeten?" in body
|
|
# Local-first: geen externe CDN-assets op het loginscherm.
|
|
assert "cdn.tailwindcss.com" not in body
|
|
assert "https://fonts.googleapis.com" not in body
|
|
assert reverse("password_reset") in body
|
|
assert 'src="/static/js/login.js?' in body
|
|
assert "<script>" not in body
|
|
assert "Persoonlijke intelligence cockpit" in body
|
|
assert 'alt="VacatureRadar"' in body
|
|
assert "Ontwikkeld door" in body
|
|
assert 'aria-label="Ontwikkeld door ITWorx.tech"' in body
|
|
assert 'src="/static/img/itworx-wordmark.png" alt=""' in body
|
|
assert 'src="/static/img/itworx-wordmark.png"' in body
|
|
assert 'href="/static/favicon.svg?' in body
|
|
assert 'sizes="any"' in body
|
|
|
|
|
|
@override_settings(VACATURERADAR_OWNER_NAME="Eigen merk")
|
|
def test_login_page_keeps_official_brand_separate_from_owner_setting(client):
|
|
body = client.get(reverse("login")).content.decode("utf-8")
|
|
|
|
assert "Eigen merk" not in body
|
|
assert 'content="VacatureRadar"' in body
|
|
|
|
|
|
def test_login_with_email(client, user):
|
|
response = client.post(
|
|
reverse("login"),
|
|
{"username": user.email, "password": "correct-horse-battery-staple"},
|
|
)
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("dashboard:today")
|
|
assert client.session.get("_auth_user_id") == str(user.pk)
|
|
|
|
|
|
def test_login_with_username_still_works(client, user):
|
|
response = client.post(
|
|
reverse("login"),
|
|
{"username": user.username, "password": "correct-horse-battery-staple"},
|
|
)
|
|
assert response.status_code == 302
|
|
assert client.session.get("_auth_user_id") == str(user.pk)
|
|
|
|
|
|
def test_login_email_case_insensitive(client, user):
|
|
response = client.post(
|
|
reverse("login"),
|
|
{"username": user.email.upper(), "password": "correct-horse-battery-staple"},
|
|
)
|
|
assert response.status_code == 302
|
|
assert client.session.get("_auth_user_id") == str(user.pk)
|
|
|
|
|
|
def test_login_wrong_password_shows_error(client, user):
|
|
response = client.post(
|
|
reverse("login"),
|
|
{"username": user.email, "password": "verkeerd-wachtwoord"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert "klopt niet" in response.content.decode("utf-8")
|
|
|
|
|
|
def test_password_reset_sends_email(client, user):
|
|
response = client.post(reverse("password_reset"), {"email": user.email})
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("password_reset_done")
|
|
assert len(mail.outbox) == 1
|
|
message = mail.outbox[0]
|
|
assert user.email in message.to
|
|
assert "wachtwoord" in message.subject.lower()
|
|
assert "/wachtwoord/herstellen/" in message.body
|
|
|
|
|
|
def test_password_reset_unknown_email_is_silent(client):
|
|
response = client.post(reverse("password_reset"), {"email": "onbekend@example.invalid"})
|
|
assert response.status_code == 302
|
|
assert len(mail.outbox) == 0
|
|
|
|
|
|
def test_demo_button_visible_by_default(client):
|
|
assert reverse("demo-login") in client.get(reverse("login")).content.decode("utf-8")
|
|
|
|
|
|
@override_settings(DEMO_MODE_ENABLED=False)
|
|
def test_demo_login_disabled_redirects(client):
|
|
response = client.post(reverse("demo-login"))
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("login")
|
|
assert client.session.get("_auth_user_id") is None
|
|
|
|
|
|
@override_settings(DEMO_MODE_ENABLED=False)
|
|
def test_demo_button_hidden_when_disabled(client):
|
|
assert "demo-login" not in client.get(reverse("login")).content.decode("utf-8")
|
|
|
|
|
|
def test_demo_login_logs_in_and_seeds_environment(client):
|
|
from apps.jobs.models import Application, JobPosting, ScoreRun
|
|
from apps.profiles.models import SearchProfile
|
|
|
|
response = client.post(reverse("demo-login"))
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("dashboard:today")
|
|
|
|
user = get_user_model().objects.get(username=settings.DEMO_USERNAME)
|
|
assert not user.has_usable_password()
|
|
assert client.session.get("_auth_user_id") == str(user.pk)
|
|
|
|
# Fictieve, gevulde demo-omgeving.
|
|
profile = SearchProfile.objects.get(user=user, is_active=True)
|
|
assert JobPosting.objects.count() >= 5
|
|
assert ScoreRun.objects.filter(profile=profile).count() >= 5
|
|
assert Application.objects.filter(user=user).exists()
|
|
|
|
# Het gevulde dashboard rendert zonder fouten.
|
|
dashboard = client.get(reverse("dashboard:today"))
|
|
assert dashboard.status_code == 200
|
|
dashboard_html = dashboard.content.decode("utf-8")
|
|
assert "Profiel actief" in dashboard_html
|
|
assert "Radar actief" not in dashboard_html
|
|
assert "Nog geen persoonlijke matches" not in dashboard_html
|
|
|
|
sources = client.get(reverse("sources:list"))
|
|
assert sources.status_code == 200
|
|
assert "minstens één import actief" in sources.content.decode("utf-8")
|
|
|
|
|
|
def test_demo_login_is_idempotent(client, monkeypatch):
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from apps.jobs.models import Application, JobPosting
|
|
from apps.jobs.services import demo_seed
|
|
from apps.profiles.models import SearchProfile
|
|
from apps.sources.models import MailboxConnection, Source
|
|
|
|
# 22:30 UTC is already the following calendar day in Europe/Brussels.
|
|
# Freezing this boundary prevents UTC `.date()` calls from creeping back in.
|
|
near_local_midnight = datetime(2026, 9, 2, 22, 30, tzinfo=UTC)
|
|
monkeypatch.setattr(demo_seed.timezone, "now", lambda: near_local_midnight)
|
|
expected_local_day = timezone.localdate(near_local_midnight)
|
|
|
|
client.post(reverse("demo-login"))
|
|
first = JobPosting.objects.count()
|
|
demo_user = get_user_model().objects.get(username=settings.DEMO_USERNAME)
|
|
profile = SearchProfile.objects.get(user=demo_user, is_active=True)
|
|
job = profile.scores.order_by("-score").first().job
|
|
job.status = JobPosting.Status.EXPIRED
|
|
job.last_seen = timezone.now() - timedelta(days=30)
|
|
job.save(update_fields=["status", "last_seen"])
|
|
application = Application.objects.filter(user=demo_user).first()
|
|
application.follow_up_date = timezone.localdate() - timedelta(days=14)
|
|
application.save(update_fields=["follow_up_date"])
|
|
|
|
client.logout()
|
|
client.post(reverse("demo-login"))
|
|
assert JobPosting.objects.count() == first
|
|
job.refresh_from_db()
|
|
application.refresh_from_db()
|
|
assert job.status == JobPosting.Status.ACTIVE
|
|
assert timezone.localdate(job.last_seen) == expected_local_day
|
|
assert application.follow_up_date > expected_local_day
|
|
assert profile.scores.count() == 6
|
|
assert MailboxConnection.objects.filter(user=demo_user, enabled=True).count() == 9
|
|
assert Source.objects.filter(source_type=Source.Type.EMAIL, failure_count=0).count() >= 9
|
|
|
|
|
|
def test_login_password_field_has_no_mask_placeholder(client):
|
|
html = client.get(reverse("login")).content.decode("utf-8")
|
|
assert 'id="id_password"' in html
|
|
assert 'placeholder="••••••••"' not in html
|
|
|
|
|
|
def test_demo_account_is_read_only_and_session_is_bounded(client):
|
|
from apps.profiles.models import SearchProfile
|
|
|
|
client.post(reverse("demo-login"))
|
|
demo_user = get_user_model().objects.get(username=settings.DEMO_USERNAME)
|
|
active = SearchProfile.objects.get(user=demo_user, is_active=True)
|
|
alternative = SearchProfile.objects.create(user=demo_user, name="Alternatief", is_active=False)
|
|
|
|
response = client.post(
|
|
reverse("profiles:activate", args=[alternative.pk]),
|
|
HTTP_REFERER=reverse("profiles:list"),
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("profiles:list")
|
|
active.refresh_from_db()
|
|
alternative.refresh_from_db()
|
|
assert active.is_active is True
|
|
assert alternative.is_active is False
|
|
assert client.session.get_expiry_age() <= settings.DEMO_SESSION_SECONDS
|
|
|
|
|
|
def test_demo_account_can_still_log_out(client):
|
|
client.post(reverse("demo-login"))
|
|
response = client.post(reverse("logout"))
|
|
assert response.status_code == 302
|
|
assert client.session.get("_auth_user_id") is None
|
|
|
|
|
|
def test_authenticated_demo_page_disables_mutating_forms(client):
|
|
client.post(reverse("demo-login"))
|
|
body = client.get(reverse("dashboard:today")).content.decode("utf-8")
|
|
assert 'data-demo-read-only="true"' in body
|
|
assert "Publieke demo · alleen-lezen" in body
|
|
assert "data-demo-allow" in body
|
|
|
|
|
|
def test_entra_login_disabled_redirects(client):
|
|
response = client.post(reverse("entra-login"))
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("login")
|
|
|
|
|
|
@override_settings(ENTRA_ID_ENABLED=True, ENTRA_CLIENT_ID="", ENTRA_TENANT_ID="")
|
|
def test_entra_login_enabled_but_unconfigured_is_handled(client):
|
|
# Ingeschakeld maar zonder credentials: nette redirect, geen 500.
|
|
response = client.post(reverse("entra-login"), follow=False)
|
|
assert response.status_code == 302
|
|
assert response.url == reverse("login")
|
|
|
|
|
|
@override_settings(ENTRA_ID_ENABLED=True)
|
|
def test_entra_button_visible_when_enabled(client):
|
|
assert reverse("entra-login") in client.get(reverse("login")).content.decode("utf-8")
|