mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 15:01:59 +00:00
e5003a1d46
复核发现:GET /api/notify/channels 对使用者照样列全局、应用渠道(应用的通知里要挑渠道,只用名字和 id),而邮件渠道的 mail_to 给的是全部收件地址原文;channel_out 的说明却写着「搭建者以上才看得到」。Webhook 渠道只给主机名,邮件渠道多露了 一层:任何使用者都能拿到内部的收件名单。 做法:channel_out 加 recipients 参数,list_channels 按 _can_edit 的同一条规矩判(关登录时谁都能改,开了登录要搭建者 以上):改得了的照原样给 mail_to(改地址要整份重填),改不了的只给条数 mail_count。个人渠道照旧写发到自己的账号邮箱。 建、改渠道的返回本来就只给能改的人,照旧。说明同步改;前端 typesApp 的 NotifyChannelRow 加 mail_count(设置页的 通知只给使用者列个人渠道,界面上用不上,不加入口)。 测试(tests/test_notify_mail.py):开登录,使用者列渠道时全局、应用的邮件渠道照样在、没有 mail_to、只有条数,自己的个人 渠道照常;搭建者看得到地址。去掉修复回跑,这一项失败。
471 lines
24 KiB
Python
471 lines
24 KiB
Python
"""邮件:通知渠道的第五种格式(notify/mail.py,docs/design/next-round/04-对外集成与触达.md B2)。
|
||
|
||
全程用假的 SMTP 工厂(mail._smtp_factory),不连任何邮件服务器。守的是:
|
||
- 个人邮件渠道只发到拥有者在名录里的账号邮箱;账号没有邮箱时记一条失败的投递、写明原因;个人渠道不收地址;
|
||
- 全局、应用的邮件渠道填固定的收件地址(最多 20 个,格式不对的拒收);邮件服务没启用时不收新的邮件渠道;
|
||
- 4xx(临时错误)和连不上按 1/2/4 秒重试,5xx 不重试;报错里绝不出现口令;
|
||
- 邮件里只有标题、状态、链接,纯文本 + 转义过的 HTML,带 List-Unsubscribe;投递记录成功写「已交给邮件服务器」(不说已送达);
|
||
- 设置的 mail 一组:口令加密存、读的时候只说配没配;启用时要配全;发件人名称不许换行;只有管理员能改;
|
||
- 发测试邮件只发到管理员自己的账号邮箱:关登录、账号没有邮箱时 422 说清怎么办;
|
||
- 管理员开了「默认发邮件给待办处理人」:第一次派到待办的人自动有一个个人邮件渠道(默认订阅三项),本人删了不再建回来;
|
||
开关缺省关时不建。
|
||
|
||
人名、地址、主机都是虚构的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import smtplib
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
from sqlalchemy import delete, select
|
||
|
||
from app.core.crypto import encrypt
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import NotifyChannel, NotifyDelivery, Setting, User
|
||
from app.notify import mail, service
|
||
from app.notify.core import Message
|
||
|
||
PASSWORD = "Sm7p-Pa55word-Zq"
|
||
|
||
|
||
class FakeSMTP:
|
||
"""假的 SMTP 连接:记下登录、发出的信;box.failures 里排着的异常按顺序在 send_message 上抛。"""
|
||
|
||
def __init__(self, box: SimpleNamespace) -> None:
|
||
self.box = box
|
||
|
||
def starttls(self, context=None) -> None:
|
||
self.box.tls += 1
|
||
|
||
def login(self, username: str, password: str) -> None:
|
||
self.box.logins.append((username, password))
|
||
if self.box.auth_error:
|
||
# 有的服务器会把收到的内容回显在错误里:报错里也不能出现口令
|
||
raise smtplib.SMTPAuthenticationError(535, f"5.7.8 bad credentials {password}".encode())
|
||
|
||
def send_message(self, em, from_addr=None, to_addrs=None):
|
||
if self.box.failures:
|
||
raise self.box.failures.pop(0)
|
||
self.box.sent.append(SimpleNamespace(from_addr=from_addr, to=list(to_addrs or []), em=em))
|
||
return {}
|
||
|
||
def quit(self) -> None:
|
||
self.box.quits += 1
|
||
|
||
def close(self) -> None:
|
||
pass
|
||
|
||
|
||
@pytest.fixture
|
||
def smtp(monkeypatch):
|
||
box = SimpleNamespace(sent=[], logins=[], failures=[], connects=[], refuse=0, tls=0, quits=0, auth_error=False)
|
||
|
||
def factory(host, port, security, timeout):
|
||
box.connects.append((host, port, security, timeout))
|
||
if box.refuse:
|
||
box.refuse -= 1
|
||
raise ConnectionRefusedError("refused")
|
||
return FakeSMTP(box)
|
||
|
||
monkeypatch.setattr(mail, "_smtp_factory", factory)
|
||
monkeypatch.setattr(mail, "RETRY_DELAYS", (0, 0, 0))
|
||
return box
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
async def clean():
|
||
"""前后各清一遍:同一个进程里别的测试留下的渠道、投递记录不算进来。"""
|
||
await _clean()
|
||
yield
|
||
await service.drain()
|
||
await _clean()
|
||
|
||
|
||
async def _clean() -> None:
|
||
async with SessionLocal() as session:
|
||
await session.execute(delete(NotifyDelivery))
|
||
await session.execute(delete(NotifyChannel))
|
||
await session.execute(delete(Setting).where(Setting.key.in_([mail.GROUP, "notify", service.MAIL_AUTO_KEY])))
|
||
await session.execute(delete(User).where(User.username.like("mail-%")))
|
||
await session.commit()
|
||
from app.core import public_url
|
||
|
||
public_url.remember(None) # 发信时按库里的对外访问地址对齐了缓存:删了那一行,缓存也清掉
|
||
|
||
|
||
async def configure(**over) -> None:
|
||
value = mail.stored_value({"enabled": True, "host": "smtp.example.com", "port": 587, "security": "starttls",
|
||
"username": "agentlab", "password": PASSWORD, "from_addr": "agentlab@example.com",
|
||
"from_name": "AgentLab", **over})
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Setting, mail.GROUP)
|
||
if row is None:
|
||
session.add(Setting(key=mail.GROUP, value=value))
|
||
else:
|
||
row.value = value
|
||
notify = await session.get(Setting, "notify")
|
||
if notify is None:
|
||
session.add(Setting(key="notify", value={"public_base_url": "https://agentlab.example.com"}))
|
||
else:
|
||
# 别的测试留下的 notify 一组:对外访问地址换成这里要的(退订头按它拼)
|
||
notify.value = {**(notify.value or {}), "public_base_url": "https://agentlab.example.com"}
|
||
await session.commit()
|
||
|
||
|
||
async def person(name: str, email: str | None) -> User:
|
||
async with SessionLocal() as session:
|
||
user = User(username=f"mail-{name}", display_name=name, role="user", email=email)
|
||
session.add(user)
|
||
await session.commit()
|
||
await session.refresh(user)
|
||
return user
|
||
|
||
|
||
async def channel(scope: str, owner_id: str | None = None, to: str = mail.PERSONAL_PLACEHOLDER,
|
||
events: list[str] | None = None) -> NotifyChannel:
|
||
async with SessionLocal() as session:
|
||
row = NotifyChannel(name=f"邮件-{scope}", scope=scope, owner_id=owner_id, format="email", url=encrypt(to),
|
||
events=events or ["task.opened"])
|
||
session.add(row)
|
||
await session.commit()
|
||
await session.refresh(row)
|
||
return row
|
||
|
||
|
||
MESSAGE = {"event": "task.opened", "status": "pending", "title": "新的待办:审批 <b>付款</b>",
|
||
"lines": [("状态", "待处理"), ("所在", "应用「报销 & 付款」")],
|
||
"links": [("去处理", "https://agentlab.example.com/apps/inbox/t1")]}
|
||
|
||
|
||
async def _deliveries() -> list[NotifyDelivery]:
|
||
async with SessionLocal() as session:
|
||
return list((await session.execute(select(NotifyDelivery))).scalars())
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 发到谁
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_personal_mail_goes_to_the_account_email_only(smtp):
|
||
await configure()
|
||
ana = await person("ana", "ana@example.com")
|
||
row = await channel("user", ana.id)
|
||
sent = await service._push([row], MESSAGE, subject_kind="task", subject_id="t1", event="task.opened",
|
||
run_id=None, task_id="t1")
|
||
assert sent == 1
|
||
[mail_out] = smtp.sent
|
||
assert mail_out.to == ["ana@example.com"] and mail_out.from_addr == "agentlab@example.com"
|
||
assert smtp.logins == [("agentlab", PASSWORD)] and smtp.tls == 1
|
||
em = mail_out.em
|
||
assert em["Subject"] == "【AgentLab】新的待办:审批 <b>付款</b>"
|
||
assert em["List-Unsubscribe"] == "<https://agentlab.example.com/settings/notify>"
|
||
assert em["Auto-Submitted"] == "auto-generated"
|
||
text = em.get_body(("plain",)).get_content()
|
||
html = em.get_body(("html",)).get_content()
|
||
assert "状态:待处理" in text and "https://agentlab.example.com/apps/inbox/t1" in text
|
||
assert "<b>付款</b>" in html and "<b>付款</b>" not in html, "HTML 里的内容一律转义"
|
||
assert "报销 & 付款" in html
|
||
[delivery] = await _deliveries()
|
||
assert (delivery.status, delivery.error, delivery.http_status) == ("ok", None, None)
|
||
|
||
|
||
async def test_personal_mail_without_account_email_is_a_failed_delivery_with_the_reason(smtp):
|
||
await configure()
|
||
bo = await person("bo", None)
|
||
row = await channel("user", bo.id)
|
||
await service._push([row], MESSAGE, subject_kind="task", subject_id="t1", event="task.opened", run_id=None,
|
||
task_id="t1")
|
||
assert smtp.sent == [] and smtp.connects == [], "没有邮箱就不连服务器"
|
||
[delivery] = await _deliveries()
|
||
assert delivery.status == "failed" and delivery.error == mail.NO_EMAIL
|
||
assert "设置 → 身份" in delivery.error
|
||
|
||
|
||
async def test_global_mail_goes_to_the_fixed_addresses(smtp):
|
||
await configure()
|
||
row = await channel("global", to="ops@example.com, lead@example.com")
|
||
await service._push([row], MESSAGE, subject_kind="task", subject_id="t1", event="task.opened", run_id=None,
|
||
task_id="t1")
|
||
assert smtp.sent[0].to == ["ops@example.com", "lead@example.com"]
|
||
|
||
|
||
async def test_disabled_mail_service_records_why(smtp):
|
||
await configure(enabled=False)
|
||
row = await channel("global", to="ops@example.com")
|
||
await service._push([row], MESSAGE, subject_kind="task", subject_id="t1", event="task.opened", run_id=None,
|
||
task_id="t1")
|
||
[delivery] = await _deliveries()
|
||
assert delivery.status == "failed" and delivery.error == mail.NOT_ENABLED and smtp.connects == []
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 重试与报错
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def _cfg() -> mail.MailConfig:
|
||
return mail.config_of(mail.stored_value({"enabled": True, "host": "smtp.example.com", "port": 587,
|
||
"security": "starttls", "username": "agentlab", "password": PASSWORD,
|
||
"from_addr": "agentlab@example.com"}))
|
||
|
||
|
||
async def test_temporary_errors_and_refused_connections_are_retried(smtp):
|
||
smtp.failures = [smtplib.SMTPDataError(451, b"4.3.0 try again later")]
|
||
smtp.refuse = 1
|
||
out = await mail.send(_cfg(), ["ana@example.com"], Message(title="t"), label="测试")
|
||
assert out.ok is True and out.attempts == 3, "连不上一次、451 一次,第三次交给了邮件服务器"
|
||
assert len(smtp.sent) == 1
|
||
|
||
|
||
async def test_permanent_errors_are_not_retried(smtp):
|
||
smtp.failures = [smtplib.SMTPDataError(550, b"5.7.1 message rejected")]
|
||
out = await mail.send(_cfg(), ["ana@example.com"], Message(title="t"), label="测试")
|
||
assert out.ok is False and out.attempts == 1 and out.retryable is False
|
||
assert "550" in out.error and "smtp.example.com" in out.error
|
||
|
||
|
||
async def test_retries_stop_after_three(smtp):
|
||
smtp.refuse = 10
|
||
out = await mail.send(_cfg(), ["ana@example.com"], Message(title="t"), label="测试")
|
||
assert out.ok is False and out.attempts == 4 and len(smtp.connects) == 4
|
||
assert out.error == "无法连接邮件服务器 smtp.example.com:587"
|
||
|
||
|
||
async def test_errors_never_contain_the_password(smtp, caplog):
|
||
smtp.auth_error = True
|
||
out = await mail.send(_cfg(), ["ana@example.com"], Message(title="t"), label="测试")
|
||
assert out.ok is False and out.attempts == 1, "认证失败(5xx)不重试"
|
||
assert PASSWORD not in (out.error or "") and "535" in out.error
|
||
assert PASSWORD not in caplog.text
|
||
smtp.auth_error = False
|
||
smtp.failures = [smtplib.SMTPSenderRefused(553, f"sender {PASSWORD} refused".encode(), "agentlab@example.com")]
|
||
out = await mail.send(_cfg(), ["ana@example.com"], Message(title="t"), label="测试")
|
||
assert PASSWORD not in (out.error or "")
|
||
|
||
|
||
async def test_partially_refused_recipients_are_said_plainly(smtp, monkeypatch):
|
||
monkeypatch.setattr(FakeSMTP, "send_message", lambda self, em, from_addr=None, to_addrs=None: {"x@example.com": (550, b"no")})
|
||
out = await mail.send(_cfg(), ["ana@example.com", "x@example.com"], Message(title="t"), label="测试")
|
||
assert out.ok is True and out.error == "已交给邮件服务器,其中 1 个收件地址被拒收"
|
||
|
||
|
||
def test_line_breaks_in_titles_never_reach_the_headers():
|
||
em = mail.build(_cfg(), ["ana@example.com"], Message(title="新的待办:审批\r\nBcc: x@example.com"), unsubscribe=None)
|
||
assert em["Subject"] == "【AgentLab】新的待办:审批 Bcc: x@example.com" and em["Bcc"] is None
|
||
|
||
|
||
def test_ssl_security_uses_no_starttls(smtp):
|
||
cfg = mail.config_of(mail.stored_value({"enabled": True, "host": "smtp.example.com", "port": 465,
|
||
"security": "ssl", "from_addr": "agentlab@example.com"}))
|
||
out = mail._send_once(cfg, ["ana@example.com"], mail.build(cfg, ["ana@example.com"], Message(title="t"),
|
||
unsubscribe=None))
|
||
assert out.ok and smtp.tls == 0 and smtp.logins == [], "没填用户名不登录"
|
||
assert smtp.connects[0][:3] == ("smtp.example.com", 465, "ssl") and smtp.connects[0][3] == mail.TIMEOUT_S
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 渠道接口
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
from app.main import app
|
||
|
||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
async def test_mail_channels_need_the_mail_service_and_valid_addresses(client, smtp):
|
||
body = {"name": "运维邮件", "scope": "global", "format": "email", "url": "ops@example.com", "events": ["task.opened"]}
|
||
refused = await client.post("/api/notify/channels", json=body)
|
||
assert refused.status_code == 422 and refused.json()["detail"] == mail.NOT_ENABLED
|
||
await configure()
|
||
too_many = ", ".join(f"p{i}@example.com" for i in range(21))
|
||
r = await client.post("/api/notify/channels", json={**body, "url": too_many})
|
||
assert r.status_code == 422 and "最多 20 个" in r.json()["detail"]
|
||
r = await client.post("/api/notify/channels", json={**body, "url": "ops@example.com\nnot-an-address"})
|
||
assert r.status_code == 422 and "not-an-address" in r.json()["detail"]
|
||
made = await client.post("/api/notify/channels", json={**body, "url": "ops@example.com;lead@example.com"})
|
||
assert made.status_code == 201, made.text
|
||
row = made.json()
|
||
assert row["format"] == "email" and row["mail_to"] == ["ops@example.com", "lead@example.com"]
|
||
assert row["url_host"] == "" and row["signed"] is False
|
||
listed = (await client.get("/api/notify/channels")).json()
|
||
assert listed["mail"] == {"enabled": True, "account_email": None}, "关登录时没有账号邮箱"
|
||
tested = (await client.post(f"/api/notify/channels/{row['id']}/test")).json()
|
||
assert tested["ok"] is True and tested["message"].startswith("已交给邮件服务器")
|
||
assert "是否送达以邮件服务器为准" in tested["message"]
|
||
deliveries = (await client.get(f"/api/notify/deliveries?channel_id={row['id']}")).json()["items"]
|
||
assert [(d["event"], d["status"], d["format"]) for d in deliveries] == [("test", "ok", "email")]
|
||
# 只改订阅、不传地址:照旧
|
||
kept = await client.patch(f"/api/notify/channels/{row['id']}", json={"events": ["task.opened", "action.unknown"]})
|
||
assert kept.status_code == 200 and kept.json()["mail_to"] == ["ops@example.com", "lead@example.com"]
|
||
# 换成 Webhook 要填 Webhook 地址
|
||
back = await client.patch(f"/api/notify/channels/{row['id']}", json={"format": "generic"})
|
||
assert back.status_code == 422 and "Webhook 地址" in back.json()["detail"]
|
||
|
||
|
||
async def test_personal_mail_channel_takes_no_address(smtp):
|
||
from tests.fakes import auth_env
|
||
|
||
await configure()
|
||
await auth_env.reset()
|
||
try:
|
||
await auth_env.turn_on_local()
|
||
await auth_env.make_user("mail-uma", "user", email="uma@example.com")
|
||
uma = await auth_env.login("mail-uma")
|
||
r = await uma.post("/api/notify/channels", json={"name": "我的邮件", "scope": "user", "format": "email",
|
||
"url": "someone@example.com", "events": ["task.opened"]})
|
||
assert r.status_code == 422 and "账号邮箱" in r.json()["detail"], "个人渠道不能填任意地址(不当中继)"
|
||
r = await uma.post("/api/notify/channels", json={"name": "我的邮件", "scope": "user", "format": "email",
|
||
"events": ["task.opened", "task.digest"]})
|
||
assert r.status_code == 201, r.text
|
||
assert r.json()["mail_to"] == ["uma@example.com"]
|
||
listed = (await uma.get("/api/notify/channels")).json()
|
||
assert listed["mail"] == {"enabled": True, "account_email": "uma@example.com"}
|
||
tested = (await uma.post(f"/api/notify/channels/{r.json()['id']}/test")).json()
|
||
assert tested["ok"] is True and smtp.sent[-1].to == ["uma@example.com"]
|
||
finally:
|
||
await auth_env.reset()
|
||
|
||
|
||
async def test_users_listing_channels_do_not_get_global_recipients(smtp):
|
||
"""使用者列渠道照样看得到全局、应用渠道(应用的通知里要挑一个,只用名字和 id),但邮件渠道的收件地址只给能改它的人
|
||
(搭建者以上):只给条数,不给地址。Webhook 只给主机名,邮件地址不能比它多露(复核 L2)。"""
|
||
from tests.fakes import auth_env
|
||
|
||
await configure()
|
||
await auth_env.reset()
|
||
try:
|
||
await auth_env.turn_on_local()
|
||
await auth_env.make_user("mail-uma", "user", email="uma@example.com")
|
||
await auth_env.make_user("mail-bo", "builder")
|
||
everyone = "finance-lead@example.com, ops@example.com"
|
||
made = await channel("global", to=everyone)
|
||
await channel("app", to="team@example.com")
|
||
uma = await auth_env.login("mail-uma")
|
||
mine = await uma.post("/api/notify/channels", json={"name": "我的邮件", "scope": "user", "format": "email",
|
||
"events": ["task.opened"]})
|
||
assert mine.status_code == 201, mine.text
|
||
listed = (await uma.get("/api/notify/channels")).json()["items"]
|
||
by = {row["scope"]: row for row in listed}
|
||
assert set(by) == {"global", "app", "user"}, "全局、应用渠道照样列(应用的通知里要挑)"
|
||
assert "finance-lead" not in json.dumps(listed) and "team@" not in json.dumps(listed), "收件地址不给使用者"
|
||
assert "mail_to" not in by["global"] and by["global"]["mail_count"] == 2
|
||
assert "mail_to" not in by["app"] and by["app"]["mail_count"] == 1
|
||
assert by["user"]["mail_to"] == ["uma@example.com"], "自己的个人渠道照常写发到哪"
|
||
bo = await auth_env.login("mail-bo")
|
||
shown = {row["id"]: row for row in (await bo.get("/api/notify/channels")).json()["items"]}
|
||
assert shown[made.id]["mail_to"] == ["finance-lead@example.com", "ops@example.com"], "能改的人看得到,改地址要整份重填"
|
||
finally:
|
||
await auth_env.reset()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 设置
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_mail_settings_keep_the_password_encrypted_and_hidden(client):
|
||
put = await client.put("/api/settings", json={"values": {"mail": {
|
||
"enabled": True, "host": "smtp.example.com", "port": 465, "security": "ssl", "username": "agentlab",
|
||
"password": PASSWORD, "from_addr": "agentlab@example.com", "from_name": "流程助手"}}})
|
||
assert put.status_code == 200, put.text
|
||
shown = put.json()["mail"]
|
||
assert shown["password"] is None and shown["password_set"] is True and PASSWORD not in json.dumps(put.json())
|
||
async with SessionLocal() as session:
|
||
stored = (await session.get(Setting, "mail")).value
|
||
assert stored["password"] != PASSWORD and PASSWORD not in json.dumps(stored)
|
||
assert mail.config_of(stored).password == PASSWORD
|
||
# 整组回写(口令是 null、带着 password_set):口令照旧
|
||
again = await client.put("/api/settings", json={"values": {"mail": {**shown, "from_name": "AgentLab"}}})
|
||
assert again.status_code == 200 and again.json()["mail"]["password_set"] is True
|
||
async with SessionLocal() as session:
|
||
assert mail.config_of((await session.get(Setting, "mail")).value).password == PASSWORD
|
||
# 空串表示去掉
|
||
cleared = await client.put("/api/settings", json={"values": {"mail": {"password": ""}}})
|
||
assert cleared.json()["mail"]["password_set"] is False
|
||
assert cleared.json()["mail"]["host"] == "smtp.example.com", "只给一项的不冲掉别的"
|
||
|
||
|
||
@pytest.mark.parametrize("value, fragment", [
|
||
({"enabled": True}, "SMTP 服务器"),
|
||
({"enabled": True, "host": "smtp.example.com"}, "发件地址"),
|
||
({"host": "smtp://smtp.example.com"}, "主机名"),
|
||
({"port": 70000}, "端口"),
|
||
({"security": "tls"}, "加密方式"),
|
||
({"from_addr": "not-an-address"}, "发件地址"),
|
||
({"from_name": "AgentLab\r\nBcc: x@example.com"}, "换行"),
|
||
({"relay": True}, "不支持「relay」"),
|
||
])
|
||
async def test_mail_settings_are_validated(client, value, fragment):
|
||
put = await client.put("/api/settings", json={"values": {"mail": value}})
|
||
assert put.status_code == 422, put.text
|
||
assert fragment in put.json()["detail"]
|
||
|
||
|
||
async def test_mail_settings_are_admin_only_and_test_mail_goes_to_the_admin(smtp):
|
||
from tests.fakes import auth_env
|
||
|
||
await configure()
|
||
await auth_env.reset()
|
||
try:
|
||
async with auth_env.client() as off:
|
||
r = await off.post("/api/settings/mail/test")
|
||
assert r.status_code == 422 and r.json()["code"] == "mail_test_no_address", "关登录时没有账号邮箱"
|
||
root = await auth_env.turn_on_local()
|
||
r = await root.post("/api/settings/mail/test")
|
||
assert r.status_code == 422 and "没有邮箱地址" in r.json()["detail"]
|
||
async with SessionLocal() as session:
|
||
admin = (await session.execute(select(User).where(User.username == "root"))).scalar_one()
|
||
admin.email = "root@example.com"
|
||
await session.commit()
|
||
r = await root.post("/api/settings/mail/test")
|
||
assert r.status_code == 200 and r.json()["ok"] is True, r.text
|
||
assert "已交给邮件服务器" in r.json()["message"] and smtp.sent[-1].to == ["root@example.com"]
|
||
await auth_env.make_user("mail-bea", "builder")
|
||
bea = await auth_env.login("mail-bea")
|
||
assert (await bea.get("/api/settings")).json()["mail"]["password"] is None, "搭建者读得到,看不到口令"
|
||
assert (await bea.put("/api/settings", json={"values": {"mail": {"enabled": False}}})).status_code == 403
|
||
assert (await bea.post("/api/settings/mail/test")).status_code == 403
|
||
finally:
|
||
await auth_env.reset()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 默认发邮件给待办处理人
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
async def test_assignees_get_a_personal_mail_channel_only_when_the_admin_turns_it_on(smtp):
|
||
from tests.fakes import auth_env
|
||
|
||
await auth_env.reset()
|
||
try:
|
||
await auth_env.turn_on_local()
|
||
cy = await person("cy", "cy@example.com")
|
||
await configure()
|
||
assert await service.ensure_assignee_mail([cy.username]) == 0, "缺省关:不建"
|
||
await configure(default_for_assignees=True)
|
||
assert await service.ensure_assignee_mail([cy.username]) == 1
|
||
async with SessionLocal() as session:
|
||
[row] = (await session.execute(select(NotifyChannel).where(NotifyChannel.owner_id == cy.id))).scalars()
|
||
assert (row.scope, row.format, row.enabled) == ("user", "email", True)
|
||
assert row.events == ["task.opened", "action.unknown", "review.done"]
|
||
await session.delete(row)
|
||
await session.commit()
|
||
assert await service.ensure_assignee_mail([cy.username]) == 0, "本人删了就是不要:不再建回来"
|
||
finally:
|
||
await auth_env.reset()
|
||
|
||
|
||
async def test_assignee_mail_is_not_created_when_login_is_off(smtp):
|
||
dee = await person("dee", "dee@example.com")
|
||
await configure(default_for_assignees=True)
|
||
assert await service.ensure_assignee_mail([dee.username]) == 0, "关登录没有认证过的账号,个人渠道无从挂起"
|