"""Unit tests for Gmail bulk importer — no DB, no external services required.""" from __future__ import annotations import mailbox from datetime import datetime, timezone from pathlib import Path import pytest from gmail_bulk_import.importer import _message_id, _parse_date, run_import def _make_msg(headers: dict) -> mailbox.mboxMessage: msg = mailbox.mboxMessage() for k, v in headers.items(): msg[k] = v return msg def _make_mbox(path: Path, msgs: list[mailbox.mboxMessage]) -> None: mbox = mailbox.mbox(str(path), create=True) for msg in msgs: mbox.add(msg) mbox.flush() mbox.close() def _msg_with_id(mid: str = "") -> mailbox.mboxMessage: return _make_msg({ "From": "alice@example.com", "Message-ID": mid, "Date": "Tue, 10 Jun 2025 12:00:00 +0000", "Subject": "Hello", }) class TestMessageId: def test_uses_message_id_header(self): msg = _make_msg({"Message-ID": ""}) assert _message_id(msg) == "foo-123@example.com" def test_strips_angle_brackets(self): msg = _make_msg({"Message-ID": " "}) assert _message_id(msg) == "bar@baz.com" def test_fallback_to_sha256_when_missing(self): msg = _make_msg({"From": "a@b.com", "Subject": "no id"}) result = _message_id(msg) assert result.startswith("sha256-") assert len(result) == len("sha256-") + 32 def test_sha256_is_stable(self): msg = _make_msg({"From": "a@b.com", "Subject": "stable"}) assert _message_id(msg) == _message_id(msg) class TestParseDate: def test_parses_rfc2822(self): msg = _make_msg({"Date": "Tue, 10 Jun 2025 12:00:00 +0000"}) ts = _parse_date(msg) assert ts == datetime(2025, 6, 10, 12, 0, 0, tzinfo=timezone.utc) def test_result_is_utc_aware(self): msg = _make_msg({"Date": "Tue, 10 Jun 2025 12:00:00 +0000"}) assert _parse_date(msg).tzinfo is not None def test_falls_back_to_epoch_on_bad_date(self): msg = _make_msg({"Date": "not-a-date"}) assert _parse_date(msg) == datetime(1970, 1, 1, tzinfo=timezone.utc) def test_falls_back_to_epoch_when_missing(self): msg = _make_msg({"From": "a@b.com"}) assert _parse_date(msg) == datetime(1970, 1, 1, tzinfo=timezone.utc) def test_converts_offset_to_utc(self): msg = _make_msg({"Date": "Tue, 10 Jun 2025 14:00:00 +0200"}) ts = _parse_date(msg) assert ts == datetime(2025, 6, 10, 12, 0, 0, tzinfo=timezone.utc) class TestRunImport: async def test_dry_run_counts_without_writing(self, tmp_path): mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id(""), _msg_with_id("")]) stats = await run_import(mbox_path, archive, dry_run=True) assert stats["processed"] == 2 assert stats["imported"] == 2 assert not archive.exists() async def test_imports_eml_to_archive(self, tmp_path): mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id()]) stats = await run_import(mbox_path, archive) assert stats["imported"] == 1 assert stats["skipped"] == 0 assert stats["errors"] == 0 eml_files = list(archive.rglob("*.eml")) assert len(eml_files) == 1 async def test_eml_stored_under_gmail_source(self, tmp_path): mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id()]) await run_import(mbox_path, archive) eml_files = list(archive.rglob("*.eml")) assert eml_files[0].parts[len(archive.parts)] == "gmail" async def test_idempotent_second_run_skips(self, tmp_path): mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id()]) await run_import(mbox_path, archive) stats2 = await run_import(mbox_path, archive) assert stats2["imported"] == 0 assert stats2["skipped"] == 1 assert stats2["errors"] == 0 async def test_two_distinct_messages_both_imported(self, tmp_path): mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id(""), _msg_with_id("")]) stats = await run_import(mbox_path, archive) assert stats["imported"] == 2 assert stats["errors"] == 0 assert len(list(archive.rglob("*.eml"))) == 2 async def test_archive_only_mode_no_dsn(self, tmp_path): """Without a DSN, job runs archive-only (no DB connection attempted).""" mbox_path = tmp_path / "mail.mbox" archive = tmp_path / "archive" _make_mbox(mbox_path, [_msg_with_id()]) stats = await run_import(mbox_path, archive, dsn=None) assert stats["imported"] == 1 assert list(archive.rglob("*.eml"))