Some checks failed
CI / skinny-install (aco) (push) Successful in 1m7s
CI / skinny-install (api) (push) Successful in 33s
CI / skinny-install (bcda) (push) Successful in 39s
CI / skinny-install (bib) (push) Successful in 38s
CI / skinny-install (bls) (push) Successful in 34s
CI / skinny-install (ccw) (push) Successful in 38s
CI / skinny-install (cli) (push) Successful in 34s
CI / skinny-install (cms) (push) Successful in 38s
CI / skinny-install (conf) (push) Successful in 35s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 35s
CI / skinny-install (pfs) (push) Successful in 42s
CI / skinny-install (rex) (push) Successful in 40s
CI / lint-test (push) Failing after 11m44s
Deploy / build-scan-report (push) Failing after 6m13s
80 new tests covering federalregister, email_ingest, regulations_gov, prisma/fetch (incl. deterministic altcha PoW vector), and zot/ops. All external deps mocked (httpx, imaplib, subprocess, pydo). No network calls in CI.
167 lines
5.0 KiB
Python
167 lines
5.0 KiB
Python
"""Tests for bib.email_ingest — IMAP → bib ingest."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import email.message
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from bib.email_ingest import (
|
||
Mailbox,
|
||
_date_iso,
|
||
_extract_body,
|
||
_safe_filename,
|
||
_sender_domain,
|
||
_slug,
|
||
ingest,
|
||
)
|
||
|
||
# ── Pure helpers ────────────────────<E29480><E29480>─────────────────────────────
|
||
|
||
|
||
class TestSlug:
|
||
def test_basic(self):
|
||
assert _slug("Hello World!") == "hello-world"
|
||
|
||
def test_truncate(self):
|
||
assert len(_slug("x" * 100)) <= 60
|
||
|
||
def test_empty(self):
|
||
assert _slug("") == ""
|
||
|
||
|
||
class TestSafeFilename:
|
||
def test_special_chars(self):
|
||
assert _safe_filename("file (1).pdf") == "file_1_.pdf"
|
||
|
||
def test_empty(self):
|
||
assert _safe_filename("") == "attachment"
|
||
|
||
|
||
class TestSenderDomain:
|
||
def test_angle_bracket(self):
|
||
assert _sender_domain("John <john@example.com>") == "example.com"
|
||
|
||
def test_bare(self):
|
||
assert _sender_domain("user@domain.org") == "domain.org"
|
||
|
||
def test_no_domain(self):
|
||
assert _sender_domain("no email here") == ""
|
||
|
||
|
||
class TestDateIso:
|
||
def test_rfc2822(self):
|
||
assert _date_iso("Wed, 15 Apr 2026 22:00:35 +0000") == "2026-04-15"
|
||
|
||
def test_empty(self):
|
||
assert _date_iso("") == ""
|
||
|
||
def test_garbage(self):
|
||
assert _date_iso("not a date") == ""
|
||
|
||
|
||
class TestExtractBody:
|
||
def test_plain_text(self):
|
||
msg = email.message.EmailMessage()
|
||
msg.set_content("Hello world")
|
||
assert _extract_body(msg) == "Hello world\n"
|
||
|
||
def test_html_fallback(self):
|
||
msg = email.message.EmailMessage()
|
||
msg["Content-Type"] = "multipart/alternative"
|
||
msg.add_alternative("<p>Hello</p>", subtype="html")
|
||
body = _extract_body(msg)
|
||
assert "Hello" in body
|
||
|
||
|
||
# ── ingest (mocked IMAP + Store) ─────────────────────────────────
|
||
|
||
|
||
def _make_raw_email(
|
||
subject="Test", sender="cms@example.gov", body="Hello", msg_id="<abc@test>"
|
||
):
|
||
msg = email.message.EmailMessage()
|
||
msg["Subject"] = subject
|
||
msg["From"] = sender
|
||
msg["Date"] = "Wed, 15 Apr 2026 10:00:00 +0000"
|
||
msg["Message-ID"] = msg_id
|
||
msg.set_content(body)
|
||
return msg.as_bytes()
|
||
|
||
|
||
class TestIngest:
|
||
@patch("bib.email_ingest.imaplib.IMAP4_SSL")
|
||
def test_processes_unseen(self, MockIMAP):
|
||
conn = MockIMAP.return_value
|
||
conn.login.return_value = ("OK", [])
|
||
conn.select.return_value = ("OK", [])
|
||
conn.uid.side_effect = [
|
||
("OK", [b"1 2"]), # SEARCH
|
||
(
|
||
"OK",
|
||
[(b"1 (RFC822 {100}", _make_raw_email(msg_id="<m1@test>"))],
|
||
), # FETCH 1
|
||
("OK", []), # STORE 1
|
||
(
|
||
"OK",
|
||
[
|
||
(
|
||
b"2 (RFC822 {100}",
|
||
_make_raw_email(subject="Second", msg_id="<m2@test>"),
|
||
)
|
||
],
|
||
), # FETCH 2
|
||
("OK", []), # STORE 2
|
||
]
|
||
conn.close.return_value = ("OK", [])
|
||
conn.logout.return_value = ("OK", [])
|
||
|
||
store = MagicMock()
|
||
store.upsert.return_value = "KEY1"
|
||
|
||
mb = Mailbox(
|
||
host="mail.test", port=993, username="test@test.com", password="pw"
|
||
)
|
||
stats = ingest(store, mb, scratch_root=Path("/tmp/test-ingest"))
|
||
assert stats["seen"] == 2
|
||
assert stats["ingested"] == 2
|
||
assert stats["errors"] == 0
|
||
assert store.upsert.call_count == 2
|
||
|
||
@patch("bib.email_ingest.imaplib.IMAP4_SSL")
|
||
def test_empty_inbox(self, MockIMAP):
|
||
conn = MockIMAP.return_value
|
||
conn.login.return_value = ("OK", [])
|
||
conn.select.return_value = ("OK", [])
|
||
conn.uid.return_value = ("OK", [b""])
|
||
conn.close.return_value = ("OK", [])
|
||
conn.logout.return_value = ("OK", [])
|
||
|
||
store = MagicMock()
|
||
mb = Mailbox(
|
||
host="mail.test", port=993, username="test@test.com", password="pw"
|
||
)
|
||
stats = ingest(store, mb)
|
||
assert stats["seen"] == 0
|
||
assert stats["ingested"] == 0
|
||
|
||
@patch("bib.email_ingest.imaplib.IMAP4_SSL")
|
||
def test_limit(self, MockIMAP):
|
||
conn = MockIMAP.return_value
|
||
conn.login.return_value = ("OK", [])
|
||
conn.select.return_value = ("OK", [])
|
||
conn.uid.side_effect = [
|
||
("OK", [b"1 2 3 4 5"]), # SEARCH returns 5
|
||
("OK", [(b"1 (RFC822 {100}", _make_raw_email(msg_id="<l1@test>"))]),
|
||
("OK", []),
|
||
]
|
||
conn.close.return_value = ("OK", [])
|
||
conn.logout.return_value = ("OK", [])
|
||
|
||
store = MagicMock()
|
||
store.upsert.return_value = "K"
|
||
mb = Mailbox(host="h", port=993, username="u@d", password="p")
|
||
stats = ingest(store, mb, limit=1)
|
||
assert stats["seen"] == 1
|
||
assert stats["ingested"] == 1
|