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