@@ -0,0 +1,217 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
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.tasks import cleanup_raw_documents, fetch_source, schedule_due_sources
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_task_helpers(profile, job, source, monkeypatch):
|
||||
result = rescore_active_jobs(profile.pk)
|
||||
assert result["scores_created"] == 1
|
||||
assert ScoreRun.objects.filter(job=job, profile=profile).exists()
|
||||
assert update_job_lifecycle() == {"expired": 0, "uncertain": 0, "removed": 0}
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
"apps.sources.tasks.fetch_source.delay", lambda source_id: calls.append(source_id)
|
||||
)
|
||||
source.next_run_at = timezone.now() - timedelta(minutes=1)
|
||||
source.save(update_fields=["next_run_at"])
|
||||
assert schedule_due_sources()["scheduled"] == 1
|
||||
assert calls == [source.pk]
|
||||
|
||||
old = RawDocument.objects.create(
|
||||
source=source,
|
||||
kind=RawDocument.Kind.TEXT,
|
||||
content_hash="8" * 64,
|
||||
retain_until=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
assert cleanup_raw_documents()["deleted"] >= 1
|
||||
assert not RawDocument.objects.filter(pk=old.pk).exists()
|
||||
|
||||
source.policy = Source.Policy.DENY
|
||||
source.policy_reason = "test"
|
||||
source.save(update_fields=["policy", "policy_reason"])
|
||||
assert fetch_source(source.pk)["status"] == "skipped"
|
||||
assert SourceRun.objects.filter(source=source, status=SourceRun.Status.SKIPPED).exists()
|
||||
|
||||
|
||||
def test_fetch_source_skips_when_lease_blocked(db, source, monkeypatch):
|
||||
SourceLease.objects.create(
|
||||
source=source,
|
||||
token="active-worker",
|
||||
worker_id="active-worker",
|
||||
expires_at=timezone.now() + timedelta(minutes=2),
|
||||
)
|
||||
calls = {"count": 0}
|
||||
|
||||
def fake_fetch_url(*args, **kwargs):
|
||||
calls["count"] += 1
|
||||
return FetchedDocument(
|
||||
requested_url=source.base_url,
|
||||
final_url=source.base_url,
|
||||
status_code=200,
|
||||
headers={},
|
||||
content=b"Vacature",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("apps.sources.tasks.fetch_url", fake_fetch_url)
|
||||
assert fetch_source(source.pk)["status"] == "skipped"
|
||||
assert calls["count"] == 0
|
||||
assert SourceRun.objects.filter(source=source, status=SourceRun.Status.SKIPPED).exists()
|
||||
|
||||
|
||||
def test_fetch_source_handles_rate_limit_backoff(db, source, monkeypatch):
|
||||
called = {"count": 0}
|
||||
|
||||
def fake_fetch_url(*args, **kwargs):
|
||||
called["count"] += 1
|
||||
raise RateLimitedError(30)
|
||||
|
||||
monkeypatch.setattr("apps.sources.tasks.fetch_url", fake_fetch_url)
|
||||
result = fetch_source(source.pk)
|
||||
assert result["status"] == "rate_limited"
|
||||
assert called["count"] == 1
|
||||
source.refresh_from_db()
|
||||
assert source.next_run_at is not None
|
||||
assert SourceRun.objects.filter(source=source, status=SourceRun.Status.FAILED).exists()
|
||||
|
||||
|
||||
def test_fetch_source_resets_fail_state_on_304(db, source, monkeypatch):
|
||||
source.failure_count = 4
|
||||
source.save(update_fields=["failure_count"])
|
||||
|
||||
def fake_fetch_url(*args, **kwargs):
|
||||
return FetchedDocument(
|
||||
requested_url=source.base_url,
|
||||
final_url=source.base_url,
|
||||
status_code=304,
|
||||
headers={},
|
||||
content=b"",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("apps.sources.tasks.fetch_url", fake_fetch_url)
|
||||
assert fetch_source(source.pk)["status"] == "not_modified"
|
||||
source.refresh_from_db()
|
||||
assert source.failure_count == 0
|
||||
|
||||
|
||||
def test_fetch_source_is_blocked_by_conflicting_review(db):
|
||||
source = Source.objects.create(
|
||||
name="Blocked source",
|
||||
source_type=Source.Type.EMPLOYER,
|
||||
base_url="https://jobs.example.org/vacatures/",
|
||||
domain="jobs.example.org",
|
||||
status=Source.Status.ACTIVE,
|
||||
policy=Source.Policy.ALLOW,
|
||||
)
|
||||
SourcePolicyReview.objects.create(
|
||||
source=source,
|
||||
decision=SourcePolicyReview.Decision.DENY,
|
||||
reason="Review blokkeert deze bron",
|
||||
)
|
||||
result = fetch_source(source.pk)
|
||||
assert result["status"] == "skipped"
|
||||
assert SourceRun.objects.filter(source=source, status=SourceRun.Status.SKIPPED).exists()
|
||||
|
||||
|
||||
def test_fetch_source_is_blocked_by_expired_review(db):
|
||||
source = Source.objects.create(
|
||||
name="Expired review source",
|
||||
source_type=Source.Type.EMPLOYER,
|
||||
base_url="https://jobs.example.org/vacatures/",
|
||||
domain="jobs.example.org",
|
||||
status=Source.Status.ACTIVE,
|
||||
policy=Source.Policy.ALLOW,
|
||||
)
|
||||
SourcePolicyReview.objects.create(
|
||||
source=source,
|
||||
decision=SourcePolicyReview.Decision.ALLOW,
|
||||
reason="Review verlopen",
|
||||
expires_at=timezone.now() - timedelta(days=1),
|
||||
)
|
||||
result = fetch_source(source.pk)
|
||||
assert result["status"] == "skipped"
|
||||
assert SourceRun.objects.filter(source=source, status=SourceRun.Status.SKIPPED).exists()
|
||||
|
||||
# Outside the digest window this is a safe no-op.
|
||||
assert build_due_digests()["sent"] == 0
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.django_db
|
||||
def test_job_profile_source_and_application_views(client, user, profile, job, source):
|
||||
client.force_login(user)
|
||||
assert client.get(reverse("jobs:list"), {"q": "Infrastructure"}).status_code == 200
|
||||
assert client.get(reverse("jobs:detail", kwargs={"pk": job.pk})).status_code == 200
|
||||
response = client.post(
|
||||
reverse("jobs:feedback", kwargs={"pk": job.pk}),
|
||||
{"action": Feedback.Action.INTERESTING},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert Feedback.objects.filter(user=user, job=job).exists()
|
||||
|
||||
assert client.get(reverse("profiles:list")).status_code == 200
|
||||
assert client.get(reverse("profiles:edit", kwargs={"pk": profile.pk})).status_code == 200
|
||||
assert client.get(reverse("sources:list")).status_code == 200
|
||||
assert client.get(reverse("dashboard:system")).status_code == 200
|
||||
assert client.get(reverse("jobs:applications")).status_code == 200
|
||||
|
||||
source.policy = Source.Policy.DENY
|
||||
source.save(update_fields=["policy"])
|
||||
response = client.post(reverse("sources:retry", kwargs={"pk": source.pk}))
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.django_db
|
||||
def test_sources_candidate_bulk_actions_and_evidence_view(client, user):
|
||||
candidate = Source.objects.create(
|
||||
name="Discoverd candidate",
|
||||
source_type=Source.Type.EMPLOYER,
|
||||
base_url="https://jobs.example.org/vacatures/",
|
||||
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"}]},
|
||||
)
|
||||
|
||||
other = Source.objects.create(
|
||||
name="Another source",
|
||||
source_type=Source.Type.EMPLOYER,
|
||||
base_url="https://other.example.org/",
|
||||
domain="other.example.org",
|
||||
status=Source.Status.CANDIDATE,
|
||||
policy=Source.Policy.REVIEW,
|
||||
)
|
||||
|
||||
client.force_login(user)
|
||||
response = client.get(reverse("sources:list"))
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode("utf-8")
|
||||
assert "jobs.example.org/careers" in content
|
||||
|
||||
response = client.post(
|
||||
reverse("sources:bulk"),
|
||||
{"action": "approve_to_trial", "source_ids": [str(candidate.pk)]},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
candidate.refresh_from_db()
|
||||
assert candidate.status == Source.Status.TRIAL
|
||||
|
||||
response = client.post(
|
||||
reverse("sources:bulk"),
|
||||
{"action": "dismiss", "source_ids": [str(other.pk)]},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
other.refresh_from_db()
|
||||
assert other.status == Source.Status.DISABLED
|
||||
assert other.policy == Source.Policy.DENY
|
||||
Reference in New Issue
Block a user