Files
VacatureRadar/tests/integration/test_email_import_notifications.py
T
2026-07-22 05:12:07 +02:00

202 lines
7.1 KiB
Python

from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import pytest
from cryptography.fernet import Fernet
from django.core import mail
from django.test import override_settings
from apps.jobs.models import Feedback, ScoreRun
from apps.jobs.services.scoring import score_and_save
from apps.notifications.models import DigestOutbox
from apps.notifications.services import build_digest_payload, create_daily_outbox, send_digest
from apps.sources.models import EmailMessageRecord, MailboxConnection, RawDocument
from apps.sources.services.email_import import ingest_email, message_identity
from apps.sources.services.imap_import import poll_imap_mailbox
from apps.sources.services.mailbox_connections import encrypt_mailbox_password
@pytest.mark.django_db
def test_email_import_is_idempotent(profile):
raw = Path("fixtures/emails/sample_alert.eml").read_bytes()
identity = message_identity(raw)
first = ingest_email(raw)
second = ingest_email(raw)
assert first.pk == second.pk
assert first.message_id == identity
assert EmailMessageRecord.objects.count() == 1
assert RawDocument.objects.filter(kind=RawDocument.Kind.EMAIL).count() == 1
assert len(first.links) == 2
@pytest.mark.django_db
def test_vdab_alert_has_provider_provenance_and_replays_idempotently(profile):
raw = Path("fixtures/emails/vdab_job_alert.eml").read_bytes()
first = ingest_email(raw)
second = ingest_email(raw)
assert second.pk == first.pk
assert first.processed is True
assert first.raw_document.metadata["alert_provider"] == "vdab"
assert len(first.links) == 2
assert all("vdab.be/vindeenjob/vacatures/" in link for link in first.links)
@pytest.mark.django_db
@pytest.mark.parametrize(
("platform", "fixture", "expected_domain"),
(
(MailboxConnection.Platform.LINKEDIN, "linkedin_job_alert.eml", "linkedin.com"),
(MailboxConnection.Platform.ICTJOB, "ictjob_job_alert.eml", "ictjob.be"),
(MailboxConnection.Platform.JOBAT, "jobat_job_alert.eml", "jobat.be"),
(MailboxConnection.Platform.STEPSTONE, "stepstone_job_alert.eml", "stepstone.be"),
(MailboxConnection.Platform.CAREERJET, "careerjet_job_alert.eml", "careerjet.be"),
(MailboxConnection.Platform.RANDSTAD, "randstad_job_alert.eml", "randstad.be"),
(
MailboxConnection.Platform.ROBERTHALF,
"roberthalf_job_alert.eml",
"roberthalf.com",
),
),
)
def test_jobboard_mail_import_keeps_platform_provenance_and_rejects_external_links(
profile, platform, fixture, expected_domain
):
raw = (Path("fixtures/emails") / fixture).read_bytes()
record = ingest_email(raw, platform=platform)
assert record.processed is True
assert len(record.links) == 1
assert expected_domain in record.links[0]
assert "outside.example.invalid" not in record.links[0]
assert record.raw_document.metadata["alert_provider"] == platform
assert record.raw_document.source.name == f"{platform.label} vacaturemailbox"
@pytest.mark.django_db
def test_imap_poll_is_bounded_and_one_bad_message_does_not_abort(profile):
valid = Path("fixtures/emails/vdab_job_alert.eml").read_bytes()
oversized = b"x" * (len(valid) + 1)
class FakeImapClient:
def __init__(self, host, port, timeout=None):
self.host = host
self.port = port
self.timeout = timeout
self.logged_out = False
def login(self, user, password):
return "OK", []
def select(self, mailbox, readonly=True):
assert readonly is True
return "OK", []
def uid(self, command, *args):
if command == "search":
return "OK", [b"1 2 3"]
uid = args[0]
if uid == b"1":
return "OK", [(b"RFC822", valid)]
if uid == b"2":
return "OK", [(b"RFC822", oversized)]
return "NO", []
def logout(self):
self.logged_out = True
clients = []
def factory(host, port, timeout=None):
client = FakeImapClient(host, port, timeout=timeout)
clients.append(client)
return client
key = Fernet.generate_key().decode("ascii")
with override_settings(
MAILBOX_CREDENTIAL_KEYS=[key],
IMAP_MAX_MESSAGES_PER_POLL=3,
IMAP_MAX_MESSAGE_BYTES=len(valid),
):
connection = MailboxConnection.objects.create(
user=profile.user,
platform=MailboxConnection.Platform.VDAB,
provider=MailboxConnection.Provider.CUSTOM,
custom_host="imap.example.invalid",
username="radar@example.invalid",
encrypted_password=encrypt_mailbox_password("test-only"),
)
first = poll_imap_mailbox(
connection, client_factory=factory, host_validator=lambda url: url
)
second = poll_imap_mailbox(
connection, client_factory=factory, host_validator=lambda url: url
)
assert first == {
"status": "ok",
"imported": 1,
"duplicates": 0,
"skipped_oversized": 1,
"failed": 1,
}
assert second == {
"status": "ok",
"imported": 0,
"duplicates": 1,
"skipped_oversized": 1,
"failed": 1,
}
assert all(client.logged_out for client in clients)
record = EmailMessageRecord.objects.get(mailbox_connection=connection)
assert record.raw_document.metadata["alert_provider"] == "vdab"
assert record.raw_document.metadata["mailbox_connection_id"] == connection.pk
@pytest.mark.django_db
def test_digest_payload_outbox_deduplication_and_send(profile, job):
score = score_and_save(job, profile)
score.recommendation = ScoreRun.Recommendation.STRONG
score.score = 92
score.save(update_fields=["recommendation", "score"])
local_tz = ZoneInfo(profile.timezone)
due = datetime(2026, 7, 20, 7, 35, tzinfo=local_tz)
payload = build_digest_payload(profile, local_date=due.date())
assert payload["strong_count"] == 1
assert payload["scores"][0]["title"] == job.original_title
with override_settings(DIGEST_RECIPIENT="digest@example.invalid"):
first = create_daily_outbox(profile, now=due)
second = create_daily_outbox(profile, now=due)
assert first is not None
assert second.pk == first.pk
assert first.status == DigestOutbox.Status.PENDING
send_digest(first)
first.refresh_from_db()
assert first.status == DigestOutbox.Status.SENT
assert first.sent_at is not None
assert len(mail.outbox) == 1
assert "Infrastructure Engineer" in mail.outbox[0].body
@pytest.mark.django_db
def test_digest_skips_outside_window_and_hidden_jobs(profile, job):
score_and_save(job, profile)
Feedback.objects.create(
user=profile.user,
profile=profile,
job=job,
action=Feedback.Action.HIDE,
)
payload = build_digest_payload(profile)
assert payload["scores"] == []
local_tz = ZoneInfo(profile.timezone)
too_early = datetime(2026, 7, 20, 6, 0, tzinfo=local_tz)
assert create_daily_outbox(profile, now=too_early) is None