AutoTeam-F / tests /unit /test_mail_addy_io.py
ZRainbow's picture
feat(round-12 S2): mail provider fallback chain + addy.io + simplelogin
5041525
Raw
History Blame Contribute Delete
8.04 kB
"""Addy.io provider 单元测试 — 全 mock requests,无真实 HTTP。"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from autoteam.mail import addy_io as mod
from autoteam.mail.fallback import MailProviderUnavailable
def _set_env(monkeypatch, base="https://addy.example.com", token="tok-abc", domain="example.com"):
monkeypatch.setenv("ADDY_IO_BASE_URL", base)
monkeypatch.setenv("ADDY_IO_TOKEN", token)
monkeypatch.setenv("ADDY_IO_DOMAIN", domain)
def _make_response(status: int, body: dict | None = None, text: str = "") -> MagicMock:
r = MagicMock()
r.status_code = status
r.text = text or ("" if body is None else "{}")
r.json = MagicMock(return_value=body if body is not None else {})
return r
def _make_client(monkeypatch) -> mod.AddyIoClient:
_set_env(monkeypatch)
client = mod.AddyIoClient()
client.session = MagicMock()
return client
def test_init_missing_env_raises_unavailable(monkeypatch):
monkeypatch.delenv("ADDY_IO_BASE_URL", raising=False)
monkeypatch.delenv("ADDY_IO_TOKEN", raising=False)
monkeypatch.delenv("ADDY_IO_DOMAIN", raising=False)
with pytest.raises(MailProviderUnavailable) as exc:
mod.AddyIoClient()
msg = str(exc.value)
assert "ADDY_IO_BASE_URL" in msg
assert "ADDY_IO_TOKEN" in msg
assert "ADDY_IO_DOMAIN" in msg
def test_init_partial_env_raises_unavailable(monkeypatch):
monkeypatch.setenv("ADDY_IO_BASE_URL", "https://addy.example.com")
monkeypatch.delenv("ADDY_IO_TOKEN", raising=False)
monkeypatch.delenv("ADDY_IO_DOMAIN", raising=False)
with pytest.raises(MailProviderUnavailable):
mod.AddyIoClient()
def test_init_strips_trailing_slash_and_at_sign(monkeypatch):
monkeypatch.setenv("ADDY_IO_BASE_URL", "https://addy.example.com/")
monkeypatch.setenv("ADDY_IO_TOKEN", "tok-abc")
monkeypatch.setenv("ADDY_IO_DOMAIN", "@example.com")
client = mod.AddyIoClient()
assert client.base_url == "https://addy.example.com"
assert client.domain == "example.com"
def test_headers_use_bearer_token_and_required_headers(monkeypatch):
client = _make_client(monkeypatch)
h = client._headers()
assert h["Authorization"] == "Bearer tok-abc"
assert h["Accept"] == "application/json"
assert h["X-Requested-With"] == "XMLHttpRequest"
assert h["Content-Type"] == "application/json"
def test_login_calls_aliases_endpoint(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(200, {"data": []})
token = client.login()
assert token == "tok-abc"
args, kw = client.session.request.call_args
assert args[0] == "GET"
assert "/api/v1/aliases" in args[1]
assert kw["params"] == {"page[size]": 1}
def test_login_auth_failure_raises(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(401, text="unauthorized")
with pytest.raises(Exception) as exc:
client.login()
assert "401" in str(exc.value)
def test_create_temp_email_with_prefix_uses_custom_format(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(
201,
{"data": {"id": "alias-123", "email": "myprefix@example.com"}},
)
aid, email = client.create_temp_email(prefix="myprefix")
assert aid == "alias-123"
assert email == "myprefix@example.com"
args, kw = client.session.request.call_args
assert args[0] == "POST"
body = kw["json"]
assert body["domain"] == "example.com"
assert body["format"] == mod.ALIAS_FORMAT_CUSTOM
assert body["local_part"] == "myprefix"
def test_create_temp_email_without_prefix_uses_uuid_format(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(
201,
{"data": {"id": "alias-xyz", "email": "abc123@example.com"}},
)
aid, email = client.create_temp_email()
assert aid == "alias-xyz"
assert email == "abc123@example.com"
body = client.session.request.call_args.kwargs["json"]
assert body["format"] == mod.ALIAS_FORMAT_UUID
assert "local_part" not in body
def test_create_temp_email_response_missing_fields_raises(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(201, {"data": {}})
with pytest.raises(Exception) as exc:
client.create_temp_email(prefix="x")
assert "缺 id/email" in str(exc.value) or "缺" in str(exc.value)
def test_list_accounts_normalizes_rows(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(
200,
{
"data": [
{"id": "a-1", "email": "one@example.com", "active": True, "description": "d1"},
{"id": "a-2", "email": "two@example.com", "active": False, "description": "d2"},
]
},
)
rows = client.list_accounts(size=10)
assert len(rows) == 2
assert rows[0]["accountId"] == "a-1"
assert rows[0]["email"] == "one@example.com"
assert rows[0]["active"] is True
assert rows[1]["active"] is False
def test_list_accounts_handles_missing_data_field(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(200, {})
rows = client.list_accounts(size=10)
assert rows == []
def test_delete_account_by_id(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(204)
res = client.delete_account("alias-123")
assert res["code"] == 200
args, _ = client.session.request.call_args
assert args[0] == "DELETE"
assert "/api/v1/aliases/alias-123" in args[1]
def test_delete_account_by_email_resolves_id(monkeypatch):
client = _make_client(monkeypatch)
# 第一次:list_accounts;第二次:DELETE
list_resp = _make_response(
200,
{"data": [{"id": "alias-x", "email": "match@example.com"}]},
)
del_resp = _make_response(204)
client.session.request.side_effect = [list_resp, del_resp]
res = client.delete_account("match@example.com")
assert res["code"] == 200
# 第二次调用(DELETE)的 path 应包含 alias-x
last_args = client.session.request.call_args_list[-1]
assert "/api/v1/aliases/alias-x" in last_args.args[1]
def test_delete_account_unknown_email_returns_404(monkeypatch):
client = _make_client(monkeypatch)
client.session.request.return_value = _make_response(200, {"data": []})
res = client.delete_account("none@example.com")
assert res["code"] == 404
def test_inbox_methods_return_empty_with_warning(monkeypatch, caplog):
client = _make_client(monkeypatch)
with caplog.at_level("WARNING", logger="autoteam.mail.addy_io"):
assert client.search_emails_by_recipient("foo@example.com") == []
assert client.list_emails("alias-x") == []
assert client.delete_emails_for("foo@example.com") == 0
msgs = " ".join(r.message for r in caplog.records)
assert "alias forwarding" in msgs.lower() or "inbox" in msgs.lower()
def test_sanitize_local_part_filters_special_chars():
assert mod.AddyIoClient._sanitize_local_part("hello@world!") == "helloworld"
out = mod.AddyIoClient._sanitize_local_part(None)
assert len(out) == 10 # uuid hex truncated
assert mod.AddyIoClient._sanitize_local_part("---") != ""
def test_implements_full_mail_provider_abc(monkeypatch):
"""ABC 接口完整性自检。"""
from autoteam.mail.base import MailProvider
client = _make_client(monkeypatch)
assert isinstance(client, MailProvider)
# 确保所有抽象方法都被覆盖
for name in (
"login",
"create_temp_email",
"list_accounts",
"delete_account",
"search_emails_by_recipient",
"list_emails",
"delete_emails_for",
):
assert callable(getattr(client, name))