imap-migration-tools/test/core/test_imap_session.py
Javier Callico ac3307bf1d
Align names for scripts and commands (#40)
* Add comprehensive tests for IMAP email restoration functionality

- Implement tests for loading labels manifest, including both old and new formats.
- Add tests for extracting flags from the manifest.
- Create tests for parsing .eml files, including edge cases.
- Develop tests for retrieving .eml files from a directory.
- Validate configuration settings and handle missing credentials.
- Integrate tests for restoring emails from a backup, including Gmail mode and deletion of orphaned emails.
- Test progress caching and backup folder discovery.
- Ensure robust handling of email labels and flags during restoration.
- Verify that the restore process correctly interacts with a mock IMAP server.

* Add comprehensive tests for IMAP utilities and compression support

- Introduced tests for `imap_common.py` covering environment variable verification, IMAP connection handling, folder name normalization, MIME header decoding, message details extraction, duplicate detection, filename sanitization, and trash folder detection.
- Added tests for `imap_compress.py` to validate the functionality of the `_CompressedSocket` wrapper, ensuring data compression and decompression works as expected.
- Implemented integration tests to verify that compression is enabled during IMAP connection setup.
2026-02-15 08:39:37 -05:00

342 lines
14 KiB
Python

"""
Tests for imap_session.py
Tests cover:
- ensure_connection() behavior with and without OAuth2
- ensure_folder_session() connection change detection
- Folder selection and re-selection logic
- Error handling for connection and folder failures
"""
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../src")))
from core import imap_session
class TestBuildImapConf:
"""Tests for build_imap_conf function."""
def test_password_auth_returns_correct_dict(self):
"""Test builds correct config for password authentication."""
conf = imap_session.build_imap_conf("imap.example.com", "user@example.com", "pass123")
assert conf["host"] == "imap.example.com"
assert conf["user"] == "user@example.com"
assert conf["password"] == "pass123"
assert conf["oauth2_token"] is None
assert conf["oauth2"] is None
def test_oauth2_auth_acquires_token(self):
"""Test acquires token and builds oauth2 config when client_id is provided."""
with patch.object(
imap_session.imap_oauth2, "acquire_token", return_value=("my_token", "microsoft")
) as mock_acquire:
conf = imap_session.build_imap_conf(
"outlook.office365.com",
"user@example.com",
"pass",
client_id="cid",
client_secret="csecret",
label="source",
)
mock_acquire.assert_called_once_with("outlook.office365.com", "cid", "user@example.com", "csecret", "source")
assert conf["host"] == "outlook.office365.com"
assert conf["user"] == "user@example.com"
assert conf["password"] == "pass"
assert conf["oauth2_token"] == "my_token"
assert conf["oauth2"] == {
"provider": "microsoft",
"client_id": "cid",
"email": "user@example.com",
"client_secret": "csecret",
}
def test_no_client_id_skips_oauth2(self):
"""Test that oauth2 is None when client_id is not provided."""
with patch.object(imap_session.imap_oauth2, "acquire_token") as mock_acquire:
conf = imap_session.build_imap_conf("imap.example.com", "user", "pass")
mock_acquire.assert_not_called()
assert conf["oauth2"] is None
assert conf["oauth2_token"] is None
def test_empty_client_id_skips_oauth2(self):
"""Test that empty string client_id is treated as no OAuth2."""
with patch.object(imap_session.imap_oauth2, "acquire_token") as mock_acquire:
conf = imap_session.build_imap_conf("imap.example.com", "user", "pass", client_id="")
mock_acquire.assert_not_called()
assert conf["oauth2"] is None
def test_none_client_secret_passed_through(self):
"""Test that client_secret=None is correctly passed to acquire_token and stored."""
with patch.object(imap_session.imap_oauth2, "acquire_token", return_value=("tok", "microsoft")):
conf = imap_session.build_imap_conf("outlook.office365.com", "user@example.com", "pass", client_id="cid")
assert conf["oauth2"]["client_secret"] is None
def test_label_forwarded_to_acquire_token(self):
"""Test that label is passed through to acquire_token."""
with patch.object(imap_session.imap_oauth2, "acquire_token", return_value=("tok", "google")) as mock_acquire:
imap_session.build_imap_conf(
"imap.gmail.com", "user@gmail.com", "pass", client_id="cid", client_secret="sec", label="destination"
)
mock_acquire.assert_called_once_with("imap.gmail.com", "cid", "user@gmail.com", "sec", "destination")
class TestEnsureConnection:
"""Tests for ensure_connection function."""
def test_with_oauth2_calls_refresh(self):
"""Test that OAuth2 token refresh is called when oauth2 config is present."""
conf = {
"host": "imap.example.com",
"user": "user@example.com",
"password": "pass",
"oauth2_token": "old_token",
"oauth2": {
"provider": "microsoft",
"client_id": "client-id",
"email": "user@example.com",
},
}
mock_conn = MagicMock()
with patch.object(imap_session.imap_oauth2, "refresh_oauth2_token") as mock_refresh:
with patch.object(
imap_session.imap_common, "ensure_connection_from_conf", return_value=mock_conn
) as mock_ensure:
result = imap_session.ensure_connection(mock_conn, conf)
mock_refresh.assert_called_once_with(conf, "old_token")
mock_ensure.assert_called_once_with(mock_conn, conf)
assert result is mock_conn
def test_without_oauth2_skips_refresh(self):
"""Test that token refresh is skipped when oauth2 config is not present."""
conf = {
"host": "imap.example.com",
"user": "user@example.com",
"password": "pass",
"oauth2": None,
}
mock_conn = MagicMock()
with patch.object(imap_session.imap_oauth2, "refresh_oauth2_token") as mock_refresh:
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=mock_conn):
imap_session.ensure_connection(mock_conn, conf)
mock_refresh.assert_not_called()
def test_without_oauth2_key_skips_refresh(self):
"""Test that token refresh is skipped when oauth2 key is missing entirely."""
conf = {
"host": "imap.example.com",
"user": "user@example.com",
"password": "pass",
}
mock_conn = MagicMock()
with patch.object(imap_session.imap_oauth2, "refresh_oauth2_token") as mock_refresh:
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=mock_conn):
imap_session.ensure_connection(mock_conn, conf)
mock_refresh.assert_not_called()
def test_returns_connection_from_ensure_connection_from_conf(self):
"""Test returns the connection from ensure_connection_from_conf."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=new_conn):
result = imap_session.ensure_connection(old_conn, conf)
assert result is new_conn
def test_returns_none_when_connection_fails(self):
"""Test returns None when ensure_connection_from_conf fails."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=None):
result = imap_session.ensure_connection(MagicMock(), conf)
assert result is None
def test_with_none_connection(self):
"""Test works correctly when passed None as connection."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
new_conn = MagicMock()
with patch.object(
imap_session.imap_common, "ensure_connection_from_conf", return_value=new_conn
) as mock_ensure:
result = imap_session.ensure_connection(None, conf)
mock_ensure.assert_called_once_with(None, conf)
assert result is new_conn
class TestEnsureFolderSession:
"""Tests for ensure_folder_session function."""
def test_same_connection_no_folder_select(self):
"""Test that folder is not re-selected when connection stays the same."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
mock_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=mock_conn):
result_conn, success = imap_session.ensure_folder_session(mock_conn, conf, "INBOX", readonly=True)
assert result_conn is mock_conn
assert success is True
mock_conn.select.assert_not_called()
def test_new_connection_selects_folder(self):
"""Test that folder is selected when connection changes."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(old_conn, conf, "INBOX", readonly=True)
assert result_conn is new_conn
assert success is True
new_conn.select.assert_called_once_with('"INBOX"', readonly=True)
def test_new_connection_selects_folder_readwrite(self):
"""Test that folder is selected with readonly=False when specified."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(
old_conn, conf, "[Gmail]/All Mail", readonly=False
)
assert result_conn is new_conn
assert success is True
new_conn.select.assert_called_once_with('"[Gmail]/All Mail"', readonly=False)
def test_none_connection_returns_failure(self):
"""Test returns (None, False) when ensure_connection returns None."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
mock_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=None):
result_conn, success = imap_session.ensure_folder_session(mock_conn, conf, "INBOX", readonly=True)
assert result_conn is None
assert success is False
def test_folder_select_failure_returns_false(self):
"""Test returns (conn, False) when folder selection fails."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
new_conn.select.side_effect = Exception("Folder not found")
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(old_conn, conf, "NonExistent", readonly=True)
assert result_conn is new_conn
assert success is False
def test_none_old_connection_selects_folder(self):
"""Test that folder is selected when old connection was None."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
new_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(None, conf, "INBOX", readonly=True)
assert result_conn is new_conn
assert success is True
new_conn.select.assert_called_once_with('"INBOX"', readonly=True)
def test_folder_name_with_spaces(self):
"""Test folder selection with folder names containing spaces."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(old_conn, conf, "My Folder", readonly=True)
assert success is True
new_conn.select.assert_called_once_with('"My Folder"', readonly=True)
def test_folder_select_imap_error(self):
"""Test handles IMAP-specific errors during folder selection."""
conf = {"host": "imap.example.com", "user": "user", "password": "pass"}
old_conn = MagicMock()
new_conn = MagicMock()
new_conn.select.side_effect = Exception("IMAP error: NO SELECT failed")
with patch.object(imap_session, "ensure_connection", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(old_conn, conf, "INBOX", readonly=True)
assert result_conn is new_conn
assert success is False
class TestEnsureFolderSessionWithOAuth2:
"""Integration-style tests for ensure_folder_session with OAuth2."""
def test_oauth2_refresh_triggers_folder_reselect(self):
"""Test that OAuth2 token refresh (new connection) triggers folder reselection."""
conf = {
"host": "outlook.office365.com",
"user": "user@example.com",
"password": "oauth2_token_here",
"oauth2_token": "old_token",
"oauth2": {
"provider": "microsoft",
"client_id": "client-id",
"email": "user@example.com",
},
}
old_conn = MagicMock()
new_conn = MagicMock()
# Simulate token refresh causing a new connection
with patch.object(imap_session.imap_oauth2, "refresh_oauth2_token"):
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=new_conn):
result_conn, success = imap_session.ensure_folder_session(old_conn, conf, "INBOX", readonly=True)
assert result_conn is new_conn
assert success is True
new_conn.select.assert_called_once_with('"INBOX"', readonly=True)
def test_healthy_connection_no_reselect(self):
"""Test that healthy connection (same object) doesn't reselect folder."""
conf = {
"host": "outlook.office365.com",
"user": "user@example.com",
"password": "oauth2_token_here",
"oauth2_token": "valid_token",
"oauth2": {
"provider": "microsoft",
"client_id": "client-id",
"email": "user@example.com",
},
}
mock_conn = MagicMock()
# Simulate healthy connection (same object returned)
with patch.object(imap_session.imap_oauth2, "refresh_oauth2_token"):
with patch.object(imap_session.imap_common, "ensure_connection_from_conf", return_value=mock_conn):
result_conn, success = imap_session.ensure_folder_session(mock_conn, conf, "INBOX", readonly=True)
assert result_conn is mock_conn
assert success is True
mock_conn.select.assert_not_called()