Harden public demo for 0.3.16
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak.
|
||||
|
||||
## 0.3.16 - 2026-08-12
|
||||
|
||||
### Hersteld
|
||||
|
||||
- De publieke demo herstelt bij iedere nieuwe demosessie zes actuele fictieve vacatures, hun laatste scores en twee toekomstige opvolgmomenten zonder duplicaten of onbeperkte scorehistoriek.
|
||||
- Negen fictieve `example.test`-platformmailboxen maken de mailboxflow zichtbaar zonder echte accounts, credentials of externe polls.
|
||||
- Mailboxingress wordt uitsluitend door de mailboxscheduler verwerkt; e-mailbronnen komen niet meer in URL-fetchplanning, dashboardscanstatus of de algemene bronrunactiviteit terecht.
|
||||
- Statuscopy onderscheidt een actief zoekprofiel van operationele brongezondheid en het lege loginwachtwoordveld lijkt niet langer vooraf ingevuld.
|
||||
|
||||
## 0.3.14 - 2026-07-29
|
||||
|
||||
### Gewijzigd
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
VacatureRadar is een persoonlijke vacature-assistent voor België. De applicatie volgt toegestane werkgeversbronnen, ATS-pagina’s, feeds en vacaturemails, verwijdert dubbels en irrelevante resultaten en rangschikt de overblijvende kansen tegen jouw zoekprofiel. Jij behoudt altijd de controle: VacatureRadar helpt zoeken en opvolgen, maar solliciteert nooit automatisch.
|
||||
|
||||
**Actuele release: 0.3.15.** De volledige P0/P1-releasebasis is afgerond en live
|
||||
gevalideerd: 324 tests zijn groen, Microsoft 365 verwerkt platformmails via
|
||||
**Actuele release: 0.3.16.** De volledige P0/P1-releasebasis en de publieke
|
||||
demo-hardening zijn afgerond: 327 tests zijn groen, Microsoft 365 verwerkt platformmails via
|
||||
app-only OAuth/XOAUTH2 en de productie-installatie draait vanaf een immutable
|
||||
image in de private Gitea-containerregistry. Het controleerbare acceptatiebewijs
|
||||
staat in [Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.urls import reverse
|
||||
|
||||
from apps.notifications.models import DigestOutbox, ReminderOutbox
|
||||
from apps.profiles.models import ProfileRevision
|
||||
from apps.sources.models import SourceRun
|
||||
from apps.sources.models import Source, SourceRun
|
||||
|
||||
from ..models import ApplicationTimelineEvent, Feedback, JobVersion
|
||||
|
||||
@@ -37,7 +37,8 @@ ACTIVITY_FILTERS = (
|
||||
|
||||
def _source_events() -> list[ActivityEvent]:
|
||||
events = []
|
||||
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:100]:
|
||||
runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
for run in runs.select_related("source").order_by("-started_at")[:100]:
|
||||
detail = (
|
||||
f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · "
|
||||
f"{run.duplicate_count} duplicaat"
|
||||
|
||||
@@ -57,7 +57,8 @@ def annotate_latest_profile_score(
|
||||
|
||||
def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]:
|
||||
activity: list[dict[str, Any]] = []
|
||||
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:4]:
|
||||
source_runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
for run in source_runs.select_related("source").order_by("-started_at")[:4]:
|
||||
activity.append(
|
||||
{
|
||||
"occurred_at": run.started_at,
|
||||
@@ -154,7 +155,9 @@ def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[s
|
||||
"source_counts": source_counts,
|
||||
"recent_activity": _recent_dashboard_activity(user),
|
||||
"day_conclusion": day_conclusion,
|
||||
"latest_scan": SourceRun.objects.order_by("-started_at").first(),
|
||||
"latest_scan": SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
|
||||
.order_by("-started_at")
|
||||
.first(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -122,11 +122,10 @@ def demo_environment_seeded(user) -> bool:
|
||||
|
||||
|
||||
def seed_demo_environment(user) -> None:
|
||||
"""Maak (idempotent) een fictieve demo-omgeving voor ``user``."""
|
||||
if demo_environment_seeded(user):
|
||||
return
|
||||
|
||||
"""Maak of herstel een fictieve, tijdsbestendige demo-omgeving voor ``user``."""
|
||||
from apps.jobs.models import Application, Employer, JobPosting, ScoreRun
|
||||
from apps.sources.models import MailboxConnection, Source, SourceRun
|
||||
from apps.sources.platforms import PLATFORM_ALERTS
|
||||
|
||||
profile, _ = SearchProfile.objects.get_or_create(
|
||||
user=user,
|
||||
@@ -185,7 +184,7 @@ def seed_demo_environment(user) -> None:
|
||||
},
|
||||
)
|
||||
slug = _digest("demo-job", str(index), title, employer_name)
|
||||
job, job_created = JobPosting.objects.get_or_create(
|
||||
job, _ = JobPosting.objects.update_or_create(
|
||||
canonical_key=slug,
|
||||
defaults={
|
||||
"employer": employer,
|
||||
@@ -221,6 +220,11 @@ def seed_demo_environment(user) -> None:
|
||||
)
|
||||
created_jobs.append(job)
|
||||
|
||||
ScoreRun.objects.filter(
|
||||
job=job,
|
||||
profile=profile,
|
||||
model_version="demo-seed",
|
||||
).delete()
|
||||
ScoreRun.objects.create(
|
||||
job=job,
|
||||
profile=profile,
|
||||
@@ -244,7 +248,7 @@ def seed_demo_environment(user) -> None:
|
||||
|
||||
# Een kleine sollicitatiepijplijn voor de demo.
|
||||
if created_jobs:
|
||||
Application.objects.get_or_create(
|
||||
Application.objects.update_or_create(
|
||||
user=user,
|
||||
job=created_jobs[0],
|
||||
defaults={
|
||||
@@ -257,7 +261,7 @@ def seed_demo_environment(user) -> None:
|
||||
},
|
||||
)
|
||||
if len(created_jobs) > 3:
|
||||
Application.objects.get_or_create(
|
||||
Application.objects.update_or_create(
|
||||
user=user,
|
||||
job=created_jobs[3],
|
||||
defaults={
|
||||
@@ -267,3 +271,42 @@ def seed_demo_environment(user) -> None:
|
||||
"notes": "Sollicitatie verstuurd. Demo-data.",
|
||||
},
|
||||
)
|
||||
|
||||
# Platformmailboxen zijn zichtbare ingressconfiguratie, geen crawlbare URL-bronnen.
|
||||
# De demo gebruikt uitsluitend fictieve adressen en wordt door de scheduler overgeslagen.
|
||||
for platform in PLATFORM_ALERTS:
|
||||
MailboxConnection.objects.update_or_create(
|
||||
user=user,
|
||||
platform=platform,
|
||||
defaults={
|
||||
"provider": MailboxConnection.Provider.OUTLOOK,
|
||||
"username": "alerts@example.test",
|
||||
"mailbox": f"VacatureRadar/{platform}",
|
||||
"enabled": True,
|
||||
"poll_interval_minutes": MailboxConnection.PollInterval.THIRTY_MINUTES,
|
||||
"next_poll_at": now + timedelta(minutes=30),
|
||||
"last_polled_at": now - timedelta(minutes=5),
|
||||
"last_success_at": now - timedelta(minutes=5),
|
||||
"last_error_category": "",
|
||||
"last_error_message": "",
|
||||
"lease_token": "",
|
||||
"lease_expires_at": None,
|
||||
},
|
||||
)
|
||||
source, _ = Source.objects.update_or_create(
|
||||
source_type=Source.Type.EMAIL,
|
||||
domain=f"{platform}.mailbox.local",
|
||||
defaults={
|
||||
"name": f"{PLATFORM_ALERTS[platform].label} vacaturemailbox",
|
||||
"base_url": "",
|
||||
"status": Source.Status.ACTIVE,
|
||||
"policy": Source.Policy.ALLOW,
|
||||
"policy_reason": "Mailboxingress; niet via URL-fetch gepland.",
|
||||
"parser_key": "email-alert",
|
||||
"failure_count": 0,
|
||||
"last_failure_at": None,
|
||||
"next_run_at": None,
|
||||
"metadata": {"demo": True, "mailbox_ingress": True},
|
||||
},
|
||||
)
|
||||
SourceRun.objects.filter(source=source).delete()
|
||||
|
||||
@@ -188,7 +188,8 @@ def schedule_due_sources() -> dict[str, int]:
|
||||
now = timezone.now()
|
||||
due = Source.objects.filter(
|
||||
status__in=[Source.Status.ACTIVE, Source.Status.TRIAL],
|
||||
).filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
|
||||
).exclude(source_type=Source.Type.EMAIL)
|
||||
due = due.filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
|
||||
ids = list(due.values_list("id", flat=True).distinct()[:200])
|
||||
for source_id in ids:
|
||||
fetch_source.delay(source_id)
|
||||
@@ -200,6 +201,7 @@ def schedule_mailbox_polls() -> dict[str, int]:
|
||||
now = timezone.now()
|
||||
due_ids = list(
|
||||
MailboxConnection.objects.filter(enabled=True, next_poll_at__lte=now)
|
||||
.exclude(user__username=settings.DEMO_USERNAME)
|
||||
.filter(Q(lease_expires_at__isnull=True) | Q(lease_expires_at__lte=now))
|
||||
.values_list("id", flat=True)[:100]
|
||||
)
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ CSRF_TRUSTED_ORIGINS = merge_unique(
|
||||
)
|
||||
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "Europe/Brussels")
|
||||
VACATURERADAR_OWNER_NAME = os.getenv("VACATURERADAR_OWNER_NAME", "Jens").strip()[:40] or "Jens"
|
||||
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.14").strip() or "0.3.14"
|
||||
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.16").strip() or "0.3.16"
|
||||
LANGUAGE_CODE = "nl-be"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
@@ -9,7 +9,7 @@ DJANGO_ALLOWED_HOSTS=vacatureradar.example.be,127.0.0.1,localhost
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=https://vacatureradar.example.be
|
||||
DJANGO_TIME_ZONE=Europe/Brussels
|
||||
VACATURERADAR_OWNER_NAME=Jens
|
||||
VACATURERADAR_VERSION=0.3.14
|
||||
VACATURERADAR_VERSION=0.3.16
|
||||
|
||||
POSTGRES_DB=vacatureradar
|
||||
POSTGRES_USER=vacatureradar
|
||||
|
||||
+42
-1
@@ -1,6 +1,6 @@
|
||||
schema_version: 1
|
||||
project: VacatureRadar
|
||||
updated_at: '2026-07-30'
|
||||
updated_at: '2026-08-12'
|
||||
execution_policy:
|
||||
order: file-order, then first ready task whose dependencies are done
|
||||
no_product_questions: true
|
||||
@@ -2376,3 +2376,44 @@ tasks:
|
||||
note: 'Release 0.3.14: geïsoleerde PostgreSQL-benchmark reduceert importduur 44,74% en queries minstens 63,73%;
|
||||
twee volledige gates met 318 tests groen; productie healthy op image sha256:1ee84241, transactionele importsmoke
|
||||
teruggerold en rollbackimage 0.3.13 beschikbaar.'
|
||||
- id: VR-229
|
||||
title: Herstel de publieke demo na de visuele 0.3.15-audit
|
||||
status: done
|
||||
priority: P1
|
||||
requirement_ids:
|
||||
- NFR-008
|
||||
- NFR-009
|
||||
depends_on:
|
||||
- VR-228
|
||||
summary: Maak de publieke demo opnieuw representatief en tijdsbestendig, scheid mailboxingress van URL-fetching
|
||||
en laat operationele statussen exact zeggen wat ze meten.
|
||||
acceptance_criteria:
|
||||
- Iedere nieuwe demosessie herstelt zes fictieve actuele vacatures, scores en toekomstige opvolgdatums zonder
|
||||
duplicaten of groeiende ScoreRun-historiek.
|
||||
- De demo toont negen fictieve, actieve platformmailboxkoppelingen zonder secrets of echte persoonsgegevens.
|
||||
- E-mailbronnen worden nooit door de generieke URL-fetchscheduler gepland en oude mailboxpolicyfouten vervuilen
|
||||
dashboard en activiteit niet meer.
|
||||
- Navigatie en dashboard noemen het zoekprofiel expliciet actief zonder brongezondheid te suggereren; het lege
|
||||
loginwachtwoordveld lijkt niet vooraf ingevuld.
|
||||
- De negen kernroutes blijven in dark/light en op mobiel, desktop en ultrawide zonder overflow, consolefouten
|
||||
of headingregressies bruikbaar; de volledige kwaliteitsgate slaagt.
|
||||
verification:
|
||||
- python -m pytest tests/integration/test_auth_login.py tests/integration/test_tasks_and_more_views.py
|
||||
- Browseraudit op 390x844, 1280x600, 1440x900 en 3440x1440
|
||||
- ./scripts/codex_verify.sh
|
||||
primary_paths:
|
||||
- apps/jobs/services/demo_seed.py
|
||||
- apps/jobs/services/activity.py
|
||||
- apps/jobs/services/cockpit.py
|
||||
- apps/sources/tasks.py
|
||||
- templates/base.html
|
||||
- templates/dashboard/today.html
|
||||
- templates/registration/login.html
|
||||
- tests/integration/test_auth_login.py
|
||||
- tests/integration/test_tasks_and_more_views.py
|
||||
result:
|
||||
completed_at: '2026-08-12'
|
||||
note: Publieke demo wordt bij iedere sessie idempotent hersteld met 6 actuele fictieve vacatures, tijdsbestendige
|
||||
opvolging en 9 fictieve mailboxkoppelingen; EMAIL-bronnen en demo-mailboxen zijn uit de verkeerde schedulers
|
||||
en activityfeeds uitgesloten, statuscopy en loginplaceholder zijn gecorrigeerd. Gerichte 51 tests en volledige
|
||||
gate met 327 tests groen, 84,04% dekking.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Projectstatus
|
||||
|
||||
- Laatst bijgewerkt: 2026-07-30
|
||||
- Repositoryversie: 0.3.15 M365 OAuth en immutable registryrelease
|
||||
- Laatst bijgewerkt: 2026-08-12
|
||||
- Repositoryversie: 0.3.16 publieke demo-hardening
|
||||
- Uitvoeringsmodus: autonome backlog
|
||||
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
|
||||
|
||||
@@ -21,6 +21,22 @@ De repository bevat een uitvoerbare Django-MVP met:
|
||||
|
||||
## Laatste documentatie-update
|
||||
|
||||
Visuele demo-hardening `VR-229` op 2026-08-12:
|
||||
|
||||
- iedere publieke demosessie herstelt idempotent zes actuele fictieve vacatures,
|
||||
de laatste scores, twee toekomstige opvolgmomenten en negen geïsoleerde
|
||||
`example.test`-platformmailboxkoppelingen;
|
||||
- mailboxbronnen worden niet meer door de URL-fetchscheduler gepland, de
|
||||
publieke demo wordt uitgesloten van echte mailboxpolling en historische
|
||||
mailboxpolicyfouten vervuilen dashboard en activiteit niet langer;
|
||||
- navigatie en dashboard onderscheiden nu expliciet een actief zoekprofiel van
|
||||
bron- of systeemgezondheid; het lege loginwachtwoordveld heeft geen misleidende
|
||||
mask-placeholder meer;
|
||||
- visuele audit: negen kernroutes groen in dark/light op 390×844, 1280×600,
|
||||
1440×900 en 3440×1440 zonder overflow, heading- of consolefouten;
|
||||
- gerichte regressies: 51 tests groen; volledige gate: 327 tests, 0 failures,
|
||||
0 skips en 84,04% branch-aware dekking. Volgende uitvoerbare taak: geen.
|
||||
|
||||
README-update op 2026-07-30:
|
||||
|
||||
- de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live
|
||||
|
||||
@@ -82,7 +82,7 @@ Django/Celery
|
||||
| T-40 | Verouderde lokale CA-store of ongeldige serverketen lokt een TLS-bypass uit | onderschepte vacature- of mailboxdata en credentialdiefstal | expliciet gepinde `certifi`-CA-bundel voor HTTP, robots en IMAP; hostnaamcontrole en `CERT_REQUIRED`; nooit `verify=False`; dependency- en negatieve TLS-tests bij update | `tests/unit/test_tls.py`, `tests/security/test_robots.py`, `apps/sources/services/tls.py`, `VR-201` |
|
||||
| T-41 | Gemodificeerde `next`-parameter na vacaturefeedback | open redirect naar phishingdomein na een geldige gebruikersactie | terugkeer-URL moet dezelfde host en het actuele HTTPS-beleid respecteren; anders veilige detailfallback | `tests/integration/test_tasks_and_more_views.py`, `VR-206` |
|
||||
| T-42 | Gespoofte proxyheaders of ontbrekende publieke hostconfiguratie | host-headerfouten, onjuiste HTTPS-detectie of omzeilde IP-rate-limiting | `PUBLIC_BASE_URL`, exacte host/CSRF-validatie, proxy moet headers overschrijven, forwarded client-IP alleen vanaf `TRUSTED_PROXY_CIDRS` | `tests/unit/test_public_configuration.py`, `tests/unit/test_network.py`, `VR-225` |
|
||||
| T-43 | Gedeeld demoaccount wijzigt persistente data | bezoekers beïnvloeden elkaars ervaring of echte data | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, zichtbare UI-status en productiecheck | `tests/integration/test_auth_login.py`, `tests/unit/test_settings_security.py`, `VR-225` |
|
||||
| T-43 | Gedeeld demoaccount wijzigt persistente data of activeert echte mailboxpolling | bezoekers beïnvloeden elkaars ervaring, echte data of externe accounts | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, uitsluitend fictieve `example.test`-mailboxen; de loginseed herstelt begrensd en idempotent en de scheduler sluit de demo-user expliciet uit | `tests/integration/test_auth_login.py`, `tests/integration/test_tasks_and_more_views.py`, `tests/unit/test_settings_security.py`, `VR-225/229` |
|
||||
|
||||
## Misbruikscenario's
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
|
||||
|
||||
## Aanvullende traceability-opmerking
|
||||
- VR-228 levert release 0.3.14 met een fail-closed disposable PostgreSQL-benchmark, run-lokale importcaches, gebundelde veldprovenance en ongewijzigde score-snapshotcopy. De officiële drie iteraties bewijzen 3.264 queries per 250 items, parser 12/12, dedupeprecision/recall 1,0 en nul rankingchurn; bewijs staat in `docs/audit/IMPORT_PERFORMANCE_*.md` en het machineleesbare artifact.
|
||||
- VR-229 houdt de publieke demo representatief door fictieve vacatures, scores, mailboxkoppelingen en opvolgdatums bij iedere demosessie idempotent te herstellen. Mailboxingress is uitgesloten van URL-fetchplanning en operationele UI-copy onderscheidt een actief profiel van bron- en systeemgezondheid.
|
||||
- VR-227 professionaliseert release 0.3.13 integraal: gecentreerd ultrawideframe, mobiele gastbanner en verticale pipeline, begrensde vacaturepaginering, verplichte zes-viewport-Playwrightgate zonder skips, echte runtimeheartbeat, volledige generieke parserbenchmark en SBOM-generatie. Bewijs staat in `docs/audit/`, de E2E-/auth-/runtime-/adaptertests en `USER_INPUT_REQUIRED.md`.
|
||||
- VR-102 is gerealiseerd met auditbare policyreviews (`SourcePolicyReview`), robotscache met TTL/size/SSRF-controles en fail-closed policy-gating op verlopen of conflicterende reviews.
|
||||
- VR-107 is gerealiseerd met `apps/sources/views.py`, `apps/sources/services/manual_import.py`, `templates/sources/list.html`, `static/js/manual_import.js` en bijhorende unit/integration-tests.
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-radar-card">
|
||||
<span class="status-beacon {% if navigation_profile %}is-live{% endif %}" aria-hidden="true"></span>
|
||||
<span><strong>{% if navigation_profile %}Radar actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
|
||||
<span><strong>{% if navigation_profile %}Profiel actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<header class="cockpit-heading dashboard-heading">
|
||||
<div><p class="eyebrow">Intelligence cockpit · vandaag</p><h1>Goedemorgen, {{ user.first_name|default:user.username }}.</h1><p>{{ day_conclusion }}</p></div>
|
||||
<div class="radar-status" aria-label="Radarstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}{{ active_profile.name }} actief{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
|
||||
<div class="radar-status" aria-label="Profiel- en scanstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}Profiel actief · {{ active_profile.name }}{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>laatste URL-scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
|
||||
</header>
|
||||
|
||||
<section class="stats-grid" aria-label="Radar kerncijfers">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input class="auth-input" id="id_password" name="password" type="password"
|
||||
autocomplete="current-password" placeholder="••••••••" required
|
||||
autocomplete="current-password" required
|
||||
{% if form.errors %}aria-invalid="true"{% endif %}>
|
||||
<button class="pw-toggle" type="button" data-pw-toggle aria-label="Wachtwoord tonen">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-eye><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.7" stroke="currentColor" stroke-width="1.8"/></svg>
|
||||
|
||||
@@ -135,15 +135,54 @@ def test_demo_login_logs_in_and_seeds_environment(client):
|
||||
# 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):
|
||||
from apps.jobs.models import JobPosting
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.jobs.models import Application, JobPosting
|
||||
from apps.profiles.models import SearchProfile
|
||||
from apps.sources.models import MailboxConnection, Source
|
||||
|
||||
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 job.last_seen.date() == timezone.localdate()
|
||||
assert application.follow_up_date > timezone.localdate()
|
||||
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):
|
||||
|
||||
@@ -104,6 +104,55 @@ def test_fetch_source_resets_fail_state_on_304(db, source, monkeypatch):
|
||||
assert source.failure_count == 0
|
||||
|
||||
|
||||
def test_due_source_scheduler_never_schedules_mailbox_sources(db, monkeypatch):
|
||||
from apps.sources.tasks import schedule_due_sources
|
||||
|
||||
email_source = Source.objects.create(
|
||||
name="Mailboxbron",
|
||||
source_type=Source.Type.EMAIL,
|
||||
base_url="",
|
||||
domain="example.mailbox.local",
|
||||
status=Source.Status.ACTIVE,
|
||||
policy=Source.Policy.ALLOW,
|
||||
)
|
||||
scheduled = []
|
||||
monkeypatch.setattr(
|
||||
"apps.sources.tasks.fetch_source.delay", lambda source_id: scheduled.append(source_id)
|
||||
)
|
||||
|
||||
result = schedule_due_sources()
|
||||
|
||||
assert email_source.pk not in scheduled
|
||||
assert result["scheduled"] == len(scheduled)
|
||||
|
||||
|
||||
def test_mailbox_scheduler_excludes_public_demo_user(db, monkeypatch, settings):
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from apps.sources.models import MailboxConnection
|
||||
from apps.sources.tasks import schedule_mailbox_polls
|
||||
|
||||
demo = get_user_model().objects.create_user(username=settings.DEMO_USERNAME)
|
||||
connection = MailboxConnection.objects.create(
|
||||
user=demo,
|
||||
platform="vdab",
|
||||
provider="outlook",
|
||||
username="demo@example.test",
|
||||
mailbox="Demo/VDAB",
|
||||
enabled=True,
|
||||
next_poll_at=timezone.now() - timedelta(minutes=1),
|
||||
)
|
||||
scheduled = []
|
||||
monkeypatch.setattr(
|
||||
"apps.sources.tasks.poll_mailbox.delay",
|
||||
lambda connection_id: scheduled.append(connection_id),
|
||||
)
|
||||
|
||||
schedule_mailbox_polls()
|
||||
|
||||
assert connection.pk not in scheduled
|
||||
|
||||
|
||||
def test_fetch_source_is_blocked_by_conflicting_review(db):
|
||||
source = Source.objects.create(
|
||||
name="Blocked source",
|
||||
|
||||
Reference in New Issue
Block a user