177 lines
6.1 KiB
Python
177 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from apps.jobs.models import Employer, JobPosting
|
|
from apps.jobs.services.employer_resolution import resolve_direct_employer_match
|
|
from apps.jobs.services.normalization import CanonicalJobDraft
|
|
from apps.sources.models import Source
|
|
|
|
|
|
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",
|
|
"external_id": "sr-security-123",
|
|
"title": job.original_title,
|
|
"normalized_title": job.normalized_title,
|
|
"job_family": job.job_family,
|
|
"employer_name": job.employer_name,
|
|
"employer_domain": "jobs.example.org",
|
|
"location_text": job.raw_location,
|
|
"region": job.region,
|
|
"municipality": job.municipality,
|
|
"postal_code": job.postal_code,
|
|
"country": job.country,
|
|
"workplace_type": job.workplace_type,
|
|
"employment_types": job.employment_types,
|
|
"language": "nl",
|
|
"description_html": "<p>Test</p>",
|
|
"description_text": "test beschrijving",
|
|
"date_posted": datetime(2026, 1, 10, tzinfo=UTC),
|
|
"valid_through": datetime(2026, 6, 1, tzinfo=UTC),
|
|
"compensation": {},
|
|
"skills_required": [],
|
|
"skills_preferred": [],
|
|
"content_hash": job.content_hash,
|
|
"canonical_key": "x" * 64,
|
|
"evidence": [],
|
|
"raw": {},
|
|
}
|
|
values.update(overrides)
|
|
return CanonicalJobDraft(**values)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_resolver_merges_recruiter_alias_to_direct_source():
|
|
employer = Employer.objects.create(
|
|
name="Example IT",
|
|
normalized_name="example it",
|
|
domain="jobs.example.org",
|
|
)
|
|
JobPosting.objects.create(
|
|
employer=employer,
|
|
original_title="Security Engineer",
|
|
normalized_title="security engineer",
|
|
canonical_url="https://jobs.example.org/vacatures/security-engineer",
|
|
canonical_key="a" * 64,
|
|
job_family="security",
|
|
content_hash="a" * 64,
|
|
description_text="Security engineer voor beveiliging.",
|
|
raw_location="Antwerpen",
|
|
region="Antwerpen",
|
|
municipality="Antwerpen",
|
|
workplace_type=JobPosting.Workplace.HYBRID,
|
|
employment_types=["full_time"],
|
|
direct_employer=True,
|
|
)
|
|
source = Source.objects.create(
|
|
name="SmartRecruiters",
|
|
source_type=Source.Type.ATS,
|
|
base_url="https://jobs.smartrecruiters.com/example",
|
|
domain="jobs.smartrecruiters.com",
|
|
status=Source.Status.ACTIVE,
|
|
policy=Source.Policy.ALLOW,
|
|
)
|
|
# Use an existing direct employer job as the resolver target.
|
|
target_job = JobPosting.objects.first()
|
|
decision = resolve_direct_employer_match(
|
|
_draft_for_test(target_job),
|
|
source=source,
|
|
)
|
|
assert decision.job == target_job
|
|
assert decision.reason == "resolved_direct_match"
|
|
assert decision.canonical_url == "https://jobs.example.org/vacatures/security-engineer"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_resolver_flags_conflicting_location_for_review():
|
|
employer = Employer.objects.create(
|
|
name="Example IT",
|
|
normalized_name="example it",
|
|
domain="jobs.example.org",
|
|
)
|
|
JobPosting.objects.create(
|
|
employer=employer,
|
|
original_title="Security Engineer",
|
|
normalized_title="security engineer",
|
|
canonical_url="https://jobs.example.org/vacatures/security-engineer",
|
|
canonical_key="b" * 64,
|
|
job_family="security",
|
|
content_hash="b" * 64,
|
|
description_text="Security engineer voor beveiliging.",
|
|
raw_location="Antwerpen",
|
|
region="Antwerpen",
|
|
municipality="Antwerpen",
|
|
workplace_type=JobPosting.Workplace.HYBRID,
|
|
employment_types=["full_time"],
|
|
direct_employer=True,
|
|
)
|
|
source = Source.objects.create(
|
|
name="SmartRecruiters",
|
|
source_type=Source.Type.ATS,
|
|
base_url="https://jobs.smartrecruiters.com/example",
|
|
domain="jobs.smartrecruiters.com",
|
|
status=Source.Status.ACTIVE,
|
|
policy=Source.Policy.ALLOW,
|
|
)
|
|
decision = resolve_direct_employer_match(
|
|
_draft_for_test(
|
|
JobPosting.objects.first(),
|
|
location_text="Gent",
|
|
municipality="Gent",
|
|
region="Oost-Vlaanderen",
|
|
),
|
|
source=source,
|
|
)
|
|
assert decision.job is None
|
|
assert decision.reason == "review_direct_conflict"
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_resolver_keeps_candidate_unmerged_when_similarity_is_conservative():
|
|
employer = Employer.objects.create(
|
|
name="Example IT",
|
|
normalized_name="example it",
|
|
domain="jobs.example.org",
|
|
)
|
|
JobPosting.objects.create(
|
|
employer=employer,
|
|
original_title="Security Engineer",
|
|
normalized_title="security engineer",
|
|
canonical_url="https://jobs.example.org/vacatures/security-engineer",
|
|
canonical_key="c" * 64,
|
|
job_family="security",
|
|
content_hash="c" * 64,
|
|
description_text="Security engineer voor beveiliging.",
|
|
raw_location="Antwerpen",
|
|
region="Antwerpen",
|
|
municipality="Antwerpen",
|
|
workplace_type=JobPosting.Workplace.HYBRID,
|
|
employment_types=["full_time"],
|
|
direct_employer=True,
|
|
)
|
|
source = Source.objects.create(
|
|
name="StaffingBoard",
|
|
source_type=Source.Type.ATS,
|
|
base_url="https://jobs.staffingboard.com/example",
|
|
domain="jobs.staffingboard.com",
|
|
status=Source.Status.ACTIVE,
|
|
policy=Source.Policy.ALLOW,
|
|
)
|
|
decision = resolve_direct_employer_match(
|
|
_draft_for_test(
|
|
JobPosting.objects.first(),
|
|
title="Data Analyst",
|
|
normalized_title="data analyst",
|
|
location_text="Brussel",
|
|
employer_name="Another Employer",
|
|
employer_domain="jobs.staffingboard.com",
|
|
),
|
|
source=source,
|
|
)
|
|
assert decision.job is None
|
|
assert decision.reason == "no_direct_resolution"
|