Fix release blockers and deployment build
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-21 21:22:29 +02:00
parent b8091e59bd
commit a4eced8be5
64 changed files with 1011 additions and 675 deletions
+17 -2
View File
@@ -1,12 +1,14 @@
from __future__ import annotations
from datetime import timedelta
from decimal import Decimal
import pytest
from django.contrib.auth import get_user_model
from django.utils import timezone
from apps.profiles.models import SearchProfile
from apps.sources.models import Source
from apps.sources.models import Source, SourcePolicyReview, SourceRobotsCache
@pytest.fixture
@@ -39,7 +41,7 @@ def profile(user):
@pytest.fixture
def source(db):
return Source.objects.create(
source = Source.objects.create(
name="Example jobs",
source_type=Source.Type.EMPLOYER,
base_url="https://jobs.example.org/vacatures/",
@@ -47,6 +49,19 @@ def source(db):
status=Source.Status.ACTIVE,
policy=Source.Policy.ALLOW,
)
SourcePolicyReview.objects.create(
source=source,
decision=SourcePolicyReview.Decision.ALLOW,
reason="Testbron expliciet toegestaan",
expires_at=timezone.now() + timedelta(days=1),
)
SourceRobotsCache.objects.create(
origin="https://jobs.example.org",
expires_at=timezone.now() + timedelta(days=1),
allow_rules={"*": ["/"]},
disallow_rules={},
)
return source
@pytest.fixture
@@ -41,7 +41,7 @@ def _assert_basic_html_accessibility(payload: bytes | str, *, page_name: str) ->
headings = soup.find_all("h1")
assert headings, f"{page_name}: ontbreekt h1"
assert len(headings) == 1, f"{page_name}: verwacht exact één h1, gevonden {len(headings)}"
assert len(headings) == 1, f"{page_name}: verwacht exact één h1, gevonden {len(headings)}"
assert soup.html is not None
assert soup.html.get("lang"), f"{page_name}: <html> mist lang-attribuut"
@@ -53,7 +53,9 @@ def _assert_basic_html_accessibility(payload: bytes | str, *, page_name: str) ->
field_id = field.get("id")
has_label = bool(field.find_parent("label"))
has_label = has_label or bool(field.get("aria-label") or field.get("aria-labelledby"))
has_label = has_label or (field_id is not None and bool(soup.find("label", attrs={"for": field_id})))
has_label = has_label or (
field_id is not None and bool(soup.find("label", attrs={"for": field_id}))
)
assert has_label, (
f"{page_name}: formulierveld zonder label of aria-voorziening: "
@@ -141,9 +143,7 @@ def test_vr114_browser_flow(viewport, live_server, client, tmp_path, user, profi
if sync_playwright is None:
_run_html_accessibility_probe(client, user, profile, job)
pytest.skip(
"Playwright niet geïnstalleerd. Vult alleen de HTML/a11y-probe in."
)
pytest.skip("Playwright niet geïnstalleerd. Vult alleen de HTML/a11y-probe in.")
base = live_server.url
dashboard_url = f"{base}{reverse('dashboard:today')}"
@@ -227,9 +227,7 @@ def test_vr114_browser_flow(viewport, live_server, client, tmp_path, user, profi
page.get_by_role("link", name="Brongezondheid").click()
page.wait_for_url(sources_url)
page.locator("textarea[name='pasted_text']").fill(
"Vacaturetitel: Infrastructure Engineer\n"
"Werkgever: Example IT\n"
"Locatie: Hasselt"
"Vacaturetitel: Infrastructure Engineer\nWerkgever: Example IT\nLocatie: Hasselt"
)
page.get_by_role("button", name="Import uitvoeren").click()
page.wait_for_selector("text=Importresultaat")
+32 -11
View File
@@ -18,7 +18,9 @@ from apps.jobs.services.feedback import record_feedback
@pytest.mark.integration
@pytest.mark.django_db
def test_applied_feedback_creates_sanitized_application_snapshot_and_timeline(job, profile, source, user):
def test_applied_feedback_creates_sanitized_application_snapshot_and_timeline(
job, profile, source, user
):
JobSourceAlias.objects.create(
job=job,
source=source,
@@ -43,14 +45,20 @@ def test_applied_feedback_creates_sanitized_application_snapshot_and_timeline(jo
assert snapshot["sources"][0]["is_canonical"] is True
assert snapshot["scores"] is None
event_types = set(ApplicationTimelineEvent.objects.filter(application=application).values_list("event_type", flat=True))
event_types = set(
ApplicationTimelineEvent.objects.filter(application=application).values_list(
"event_type", flat=True
)
)
assert ApplicationTimelineEvent.EventType.CREATED in event_types
assert ApplicationTimelineEvent.EventType.SNAPSHOT_CAPTURED in event_types
@pytest.mark.integration
@pytest.mark.django_db
def test_application_update_creates_timeline_events_for_status_notes_and_contact(client, job, profile, source, user):
def test_application_update_creates_timeline_events_for_status_notes_and_contact(
client, job, profile, source, user
):
record_feedback(user=user, job=job, action="applied")
application = Application.objects.get(user=user, job=job)
@@ -71,7 +79,11 @@ def test_application_update_creates_timeline_events_for_status_notes_and_contact
assert application.contact_name == "Sofie Van Hecke"
assert application.contact_email == "recruiter@example.invalid"
event_types = set(ApplicationTimelineEvent.objects.filter(application=application).values_list("event_type", flat=True))
event_types = set(
ApplicationTimelineEvent.objects.filter(application=application).values_list(
"event_type", flat=True
)
)
assert ApplicationTimelineEvent.EventType.STATUS_CHANGED in event_types
assert ApplicationTimelineEvent.EventType.NOTES_UPDATED in event_types
assert ApplicationTimelineEvent.EventType.CONTACT_UPDATED in event_types
@@ -136,7 +148,10 @@ def test_application_export_generates_sanitized_zip_payload(client, job, source,
assert application_payload["snapshot"]["sources"][0]["url"] == job.canonical_url
csv_rows = list(csv.DictReader(io.StringIO(archive.read("timeline.csv").decode("utf-8"))))
assert {row["type"] for row in csv_rows} >= {ApplicationTimelineEvent.EventType.CREATED, ApplicationTimelineEvent.EventType.SNAPSHOT_CAPTURED}
assert {row["type"] for row in csv_rows} >= {
ApplicationTimelineEvent.EventType.CREATED,
ApplicationTimelineEvent.EventType.SNAPSHOT_CAPTURED,
}
html = archive.read("application_print.html").decode("utf-8")
assert "<html" in html.lower()
@@ -170,12 +185,18 @@ def test_application_export_and_delete_are_scoped_and_idempotent(client, job, us
)
client.force_login(user)
assert client.get(
reverse("jobs:application-export", kwargs={"pk": other_application.pk})
).status_code == 404
assert client.post(
reverse("jobs:application-delete", kwargs={"pk": other_application.pk})
).status_code == 404
assert (
client.get(
reverse("jobs:application-export", kwargs={"pk": other_application.pk})
).status_code
== 404
)
assert (
client.post(
reverse("jobs:application-delete", kwargs={"pk": other_application.pk})
).status_code
== 404
)
delete_response = client.post(
reverse("jobs:application-delete", kwargs={"pk": owner_application.pk}),
+11 -10
View File
@@ -2,13 +2,14 @@ from __future__ import annotations
from io import StringIO
import pytest
from django.core.management import call_command
from django.core.management.base import CommandError
import pytest
from apps.jobs.models import ScoreRun
from apps.jobs.services.scoring import score_and_save
@pytest.mark.django_db
def test_import_geodata_validate_only_and_import_command(tmp_path):
csv_path = tmp_path / "geodata.csv"
@@ -58,18 +59,18 @@ def test_import_geodata_rejects_half_license_metadata(tmp_path):
"postal_code,municipality,region,latitude,longitude\n9000,Gent,Vlaams-Brabant,51.05,3.73\n"
)
out = StringIO()
with pytest.raises(CommandError):
call_command(
"import_geodata",
str(csv_path),
"--source-name",
with pytest.raises(CommandError):
call_command(
"import_geodata",
str(csv_path),
"--source-name",
"local-test",
"--dataset-version",
"2026-01-01",
"--license-name",
"Test Dataset",
stdout=out,
)
"--license-name",
"Test Dataset",
stdout=out,
)
@pytest.mark.django_db
+23 -7
View File
@@ -34,6 +34,7 @@ def test_pipeline_is_idempotent_and_scores(source, profile):
assert ScoreRun.objects.filter(profile=profile).count() == 2
@pytest.mark.django_db
def test_pipeline_resolves_recruiter_alias_to_direct_employer():
source = Source.objects.create(
name="SmartRecruiters",
@@ -76,14 +77,32 @@ def test_pipeline_resolves_recruiter_alias_to_direct_employer():
{
"@context": "https://schema.org",
"@type": "JobPosting",
"identifier": {"@type": "PropertyValue", "name": "Example Public IT", "value": "VR-DEMO-SEC-001"},
"identifier": {
"@type": "PropertyValue",
"name": "Example Public IT",
"value": "VR-DEMO-SEC-001"
},
"title": "Security Engineer",
"description": "<p>Beveiliging, monitoring en hybride support.</p>",
"datePosted": "2026-07-20",
"validThrough": "2026-08-31T23:59:00+02:00",
"employmentType": ["FULL_TIME"],
"hiringOrganization": {"@type": "Organization", "name": "Example Public IT", "sameAs": "https://www.example.org"},
"jobLocation": {"@type": "Place", "address": {"@type": "PostalAddress", "streetAddress": "Voorbeeldstraat 1", "addressLocality": "Antwerpen", "addressRegion": "Antwerpen", "postalCode": "2000", "addressCountry": "BE"}},
"hiringOrganization": {
"@type": "Organization",
"name": "Example Public IT",
"sameAs": "https://www.example.org"
},
"jobLocation": {
"@type": "Place",
"address": {
"@type": "PostalAddress",
"streetAddress": "Voorbeeldstraat 1",
"addressLocality": "Antwerpen",
"addressRegion": "Antwerpen",
"postalCode": "2000",
"addressCountry": "BE"
}
},
"jobLocationType": "HYBRID",
"url": "/example/security-engineer"
}
@@ -114,7 +133,4 @@ def test_pipeline_resolves_recruiter_alias_to_direct_employer():
assert payload is not None
assert payload["reason"] == "resolved_direct_match"
assert payload["resolved_direct"] is True
assert (
payload["canonical_url"]
== "https://jobs.example.org/vacatures/security-engineer"
)
assert payload["canonical_url"] == "https://jobs.example.org/vacatures/security-engineer"
+1 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime, time
from zoneinfo import ZoneInfo
from decimal import Decimal
from zoneinfo import ZoneInfo
import pytest
from django.utils import timezone
+1 -2
View File
@@ -1,8 +1,7 @@
from datetime import timedelta
from django.utils import timezone
import pytest
from django.utils import timezone
from apps.sources.models import RawDocument, Source, SourceRun
from apps.sources.services.health import collect_source_health
@@ -1,11 +1,10 @@
from __future__ import annotations
import pytest
from django.core.cache import cache
from django.test import override_settings
from django.urls import reverse
import pytest
from apps.sources.services.fetcher import FetchedDocument
from apps.sources.services.manual_import import ManualImportError
+13 -3
View File
@@ -7,8 +7,8 @@ from django.utils import timezone
from apps.jobs.models import Feedback, ScoreRun
from apps.jobs.tasks import rescore_active_jobs, update_job_lifecycle
from apps.notifications.tasks import build_due_digests
from apps.sources.models import RawDocument, Source, SourcePolicyReview, SourceLease, SourceRun
from apps.sources.services.fetcher import RateLimitedError, FetchedDocument
from apps.sources.models import RawDocument, Source, SourceLease, SourcePolicyReview, SourceRun
from apps.sources.services.fetcher import FetchedDocument, RateLimitedError
from apps.sources.tasks import cleanup_raw_documents, fetch_source, schedule_due_sources
@@ -181,7 +181,17 @@ def test_sources_candidate_bulk_actions_and_evidence_view(client, user):
domain="jobs.example.org",
status=Source.Status.CANDIDATE,
policy=Source.Policy.REVIEW,
metadata={"discovery": [{"url": "https://jobs.example.org/careers", "discovered_from": "html", "reason": "career-link", "confidence": 0.9, "label": "Werken bij"}]},
metadata={
"discovery": [
{
"url": "https://jobs.example.org/careers",
"discovered_from": "html",
"reason": "career-link",
"confidence": 0.9,
"label": "Werken bij",
}
]
},
)
other = Source.objects.create(
+6 -1
View File
@@ -5,6 +5,7 @@ from django.utils import timezone
from apps.sources.models import Source, SourcePolicyReview
from apps.sources.services.policy import assess_url, create_policy_review, is_denied_domain
from apps.sources.services.robots import RobotsDecision
def test_platform_domains_are_denied_including_subdomains():
@@ -74,7 +75,11 @@ def test_policy_conflict_denies_even_when_allow_policy_set(db):
@pytest.mark.security
def test_trial_review_can_allow_fetch(db):
def test_trial_review_can_allow_fetch(db, monkeypatch):
monkeypatch.setattr(
"apps.sources.services.policy.assess_robots",
lambda *args, **kwargs: RobotsDecision(True, "Testrobots staan toegang toe"),
)
source = Source.objects.create(
name="Example jobs",
source_type=Source.Type.EMPLOYER,
+20 -3
View File
@@ -6,10 +6,24 @@ from django.utils import timezone
from apps.sources.models import Source, SourceRobotsCache
from apps.sources.services.robots import assess_robots
from apps.sources.services.url_security import ValidatedUrl
def _allow_test_dns(monkeypatch):
monkeypatch.setattr(
"apps.sources.services.robots.validate_public_url",
lambda url, **kwargs: ValidatedUrl(
url=url,
hostname="jobs.example.org",
port=443,
addresses=("93.184.216.34",),
),
)
@pytest.mark.security
def test_robots_uses_user_agent_matching_and_allows_default(db):
def test_robots_uses_user_agent_matching_and_allows_default(db, monkeypatch):
_allow_test_dns(monkeypatch)
source = Source.objects.create(
name="Example jobs",
source_type=Source.Type.EMPLOYER,
@@ -45,7 +59,8 @@ def test_robots_uses_user_agent_matching_and_allows_default(db):
@pytest.mark.security
def test_robots_cache_refreshes_after_ttl(db, settings):
def test_robots_cache_refreshes_after_ttl(db, settings, monkeypatch):
_allow_test_dns(monkeypatch)
source = Source.objects.create(
name="Example jobs",
source_type=Source.Type.EMPLOYER,
@@ -63,7 +78,9 @@ def test_robots_cache_refreshes_after_ttl(db, settings):
return httpx.Response(200, text="User-agent: *\nAllow: /")
client = httpx.Client(transport=httpx.MockTransport(handler))
assess_robots(source.base_url, source=source, user_agent="test-agent", client=client, now=timezone.now())
assess_robots(
source.base_url, source=source, user_agent="test-agent", client=client, now=timezone.now()
)
assert calls["count"] == 1
assess_robots(
source.base_url,
+10 -3
View File
@@ -9,10 +9,12 @@ from apps.jobs.models import AiAnalysisCache
from apps.jobs.services import ai as ai_service
from apps.jobs.services.ai import analyze_job_text
pytestmark = pytest.mark.django_db
def _evaluation_cases() -> list[dict[str, object]]:
path = Path(__file__).resolve().parents[1] / "fixtures" / "ai" / "evaluation_set.json"
return json.loads(path.read_text(encoding="utf-8"))["cases"]
path = Path(__file__).resolve().parents[2] / "fixtures" / "ai" / "evaluation_set.json"
return json.loads(path.read_text(encoding="utf-8-sig"))["cases"]
@pytest.mark.parametrize("case", _evaluation_cases(), ids=lambda case: case["id"])
@@ -31,6 +33,7 @@ def test_ai_evaluation_cases(monkeypatch, case):
return
if case["response_type"] == "timeout":
class FakeClient:
def __init__(self, *args, **kwargs):
pass
@@ -46,6 +49,7 @@ def test_ai_evaluation_cases(monkeypatch, case):
monkeypatch.setattr(httpx, "Client", FakeClient)
else:
class FakeClient:
def __init__(self, *args, **kwargs):
pass
@@ -112,7 +116,10 @@ def test_ai_cache_reuses_result(monkeypatch):
nonlocal calls
calls += 1
return _fake_response(
'{"summary_nl":"Gevalideerde IT-ops advertentie.","features":{"support_ratio":0.03,"consultancy_ratio":0.01,"travel_ratio":0.02,"seniority":"senior","evidence":["IT"]},"warnings":[]}'
'{"summary_nl":"Gevalideerde IT-ops advertentie.",'
'"features":{"support_ratio":0.03,"consultancy_ratio":0.01,'
'"travel_ratio":0.02,"seniority":"senior","evidence":["IT"]},'
'"warnings":[]}'
)
monkeypatch.setattr(httpx, "Client", FakeClient)
+54 -13
View File
@@ -1,4 +1,4 @@
from pathlib import Path
from pathlib import Path
from apps.sources.adapters.ats import (
GreenhouseAdapter,
@@ -20,19 +20,28 @@ def _assert_evidence_fields(job, *, expected_keys):
def test_greenhouse_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = GreenhouseAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/greenhouse-listing.json"), url="https://boards.greenhouse.io/example")
result = adapter.extract(
_load_fixture("fixtures/ats/greenhouse-listing.json"),
url="https://boards.greenhouse.io/example",
)
assert result.parser_key == "ats-greenhouse"
assert len(result.jobs) == 2
assert {job.external_id for job in result.jobs} == {"gh-open-1", "gh-open-2"}
assert all("closed" not in result.warnings for _ in [0])
assert result.confidence >= 0.89
_assert_evidence_fields(result.jobs[0], expected_keys={"external_id", "url", "location_text", "date_posted", "employment_types"})
_assert_evidence_fields(
result.jobs[0],
expected_keys={"external_id", "url", "location_text", "date_posted", "employment_types"},
)
def test_greenhouse_adapter_detail_supports_changed_markup():
adapter = GreenhouseAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/greenhouse-detail.json"), url="https://boards.greenhouse.io/example/jobs/senior-software-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/greenhouse-detail.json"),
url="https://boards.greenhouse.io/example/jobs/senior-software-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -40,12 +49,24 @@ def test_greenhouse_adapter_detail_supports_changed_markup():
assert job.external_id == "gh-open-1"
assert job.url == "https://boards.greenhouse.io/example/jobs/senior-software-engineer"
assert job.description_html
_assert_evidence_fields(job, expected_keys={"external_id", "url", "location_text", "date_posted", "valid_through", "employment_types"})
_assert_evidence_fields(
job,
expected_keys={
"external_id",
"url",
"location_text",
"date_posted",
"valid_through",
"employment_types",
},
)
def test_lever_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = LeverAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/lever-listing.json"), url="https://jobs.lever.co/example")
result = adapter.extract(
_load_fixture("fixtures/ats/lever-listing.json"), url="https://jobs.lever.co/example"
)
assert result.parser_key == "ats-lever"
assert len(result.jobs) == 2
@@ -55,7 +76,10 @@ def test_lever_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_lever_adapter_detail_supports_changed_markup():
adapter = LeverAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/lever-detail.json"), url="https://jobs.lever.co/example/platform-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/lever-detail.json"),
url="https://jobs.lever.co/example/platform-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -67,7 +91,9 @@ def test_lever_adapter_detail_supports_changed_markup():
def test_recruitee_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = RecruiteeAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/recruitee-listing.json"), url="https://acme.recruitee.com/o/" )
result = adapter.extract(
_load_fixture("fixtures/ats/recruitee-listing.json"), url="https://acme.recruitee.com/o/"
)
assert result.parser_key == "ats-recruitee"
assert len(result.jobs) == 2
@@ -76,7 +102,10 @@ def test_recruitee_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_recruitee_adapter_detail_supports_changed_markup():
adapter = RecruiteeAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/recruitee-detail.json"), url="https://acme.recruitee.com/o/system-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/recruitee-detail.json"),
url="https://acme.recruitee.com/o/system-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -87,7 +116,10 @@ def test_recruitee_adapter_detail_supports_changed_markup():
def test_smartrecruiters_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = SmartRecruitersAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/smartrecruiters-listing.json"), url="https://jobs.smartrecruiters.com/example")
result = adapter.extract(
_load_fixture("fixtures/ats/smartrecruiters-listing.json"),
url="https://jobs.smartrecruiters.com/example",
)
assert result.parser_key == "ats-smartrecruiters"
assert len(result.jobs) == 2
@@ -96,7 +128,10 @@ def test_smartrecruiters_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_smartrecruiters_adapter_detail_supports_changed_markup():
adapter = SmartRecruitersAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/smartrecruiters-detail.json"), url="https://jobs.smartrecruiters.com/example/security-engineer")
result = adapter.extract(
_load_fixture("fixtures/ats/smartrecruiters-detail.json"),
url="https://jobs.smartrecruiters.com/example/security-engineer",
)
assert len(result.jobs) == 1
job = result.jobs[0]
@@ -106,7 +141,10 @@ def test_smartrecruiters_adapter_detail_supports_changed_markup():
def test_workable_adapter_listing_extracts_open_jobs_and_skips_closed():
adapter = WorkableAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/workable-listing.json"), url="https://apply.workable.com/example")
result = adapter.extract(
_load_fixture("fixtures/ats/workable-listing.json"),
url="https://apply.workable.com/example",
)
assert result.parser_key == "ats-workable"
assert len(result.jobs) == 2
@@ -115,7 +153,10 @@ def test_workable_adapter_listing_extracts_open_jobs_and_skips_closed():
def test_workable_adapter_detail_supports_changed_markup():
adapter = WorkableAdapter()
result = adapter.extract(_load_fixture("fixtures/ats/workable-detail.json"), url="https://apply.workable.com/example/data-engineer/")
result = adapter.extract(
_load_fixture("fixtures/ats/workable-detail.json"),
url="https://apply.workable.com/example/data-engineer/",
)
assert len(result.jobs) == 1
job = result.jobs[0]
+12 -7
View File
@@ -38,12 +38,15 @@ def test_discovery_from_html_includes_jsonld_and_feed_links():
assert candidate_urls["https://example.org/werken-bij"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"] == "html-jsonld"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer/solliciteer"] == "html-jsonld"
assert candidate_urls["https://example.org/feed/jobs.xml"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"].startswith("html-jsonld")
assert any(
c.source_type == Source.Type.RSS and c.reason == "feed" for c in result
assert (
candidate_urls["https://jobs.example.org/vacatures/data-engineer/solliciteer"]
== "html-jsonld"
)
assert candidate_urls["https://example.org/feed/jobs.xml"] == "html"
assert candidate_urls["https://jobs.example.org/vacatures/data-engineer"].startswith(
"html-jsonld"
)
assert any(c.source_type == Source.Type.RSS and c.reason == "feed" for c in result)
assert any(
c.source_type == Source.Type.EMPLOYER and c.url == "https://example.org/werken-bij"
for c in result
@@ -69,14 +72,16 @@ def test_discovery_from_sitemap_prefers_candidates_and_filters_private_or_denied
assert len(index_candidates) == 1
assert index_candidates[0].url == "https://jobs.example.org/sitemap-jobs.xml"
assert len(urlset_candidates) >= 2
assert any(item.url == "https://jobs.example.org/vacatures/data-engineer" for item in urlset_candidates)
assert any(
item.url == "https://jobs.example.org/vacatures/data-engineer" for item in urlset_candidates
)
assert all("linkedin.com" not in item.url for item in index_candidates + urlset_candidates)
def test_discovery_from_email_extracts_employer_domain_root():
raw = Path("fixtures/emails/sample_alert.eml").read_bytes()
result = discover_from_email(raw)
assert [item.url for item in result] == ["https://jobs.example.org"]
assert [item.url for item in result] == ["https://jobs.example.org/"]
assert result[0].source_type == Source.Type.EMPLOYER
+1 -1
View File
@@ -10,7 +10,7 @@ from apps.jobs.services.normalization import CanonicalJobDraft
from apps.sources.models import Source
def _draft_for_test(job: JobPosting, *, **overrides) -> CanonicalJobDraft:
def _draft_for_test(job: JobPosting, **overrides) -> CanonicalJobDraft:
values = {
"source_url": "https://jobs.smartrecruiters.com/example/security-engineer",
"canonical_url": "https://jobs.smartrecruiters.com/example/security-engineer",
+3 -1
View File
@@ -27,7 +27,9 @@ def test_learning_skip_for_implicit_hide(profile, job, user):
def test_learning_marks_explicit_title_hide_as_nonlearning(profile, job, user):
feedback = record_feedback(user=user, job=job, action=Feedback.Action.HIDE, reason="Titel past niet bij mij")
feedback = record_feedback(
user=user, job=job, action=Feedback.Action.HIDE, reason="Titel past niet bij mij"
)
metadata = feedback.metadata.get("learning", {})
assert metadata["status"] == "queued"
assert metadata["reason_code"] == "non_learning_title"
+7 -3
View File
@@ -6,8 +6,8 @@ from apps.sources.services.fetcher import (
ContentRejectedError,
FetchError,
FetchTimeoutError,
RateLimitedError,
PolicyBlockedError,
RateLimitedError,
fetch_url,
)
from apps.sources.services.url_security import ValidatedUrl
@@ -79,7 +79,9 @@ def test_fetcher_rejects_dns_rebinding(monkeypatch):
def validate(url: str, **kwargs):
if "/final" in url:
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=("198.51.100.12",))
return ValidatedUrl(
url=url, hostname="example.org", port=443, addresses=("198.51.100.12",)
)
return ValidatedUrl(url=url, hostname="example.org", port=443, addresses=("93.184.216.34",))
monkeypatch.setattr("apps.sources.services.fetcher.validate_public_url", validate)
@@ -133,7 +135,9 @@ def test_fetcher_rejects_policy_content_size_and_http(monkeypatch):
client.close()
client = httpx.Client(
transport=httpx.MockTransport(lambda request: (_ for _ in ()).throw(httpx.TimeoutException("timeout")))
transport=httpx.MockTransport(
lambda request: (_ for _ in ()).throw(httpx.TimeoutException("timeout"))
)
)
with pytest.raises(FetchTimeoutError):
fetch_url("https://example.org/slow", client=client)
+1 -2
View File
@@ -127,8 +127,7 @@ def test_validate_csv_geodata_flags_duplicates_and_invalid_coordinates(tmp_path:
validate_csv_geodata(source)
source.write_text(
"postal_code,municipality,region,latitude,longitude\n"
"9000,Gent,Vlaams-Brabant,99,3.73\n"
"postal_code,municipality,region,latitude,longitude\n9000,Gent,Vlaams-Brabant,99,3.73\n"
)
with pytest.raises(CommandError, match="latitude buiten bereik"):
validate_csv_geodata(source)
+1 -3
View File
@@ -34,9 +34,7 @@ def test_manual_import_url_uses_fetch_and_succeeds(monkeypatch, user):
monkeypatch.setattr("apps.sources.services.manual_import.fetch_url", fake_fetch)
monkeypatch.setattr("apps.sources.services.manual_import.process_raw_document", fake_process)
summary = import_manual_source(
actor=user, source_url="https://jobs.example.org/jobs"
)
summary = import_manual_source(actor=user, source_url="https://jobs.example.org/jobs")
source = Source.objects.get(pk=summary.source_id)
assert summary.mode == "url"
+7 -5
View File
@@ -19,7 +19,11 @@ def test_source_lease_blocks_concurrent_workers_and_recovers_when_expired(source
assert acquire_source_lease(source_id=source.pk, worker_token="worker-b", now=now) is None
assert release_source_lease(source_id=source.pk, worker_token="worker-a", now=now)
lease_b = acquire_source_lease(source_id=source.pk, worker_token="worker-b", now=timezone.now())
lease_b = acquire_source_lease(
source_id=source.pk,
worker_token="worker-b",
now=now + timedelta(seconds=source.minimum_interval_seconds),
)
assert lease_b is not None
@@ -51,7 +55,7 @@ def test_source_lease_allows_origin_concurrency(db):
)
source_b = Source.objects.create(
name="Source B",
source_type=Source.Type.EMPLOYER,
source_type=Source.Type.RSS,
base_url="https://shared.example.org/careers/",
domain=shared_domain,
status=Source.Status.ACTIVE,
@@ -76,8 +80,6 @@ def test_failure_backoff_uses_retry_after_and_bounds():
policy=Source.Policy.ALLOW,
)
source.save()
backoff = calculate_failure_backoff_seconds(
source, failure_count=2, retry_after_seconds=120
)
backoff = calculate_failure_backoff_seconds(source, failure_count=2, retry_after_seconds=120)
assert backoff >= 120
assert backoff <= 3600 + 45
+17 -11
View File
@@ -4,10 +4,9 @@ from decimal import Decimal
import pytest
from apps.jobs.models import AiAnalysisCache
from apps.jobs.services.ai import AiAnalysis
from apps.jobs.models import Employer, JobPosting, ScoreRun
from apps.jobs.models import AiAnalysisCache, Employer, JobPosting, ScoreRun
from apps.jobs.services import scoring
from apps.jobs.services.ai import AiAnalysis
from apps.jobs.services.scoring import calculate_score
@@ -78,10 +77,12 @@ def test_distance_boundary_inclusief(matching_job, profile, monkeypatch):
def test_remote_job_heeft_geen_afstandsexclusie(matching_job, profile):
matching_job.workplace_type = JobPosting.Workplace.REMOTE
matching_job.postal_code = None
matching_job.municipality = None
matching_job.raw_location = None
matching_job.save(update_fields=["workplace_type", "postal_code", "municipality", "raw_location"])
matching_job.postal_code = ""
matching_job.municipality = ""
matching_job.raw_location = ""
matching_job.save(
update_fields=["workplace_type", "postal_code", "municipality", "raw_location"]
)
matching_job.latitude = None
matching_job.longitude = None
result = calculate_score(matching_job, profile)
@@ -90,12 +91,14 @@ def test_remote_job_heeft_geen_afstandsexclusie(matching_job, profile):
def test_onbekende_locatie_leidt_niet_tot_automatische_uitsluiting(matching_job, profile):
matching_job.postal_code = None
matching_job.municipality = None
matching_job.postal_code = ""
matching_job.municipality = ""
matching_job.raw_location = "onbekend"
matching_job.latitude = None
matching_job.longitude = None
matching_job.save(update_fields=["postal_code", "municipality", "raw_location", "latitude", "longitude"])
matching_job.save(
update_fields=["postal_code", "municipality", "raw_location", "latitude", "longitude"]
)
result = calculate_score(matching_job, profile)
assert result.evidence["distance_km"] is None
assert result.recommendation != ScoreRun.Recommendation.HIDDEN
@@ -162,7 +165,10 @@ def test_ai_score_influence_is_opt_in_and_bounded(matching_job, profile, monkeyp
)
result = calculate_score(matching_job, profile)
assert result.recommendation in {ScoreRun.Recommendation.STRONG, ScoreRun.Recommendation.POSSIBLE}
assert result.recommendation in {
ScoreRun.Recommendation.STRONG,
ScoreRun.Recommendation.POSSIBLE,
}
assert result.model_version == "local-model"
assert result.evidence["ai"]["status"] == AiAnalysisCache.Status.OK
assert result.evidence["ai"]["weight_applied"] == 20.0
+8 -1
View File
@@ -19,7 +19,14 @@ def test_validate_production_security_skips_when_debug_mode_is_enabled():
@pytest.mark.parametrize(
"secret_key, allowed_hosts, csrf_trusted_origins, session_cookie_secure, csrf_cookie_secure, secure_ssl_redirect",
(
"secret_key",
"allowed_hosts",
"csrf_trusted_origins",
"session_cookie_secure",
"csrf_cookie_secure",
"secure_ssl_redirect",
),
[
("", ["example.org"], ["https://example.org"], True, True, True),
("change-me", ["example.org"], ["https://example.org"], True, True, True),