"""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))