feat: release regional radar and mailbox integrations
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-22 05:12:07 +02:00
parent 598d3ec18a
commit 551d0f46c2
131 changed files with 6209 additions and 336 deletions
+51
View File
@@ -89,6 +89,22 @@ def test_lever_adapter_detail_supports_changed_markup():
assert job.location_text == "Aalst"
def test_lever_adapter_supports_documented_public_postings_api_shape():
adapter = LeverAdapter()
result = adapter.extract(
_load_fixture("fixtures/ats/lever-public-postings.json"),
url="https://api.lever.co/v0/postings/deliverect?mode=json",
)
assert len(result.jobs) == 1
job = result.jobs[0]
assert job.title == "Backend Engineer"
assert job.url == "https://jobs.lever.co/deliverect/deliverect-open-1"
assert job.location_text == "Ghent, Flemish Region"
assert job.employment_types == ["Full-time Permanent"]
assert job.workplace_type == "hybrid"
def test_recruitee_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = RecruiteeAdapter()
result = adapter.extract(
@@ -114,6 +130,23 @@ def test_recruitee_adapter_detail_supports_changed_markup():
assert job.employment_types == ["permanent"]
def test_recruitee_adapter_supports_documented_public_offers_shape():
adapter = RecruiteeAdapter()
result = adapter.extract(
_load_fixture("fixtures/ats/recruitee-public-offers.json"),
url="https://vito.recruitee.com/api/offers/",
)
assert len(result.jobs) == 1
job = result.jobs[0]
assert job.title == "Research Software Engineer"
assert job.employer_name == "VITO"
assert job.url == "https://jobs.vito.be/o/research-software-engineer"
assert job.postal_code == "2400"
assert job.employment_types == ["fulltime_fixed_term"]
assert job.workplace_type == "hybrid"
def test_smartrecruiters_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = SmartRecruitersAdapter()
result = adapter.extract(
@@ -139,6 +172,24 @@ def test_smartrecruiters_adapter_detail_supports_changed_markup():
assert job.workplace_type in {"remote", "hybrid", ""}
def test_smartrecruiters_adapter_supports_documented_public_postings_shape():
adapter = SmartRecruitersAdapter()
result = adapter.extract(
_load_fixture("fixtures/ats/smartrecruiters-public-postings.json"),
url=("https://api.smartrecruiters.com/v1/companies/ARHS/postings?limit=100&country=be"),
)
assert len(result.jobs) == 2
job = result.jobs[1]
assert job.title == "Cloud Engineer"
assert job.employer_name == "ARHS"
assert job.url == "https://jobs.smartrecruiters.com/ARHS/744000136173889"
assert job.location_text == "Leuven"
assert job.country == "be"
assert job.employment_types == ["Permanent"]
assert job.workplace_type == "remote"
def test_workable_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = WorkableAdapter()
result = adapter.extract(
@@ -24,3 +24,18 @@ def test_runtime_is_compatible_with_read_only_container() -> None:
assert '"--no-control-socket"' in dockerfile
assert "exec -T web python manage.py collectstatic" not in deploy_script
assert "for required_host in 127.0.0.1 localhost" in deploy_script
def test_deployment_examples_use_per_platform_mailbox_configuration() -> None:
required_keys = {
"MAILBOX_CREDENTIAL_KEYS",
"IMAP_CONNECT_TIMEOUT_SECONDS",
"IMAP_MAX_MESSAGES_PER_POLL",
"IMAP_MAX_MESSAGE_BYTES",
}
for filename in (".env.example", "scripts/codex_bootstrap.sh", "scripts/release_verify.sh"):
content = (ROOT / filename).read_text(encoding="utf-8")
assert "IMAP_ENABLED" not in content
for key in required_keys:
assert f"{key}=" in content
+44
View File
@@ -1,6 +1,10 @@
from pathlib import Path
import pytest
from apps.sources.adapters.email_alert import EmailAlertAdapter
from apps.sources.models import MailboxConnection
from apps.sources.platforms import PLATFORM_ALERTS
def test_email_adapter_extracts_job_links_and_skips_unsubscribe():
@@ -10,3 +14,43 @@ def test_email_adapter_extracts_job_links_and_skips_unsubscribe():
assert "https://jobs.example.org/vacatures/infrastructure-engineer" in urls
assert "https://www.linkedin.com/jobs/view/123456" in urls
assert all("unsubscribe" not in url for url in urls)
def test_email_adapter_recognizes_sanitized_vdab_alert():
raw = Path("fixtures/emails/vdab_job_alert.eml").read_bytes()
result = EmailAlertAdapter().extract_message(raw)
assert {job.title for job in result.jobs} == {"Python developer", "Cloud engineer"}
assert all(job.raw["alert_provider"] == "vdab" for job in result.jobs)
assert all(job.raw["target_domain"] == "www.vdab.be" for job in result.jobs)
assert all("uitschrijven" not in job.url for job in result.jobs)
@pytest.mark.parametrize(
("provider", "fixture", "title", "domain"),
(
("linkedin", "linkedin_job_alert.eml", "Software Engineer bij Example", "linkedin.com"),
("ictjob", "ictjob_job_alert.eml", "Cloud Engineer", "ictjob.be"),
("jobat", "jobat_job_alert.eml", "Data Engineer", "jobat.be"),
("stepstone", "stepstone_job_alert.eml", "Software Developer", "stepstone.be"),
("careerjet", "careerjet_job_alert.eml", "System Engineer", "careerjet.be"),
("randstad", "randstad_job_alert.eml", "Infrastructure Engineer", "randstad.be"),
("roberthalf", "roberthalf_job_alert.eml", "Network Engineer", "roberthalf.com"),
),
)
def test_platform_bound_alert_extracts_only_expected_https_jobboard_link(
provider, fixture, title, domain
):
raw = (Path("fixtures/emails") / fixture).read_bytes()
result = EmailAlertAdapter().extract_message(raw, expected_provider=provider)
assert [job.title for job in result.jobs] == [title]
assert result.jobs[0].raw["alert_provider"] == provider
assert result.jobs[0].raw["target_domain"].endswith(domain)
assert result.jobs[0].url.startswith("https://")
assert "outside.example.invalid" not in result.jobs[0].url
def test_mailbox_platform_choices_and_alert_catalog_stay_in_sync():
assert set(MailboxConnection.Platform.values) == set(PLATFORM_ALERTS)
+110
View File
@@ -0,0 +1,110 @@
import pytest
from cryptography.fernet import Fernet
from django.core.exceptions import ValidationError
from django.test import override_settings
from apps.sources.models import MailboxConnection
from apps.sources.services.imap_import import poll_imap_mailbox
from apps.sources.services.mailbox_connections import (
MailboxCredentialError,
decrypt_mailbox_password,
encrypt_mailbox_password,
rotate_mailbox_credentials,
)
from apps.sources.services.url_security import UnsafeUrlError
def test_mailbox_password_is_encrypted_and_supports_key_rotation():
old_key = Fernet.generate_key().decode("ascii")
new_key = Fernet.generate_key().decode("ascii")
with override_settings(MAILBOX_CREDENTIAL_KEYS=[old_key]):
ciphertext = encrypt_mailbox_password("app-password-value")
connection = MailboxConnection(encrypted_password=ciphertext)
assert "app-password-value" not in ciphertext
with override_settings(MAILBOX_CREDENTIAL_KEYS=[new_key, old_key]):
assert decrypt_mailbox_password(connection) == "app-password-value"
rotated = encrypt_mailbox_password("replacement-value")
assert Fernet(new_key.encode("ascii")).decrypt(rotated.encode("ascii")) == b"replacement-value"
def test_mailbox_password_fails_closed_without_valid_key():
with (
override_settings(MAILBOX_CREDENTIAL_KEYS=[]),
pytest.raises(MailboxCredentialError, match="niet geconfigureerd"),
):
encrypt_mailbox_password("never-stored")
with (
override_settings(MAILBOX_CREDENTIAL_KEYS=["invalid"]),
pytest.raises(MailboxCredentialError, match="ongeldige sleutel"),
):
encrypt_mailbox_password("never-stored")
@pytest.mark.parametrize("host", ["", "https://imap.example.org", "imap.example.org/path", "a b"])
def test_custom_mailbox_host_rejects_non_hostname_values(host):
connection = MailboxConnection(
platform=MailboxConnection.Platform.VDAB,
provider=MailboxConnection.Provider.CUSTOM,
custom_host=host,
port=993,
username="mailbox@example.invalid",
encrypted_password="ciphertext",
)
with pytest.raises(ValidationError):
connection.clean()
@pytest.mark.parametrize("port", [0, 65536])
def test_mailbox_rejects_invalid_port(port):
connection = MailboxConnection(
platform=MailboxConnection.Platform.VDAB,
provider=MailboxConnection.Provider.GMAIL,
port=port,
username="mailbox@example.invalid",
encrypted_password="ciphertext",
)
with pytest.raises(ValidationError):
connection.clean()
@pytest.mark.django_db
def test_rotation_reencrypts_existing_credentials_with_primary_key(user):
old_key = Fernet.generate_key().decode("ascii")
new_key = Fernet.generate_key().decode("ascii")
with override_settings(MAILBOX_CREDENTIAL_KEYS=[old_key]):
ciphertext = encrypt_mailbox_password("rotate-me")
connection = MailboxConnection.objects.create(
user=user,
platform=MailboxConnection.Platform.VDAB,
provider=MailboxConnection.Provider.GMAIL,
username="rotate@example.invalid",
encrypted_password=ciphertext,
)
with override_settings(MAILBOX_CREDENTIAL_KEYS=[new_key, old_key]):
assert rotate_mailbox_credentials() == 1
connection.refresh_from_db()
with override_settings(MAILBOX_CREDENTIAL_KEYS=[new_key]):
assert decrypt_mailbox_password(connection) == "rotate-me"
@pytest.mark.django_db
def test_private_imap_host_is_blocked_before_connection(user):
key = Fernet.generate_key().decode("ascii")
with override_settings(MAILBOX_CREDENTIAL_KEYS=[key]):
connection = MailboxConnection.objects.create(
user=user,
platform=MailboxConnection.Platform.VDAB,
provider=MailboxConnection.Provider.CUSTOM,
custom_host="127.0.0.1",
username="private@example.invalid",
encrypted_password=encrypt_mailbox_password("never-sent"),
)
with pytest.raises(UnsafeUrlError, match="Niet-publiek"):
poll_imap_mailbox(
connection,
client_factory=lambda *args, **kwargs: pytest.fail(
"Private IMAP-host had niet geconnecteerd mogen worden"
),
)
+253
View File
@@ -0,0 +1,253 @@
from pathlib import Path
import pytest
from apps.sources.adapters.kempen import KempenEmployerAdapter
from apps.sources.adapters.mol_region import MolRegionEmployerAdapter
from apps.sources.adapters.regional import CordaCampusAdapter, LocalEmployerListingAdapter
def test_corda_adapter_extracts_local_employer_cards_and_rejects_off_domain_links():
content = Path("fixtures/pages/corda-campus-jobs.html").read_text(encoding="utf-8")
result = CordaCampusAdapter().extract(
content,
url="https://www.cordacampus.com/jobs/",
)
assert result.parser_key == "regional-corda-campus"
assert len(result.jobs) == 2
assert {job.title for job in result.jobs} == {"System Engineer", "Data Engineer"}
assert {job.employer_name for job in result.jobs} == {
"Voorbeeld IT",
"Voorbeeld Data",
}
assert all(job.region == "Limburg" for job in result.jobs)
assert all("evil.example" not in job.url for job in result.jobs)
def test_corda_adapter_does_not_capture_individual_detail_pages():
result = CordaCampusAdapter().extract(
"<h1>System Engineer</h1>",
url="https://www.cordacampus.com/job/system-engineer/",
)
assert result.jobs == []
assert result.confidence == 0.0
@pytest.mark.parametrize(
("fixture_name", "url", "expected_title", "expected_employer"),
[
(
"aca-group-jobs.html",
"https://acagroup.be/en/jobs",
"AWS Cloud & DevOps Engineer",
"ACA Group",
),
(
"xplore-group-jobs.html",
"https://www.xploregroup.be/en/jobs",
"Platform Engineer",
"Xplore Group",
),
(
"uhasselt-jobs.html",
"https://www.uhasselt.be/vacatures",
"Expert systeembeheerder (100%)",
"Universiteit Hasselt",
),
(
"pxl-jobs.html",
"https://ses.pxl.be/",
"Lector netwerkbeheer",
"Hogeschool PXL",
),
(
"zol-jobs.html",
"https://ziekenhuis-oost-limburg.cvw.io/?lang=nl-BE",
"(Senior) IT Engineer Infrastructuur",
"Ziekenhuis Oost-Limburg",
),
],
)
def test_local_employer_adapter_extracts_reviewed_routes_only(
fixture_name, url, expected_title, expected_employer
):
content = Path(f"fixtures/pages/{fixture_name}").read_text(encoding="utf-8")
result = LocalEmployerListingAdapter().extract(content, url=url)
assert result.parser_key == "regional-local-employers"
assert len(result.jobs) == 1
assert result.jobs[0].title == expected_title
assert result.jobs[0].employer_name == expected_employer
assert result.jobs[0].region == "Limburg"
assert "evil.example" not in result.jobs[0].url
def test_local_employer_adapter_fails_closed_for_unreviewed_host_or_path():
adapter = LocalEmployerListingAdapter()
assert adapter.extract("<h3>Fake</h3>", url="https://evil.example/jobs").jobs == []
assert (
adapter.extract(
"<h3>Detail</h3>",
url="https://www.uhasselt.be/vacatures/detail/3999",
).jobs
== []
)
@pytest.mark.parametrize(
("fixture_name", "url", "expected_title", "expected_employer"),
[
(
"sck-cen-jobs.html",
"https://www.sckcen.be/nl/carriere/vacatures",
"Automatisatie Ingenieur PLC & OT-netwerken",
"SCK CEN",
),
(
"cipal-schaubroeck-jobs.html",
"https://cipalschaubroeck.teamtailor.com/jobs",
"Network & Security Engineer",
"Cipal Schaubroeck",
),
(
"vanroey-jobs.html",
"https://www.vanroey.be/en/job-overview/",
"Network Engineer",
"VanRoey",
),
(
"netropolix-jobs.html",
"https://netropolix.recruitee.com/",
"System Engineer - Geel",
"NTX (Netropolix)",
),
(
"thomas-more-jobs.html",
"https://jobpage.cvwarehouse.com/?companyGuid=eab9ca13-ee10-4504-8a87-a785d0b037ef&lang=nl-BE",
"Een medewerker lokale IT - Campus Geel",
"Thomas More",
),
],
)
def test_mol_region_adapter_extracts_only_reviewed_local_cards(
fixture_name, url, expected_title, expected_employer
):
content = Path(f"fixtures/pages/{fixture_name}").read_text(encoding="utf-8")
result = MolRegionEmployerAdapter().extract(content, url=url)
assert result.parser_key == "regional-mol-employers"
assert result.warnings == []
assert len(result.jobs) == 1
assert result.jobs[0].title == expected_title
assert result.jobs[0].employer_name == expected_employer
assert result.jobs[0].region == "Antwerpen"
assert "evil.example" not in result.jobs[0].url
def test_mol_region_adapter_fails_closed_for_unreviewed_route_and_company():
adapter = MolRegionEmployerAdapter()
assert adapter.extract("<h1>Fake</h1>", url="https://evil.example/jobs").jobs == []
assert (
adapter.extract(
"<h1>Detail</h1>",
url="https://www.sckcen.be/nl/carriere/vacatures/detail",
).jobs
== []
)
@pytest.mark.parametrize(
("fixture_name", "url", "expected_title", "expected_employer", "expected_postal"),
[
(
"ziekenhuis-geel-jobs.html",
"https://ziekenhuisgeel.careersite.be/nl/vacatures",
"ICT Support Engineer",
"Ziekenhuis Geel",
"2440",
),
(
"stad-geel-jobs.html",
"https://geel.hro.be/",
"Administratief medewerker digitale dienstverlening",
"Lokaal bestuur Geel",
"2440",
),
(
"stad-turnhout-jobs.html",
"https://jobs.turnhout.be/",
"Coördinator digitale projecten",
"Stad Turnhout",
"2300",
),
(
"renotec-geel-jobs.html",
"https://jobs.renotec.be/nl/alle-jobs/",
"Technisch commercieel verantwoordelijke",
"Group Renotec",
"2440",
),
(
"ravago-kempen-jobs.html",
"https://ravago.softgarden.io/en/vacancies",
"SAP Operations Analyst",
"Ravago",
"2370",
),
(
"sanofi-geel-jobs.html",
"https://jobs.sanofi.com/en/belgium",
"Maintenance Engineer",
"Sanofi",
"2440",
),
(
"daf-westerlo-jobs.html",
"https://www.daf.com/nl-nl/werken-bij-daf/vacatures",
"Technieker Facilitaire Diensten",
"DAF Trucks",
"2260",
),
],
)
def test_kempen_adapter_extracts_only_reviewed_local_cards(
fixture_name, url, expected_title, expected_employer, expected_postal
):
content = Path(f"fixtures/pages/{fixture_name}").read_text(encoding="utf-8")
result = KempenEmployerAdapter().extract(content, url=url)
assert result.parser_key == "regional-kempen-employers"
assert result.warnings == []
assert len(result.jobs) == 1
assert result.jobs[0].title == expected_title
assert result.jobs[0].employer_name == expected_employer
assert result.jobs[0].postal_code == expected_postal
assert "evil.example" not in result.jobs[0].url
def test_kempen_adapter_fails_closed_for_unreviewed_route():
adapter = KempenEmployerAdapter()
assert adapter.extract("<h1>Fake</h1>", url="https://evil.example/jobs").jobs == []
assert (
adapter.extract(
"<h1>Detail</h1>",
url="https://jobs.sanofi.com/en/search-jobs",
).jobs
== []
)
assert (
adapter.extract(
"<h1>Andere werkgever</h1>",
url="https://jobpage.cvwarehouse.com/?companyGuid=other&lang=nl-BE",
).jobs
== []
)
+14 -1
View File
@@ -3,7 +3,20 @@ from __future__ import annotations
import pytest
from django.core.exceptions import ImproperlyConfigured
from config.settings import _validate_production_security
from config.settings import _validate_production_security, env_int_range
def test_env_int_range_accepts_default_and_rejects_invalid_values(monkeypatch):
monkeypatch.delenv("TEST_BOUNDED_SETTING", raising=False)
assert env_int_range("TEST_BOUNDED_SETTING", 6, minimum=0, maximum=23) == 6
monkeypatch.setenv("TEST_BOUNDED_SETTING", "24")
with pytest.raises(ImproperlyConfigured):
env_int_range("TEST_BOUNDED_SETTING", 6, minimum=0, maximum=23)
monkeypatch.setenv("TEST_BOUNDED_SETTING", "morgen")
with pytest.raises(ImproperlyConfigured):
env_int_range("TEST_BOUNDED_SETTING", 6, minimum=0, maximum=23)
def test_validate_production_security_skips_when_debug_mode_is_enabled():
+25
View File
@@ -2,6 +2,7 @@ from datetime import timedelta
import pytest
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.utils import timezone
from apps.sources.models import Source, SourceRun
@@ -40,3 +41,27 @@ def test_source_run_finish_updates_metrics(source):
assert run.finished_at is not None
assert run.http_status == 200
assert run.extracted_count == 3
@pytest.mark.django_db(transaction=True)
def test_shared_ats_domain_accepts_distinct_public_feeds_but_not_duplicate_urls():
Source.objects.create(
name="Lever A",
source_type=Source.Type.ATS,
base_url="https://api.lever.co/v0/postings/a?mode=json",
domain="api.lever.co",
)
Source.objects.create(
name="Lever B",
source_type=Source.Type.ATS,
base_url="https://api.lever.co/v0/postings/b?mode=json",
domain="api.lever.co",
)
with pytest.raises(IntegrityError):
Source.objects.create(
name="Lever A dubbel",
source_type=Source.Type.ATS,
base_url="https://api.lever.co/v0/postings/a?mode=json",
domain="api.lever.co",
)
+15
View File
@@ -0,0 +1,15 @@
import ssl
from pathlib import Path
import certifi
from apps.sources.services.tls import trusted_tls_context
def test_application_tls_context_uses_strict_pinned_ca_bundle():
context = trusted_tls_context()
assert Path(certifi.where()).is_file()
assert context.verify_mode == ssl.CERT_REQUIRED
assert context.check_hostname is True
assert context.cert_store_stats()["x509_ca"] > 0