AutoTeam-F / tests /unit /test_maillab.py
ZRainbow's picture
fix(maillab): 修补 list_emails type=0 / list_accounts 翻页 / 删 phantom 字段 + 改教程
a11e8b5
Raw
History Blame Contribute Delete
10 kB
"""测试 maillab provider。
只测能离线复现的纯逻辑路径(字段映射、createTime 解析、accountId 解析、
auth header 形态)。涉及实际 e2e 的待验证项见 maillab.py 的 TODO(maillab-verify)。
"""
from __future__ import annotations
import pytest
from autoteam.mail import maillab as mod
def _make_client(monkeypatch):
monkeypatch.setenv("MAILLAB_API_URL", "http://example.com")
monkeypatch.setenv("MAILLAB_USERNAME", "admin@example.com")
monkeypatch.setenv("MAILLAB_PASSWORD", "secret")
client = mod.MaillabClient()
client.token = "fake.jwt.token" # 跳过 _ensure_login
return client
def test_auth_header_uses_raw_jwt_without_bearer_prefix(monkeypatch):
"""已现场验证:maillab security.js 不解析 Bearer 前缀,直接读 Authorization。"""
client = _make_client(monkeypatch)
headers = client._headers()
assert headers["Authorization"] == "fake.jwt.token"
assert "Bearer " not in headers["Authorization"]
def test_parse_create_time_iso_string():
# 2026-04-25 10:30:00 UTC = epoch 1777113000
assert mod._parse_create_time("2026-04-25 10:30:00") == 1777113000
# ISO 8601 with explicit Z
assert mod._parse_create_time("2026-04-25T10:30:00Z") == 1777113000
def test_parse_create_time_epoch_int_passthrough():
assert mod._parse_create_time(1761331200) == 1761331200
# 毫秒应自动除以 1000
assert mod._parse_create_time(1761331200000) == 1761331200
def test_parse_create_time_handles_none_and_empty():
assert mod._parse_create_time(None) is None
assert mod._parse_create_time("") is None
def test_normalize_mail_record_maps_maillab_fields(monkeypatch):
client = _make_client(monkeypatch)
row = {
"emailId": 411,
"accountId": 43,
"sendEmail": "noreply@tm.openai.com",
"name": "OpenAI",
"subject": "Your ChatGPT code is 654321",
"text": "Your ChatGPT code is 654321",
"content": "<p>Your ChatGPT code is 654321</p>",
"toEmail": "tmp-user@example.com",
"messageId": "<msg@x>",
"createTime": "2026-04-25 10:30:00",
}
out = client._normalize_mail_record(row)
assert out["emailId"] == 411
assert out["sendEmail"] == "noreply@tm.openai.com"
assert out["sender"] == "OpenAI"
assert out["subject"] == "Your ChatGPT code is 654321"
assert out["content"] == "<p>Your ChatGPT code is 654321</p>"
assert out["text"] == "Your ChatGPT code is 654321"
assert out["toEmail"] == "tmp-user@example.com"
assert out["accountEmail"] == "tmp-user@example.com"
assert out["receiveEmail"] == "tmp-user@example.com"
assert out["createTime"] == 1777113000
assert out["raw"] == row
def test_normalize_mail_record_falls_back_to_message_field(monkeypatch):
"""maillab 部分版本 HTML 在 message 字段而非 content,需要兜底。"""
client = _make_client(monkeypatch)
row = {
"emailId": 1,
"subject": "Hi",
"message": "<p>fallback html</p>",
"toEmail": "to@e.com",
"createTime": "2026-04-25T10:30:00",
}
out = client._normalize_mail_record(row)
assert out["content"] == "<p>fallback html</p>"
# text 字段缺失时,从 HTML 剥可见文本
assert "fallback html" in out["text"]
def test_search_emails_by_recipient_filters_by_to_email(monkeypatch):
client = _make_client(monkeypatch)
monkeypatch.setattr(client, "_resolve_account_id", lambda v: 43)
monkeypatch.setattr(client, "_resolve_account_email", lambda v: "tmp-user@example.com")
def fake_list(account_id, size=10):
return [
{
"emailId": 1,
"accountId": 43,
"toEmail": "tmp-user@example.com",
"subject": "match",
"text": "x",
"content": "x",
},
{
"emailId": 2,
"accountId": 43,
"toEmail": "someone-else@example.com",
"subject": "should be filtered",
"text": "y",
"content": "y",
},
]
monkeypatch.setattr(client, "list_emails", fake_list)
out = client.search_emails_by_recipient("tmp-user@example.com", size=10)
assert [e["emailId"] for e in out] == [1]
def test_create_temp_email_builds_full_email_address(monkeypatch):
client = _make_client(monkeypatch)
# 显式传 domain 参数,优先级最高,绕开 runtime_config
captured: dict = {}
def fake_post(path, body=None):
captured["path"] = path
captured["body"] = body
return {"code": 200, "data": {"accountId": 43, "email": body["email"]}}
monkeypatch.setattr(client, "_post", fake_post)
aid, email = client.create_temp_email(prefix="alice", domain="@example.com")
assert aid == 43
assert email == "alice@example.com"
assert captured["path"] == "/account/add"
assert captured["body"] == {"email": "alice@example.com"}
def test_create_temp_email_falls_back_to_uuid_prefix(monkeypatch):
client = _make_client(monkeypatch)
def fake_post(path, body=None):
return {"code": 200, "data": {"accountId": 99, "email": body["email"]}}
monkeypatch.setattr(client, "_post", fake_post)
aid, email = client.create_temp_email(prefix=None, domain="example.com")
assert aid == 99
# 自动生成 10 字符 hex 前缀
local, _, domain = email.partition("@")
assert len(local) == 10
assert domain == "example.com"
def test_login_requires_url_and_credentials(monkeypatch):
monkeypatch.delenv("MAILLAB_API_URL", raising=False)
monkeypatch.delenv("MAILLAB_USERNAME", raising=False)
monkeypatch.delenv("MAILLAB_PASSWORD", raising=False)
client = mod.MaillabClient()
with pytest.raises(Exception, match="MAILLAB_API_URL"):
client.login()
def test_login_posts_credentials_and_stores_token(monkeypatch):
client = _make_client(monkeypatch)
client.token = None # 强制走真实 login 路径
captured: dict = {}
def fake_post(path, body=None):
captured["path"] = path
captured["body"] = body
return {"code": 200, "data": {"token": "jwt-xyz"}}
monkeypatch.setattr(client, "_post", fake_post)
token = client.login()
assert token == "jwt-xyz"
assert client.token == "jwt-xyz"
assert captured["path"] == "/login"
assert captured["body"] == {"email": "admin@example.com", "password": "secret"}
def test_extract_verification_code_inherits_from_base(monkeypatch):
client = _make_client(monkeypatch)
code = client.extract_verification_code({"text": "Your ChatGPT code is 314159", "content": ""})
assert code == "314159"
def test_list_emails_passes_type_zero_to_avoid_empty_response(monkeypatch):
"""maillab service/email-service.js list() 把 type 当 IS NULL 处理时,所有 RECEIVE
类型(type=0)的邮件都会被过滤掉。这里强制断言我们传了 type=0 防止退化。"""
client = _make_client(monkeypatch)
monkeypatch.setattr(client, "_resolve_account_id", lambda v: 7)
monkeypatch.setattr(client, "_resolve_account_email", lambda v: "x@example.com")
captured: dict = {}
def fake_get(path, params=None):
captured["path"] = path
captured["params"] = params or {}
return {"code": 200, "data": {"list": []}}
monkeypatch.setattr(client, "_get", fake_get)
client.list_emails(7, size=10)
assert captured["params"]["type"] == 0
assert captured["params"]["accountId"] == 7
assert captured["params"]["size"] == 10
def test_list_accounts_paginates_past_server_size_cap(monkeypatch):
"""maillab account-service.js list() 服务端硬 cap 30 条。请求 size=200 必须循环翻页。"""
client = _make_client(monkeypatch)
pages = [
# page 1: 30 条
[{"accountId": i, "email": f"u{i}@e.com", "sort": 100 - i} for i in range(1, 31)],
# page 2: 15 条
[{"accountId": i, "email": f"u{i}@e.com", "sort": 70 - i} for i in range(31, 46)],
# page 3: 空
[],
]
calls = {"i": 0}
def fake_get(path, params=None):
idx = calls["i"]
calls["i"] += 1
rows = pages[idx] if idx < len(pages) else []
return {"code": 200, "data": rows}
monkeypatch.setattr(client, "_get", fake_get)
out = client.list_accounts(size=200)
assert len(out) == 45
def test_list_accounts_stops_paginating_when_size_satisfied(monkeypatch):
"""请求 size <= 30 时,只调用一次,绝不继续翻页消耗服务器额度。"""
client = _make_client(monkeypatch)
calls = {"i": 0}
def fake_get(path, params=None):
calls["i"] += 1
return {
"code": 200,
"data": [{"accountId": i, "email": f"u{i}@e.com", "sort": 100 - i} for i in range(1, 31)],
}
monkeypatch.setattr(client, "_get", fake_get)
out = client.list_accounts(size=10)
assert len(out) == 10
assert calls["i"] == 1
def test_list_accounts_omits_phantom_mailcount_sendcount_fields(monkeypatch):
"""entity/account.js 没有 mailCount/sendCount 列,实现里不应再暴露这两个永远为 None
的字段;改取真实字段 latestEmailTime / status / name。"""
client = _make_client(monkeypatch)
def fake_get(path, params=None):
return {
"code": 200,
"data": [
{
"accountId": 1,
"email": "u1@e.com",
"name": "alice",
"status": 0,
"latestEmailTime": "2026-04-25 10:00:00",
"sort": 1,
}
],
}
monkeypatch.setattr(client, "_get", fake_get)
out = client.list_accounts(size=5)
assert "mailCount" not in out[0]
assert "sendCount" not in out[0]
assert out[0]["name"] == "alice"
assert out[0]["status"] == 0
assert out[0]["latestEmailTime"] == 1777111200 # 2026-04-25 10:00:00 UTC