mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:51:57 +00:00
86d2840e41
邮件(04 细案 B2,集成-I4 / I5,改写应用平台 Q6): - notify/mail.py:标准库 smtplib 放进 asyncio.to_thread,单次超时 10 秒;4xx 和连不上按 1/2/4 秒重试,5xx 不重试; 报错只写主机名和服务器回话,口令一律抹掉;纯文本 + 转义过的 HTML,带 List-Unsubscribe 和 Auto-Submitted;测试可换假的 SMTP 工厂。模块说明写清改写 Q6 的理由:SMTP 是可选配置、退信不追踪照实说「已交给邮件服务器」 - 通知渠道多一种格式「邮件」(core.CHANNEL_FORMATS;定时任务的 Webhook 只会发 HTTP,FORMATS 不加,免得定时任务能选 邮件):个人邮件渠道不收地址,只发到名录里的账号邮箱,没有邮箱记一条失败投递并写明去「设置 → 身份」填;全局、应用的填 固定地址,最多 20 个;邮件服务没启用时不收新的邮件渠道;渠道列表另给 mail(启没启用、本人的账号邮箱),投递记录带格式 - 设置的 mail 一组(只有管理员能改):口令用 core/crypto 加密,读的时候只说配没配;启用时要配全;发件人名称、主机不许 换行;「发测试邮件」只发到管理员自己的账号邮箱,关登录、没有邮箱时 422 说清怎么办 - mail.default_for_assignees(缺省关):开了之后第一次派到待办的人自动有一个个人邮件渠道(订阅待办打开、动作结果未知、 审核有结果),本人删了不再建回来;关登录时不建 待办汇总与密钥到期(04 细案 B3): - notify/digest.py 接进调度循环的步骤列表:每天到 notify.digest_at(缺省 09:00,服务端时区)给订阅了 task.digest 的 个人渠道发汇总(条数、最早时限、最多 5 条节点名、去待办箱的链接,没有待办不发);按日期在 settings 表里插一行认领, 重启不重发;认领行和自动建邮件渠道的名单是通知自己记的账,设置接口不列不收 - key.expiring:到期前 7 天、1 天各推一次,个人密钥推本人的个人渠道、服务账号的推全局渠道,推之前记审计、按审计去重; 轮换下来的旧密钥、撤销了的、建的时候就在这一档里的不推 - 两个事件推送实现了,才在 notify/service.EVENTS 打开它们的订阅范围 设置的 notify 一组加 digest_at;只给一部分时补上存着的另几项;环境变量锁住对外访问地址时只核对那一项,汇总时间照常能改。 apps 一组加 share_links(分享链接的开关,缺省关,下一个提交用)。测试:假 SMTP 工厂下的收件人、失败投递、重试、口令、 设置与权限、自动建渠道;汇总按时、只发一次、重启不重发;密钥两档、谁收、不该推的几种。
90 lines
4.8 KiB
Python
90 lines
4.8 KiB
Python
"""设置接口的两处改动:应用一组(按需视图的每日上限,D-j;使用者能不能建个人应用,normal-user-apps N7)登记进默认设置;
|
|
登录设置不经 /api/settings 读写(D-l)。
|
|
|
|
- apps.on_demand_daily_limit 默认 200,只收 0 到 100,000 的整数,写错 422、一项都不落库;
|
|
- apps.user_personal_apps 默认关,只收开或关;
|
|
- auth 这一组(OIDC 客户端密钥、管理员映射)GET 不给,PUT 带着也不写——只经 /api/auth/settings 改;
|
|
- 改设置只给管理员(开登录时搭建者只读)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import delete
|
|
|
|
from app.api.settings import APPS_GROUP, apps_settings, apps_values
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import Setting
|
|
from tests.fakes import auth_env
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def clean():
|
|
await auth_env.reset()
|
|
async with SessionLocal() as session:
|
|
await session.execute(delete(Setting).where(Setting.key == APPS_GROUP))
|
|
await session.commit()
|
|
yield
|
|
async with SessionLocal() as session:
|
|
await session.execute(delete(Setting).where(Setting.key == APPS_GROUP))
|
|
await session.commit()
|
|
await auth_env.reset()
|
|
|
|
|
|
async def test_daily_limit_is_a_registered_setting_with_default_200():
|
|
async with auth_env.client() as c:
|
|
body = (await c.get("/api/settings")).json()
|
|
assert body[APPS_GROUP] == {"on_demand_daily_limit": 200, "user_personal_apps": False, "share_links": False}
|
|
put = await c.put("/api/settings", json={"values": {APPS_GROUP: {"on_demand_daily_limit": 30}}})
|
|
assert put.status_code == 200, put.text
|
|
assert put.json()[APPS_GROUP] == {"on_demand_daily_limit": 30, "user_personal_apps": False, "share_links": False}
|
|
assert (await c.get("/api/settings")).json()[APPS_GROUP] == {"on_demand_daily_limit": 30,
|
|
"user_personal_apps": False, "share_links": False}
|
|
async with SessionLocal() as session:
|
|
assert await apps_settings(session) == {"on_demand_daily_limit": 30, "user_personal_apps": False,
|
|
"share_links": False}
|
|
assert apps_values({"on_demand_daily_limit": "坏的", "user_personal_apps": "开", "share_links": 1}) == {
|
|
"on_demand_daily_limit": 200, "user_personal_apps": False, "share_links": False}, "存坏了的按默认"
|
|
|
|
|
|
@pytest.mark.parametrize("value, fragment", [
|
|
({"on_demand_daily_limit": -1}, "按需视图每日上限"),
|
|
({"on_demand_daily_limit": True}, "按需视图每日上限"),
|
|
({"on_demand_daily_limit": 1.5}, "按需视图每日上限"),
|
|
({"on_demand_daily_limit": 100_001}, "100,000"),
|
|
({"ttl": 3}, "不支持「ttl」"),
|
|
({"user_personal_apps": "yes"}, "使用者可以建个人应用"),
|
|
({"user_personal_apps": 1}, "只能是开或关"),
|
|
("200", "需要是一个对象"),
|
|
])
|
|
async def test_bad_daily_limits_are_refused(value, fragment):
|
|
async with auth_env.client() as c:
|
|
r = await c.put("/api/settings", json={"values": {APPS_GROUP: value, "ui": {"theme": "dark"}}})
|
|
assert r.status_code == 422 and fragment in r.json()["detail"], r.text
|
|
async with SessionLocal() as session:
|
|
assert await session.get(Setting, APPS_GROUP) is None, "一项都不落库"
|
|
|
|
|
|
async def test_auth_settings_are_not_read_or_written_here():
|
|
from app.auth import SETTING_KEY, load_settings
|
|
|
|
admin = await auth_env.turn_on_local()
|
|
async with SessionLocal() as session:
|
|
row = await session.get(Setting, SETTING_KEY)
|
|
before = dict(row.value)
|
|
body = (await admin.get("/api/settings")).json()
|
|
assert SETTING_KEY not in body, "登录设置不经这里读:里面有 OIDC 的客户端密钥"
|
|
r = await admin.put("/api/settings", json={"values": {SETTING_KEY: {"mode": "off"}, "ui": {"theme": "dark"}}})
|
|
assert r.status_code == 200 and SETTING_KEY not in r.json()
|
|
async with SessionLocal() as session:
|
|
assert (await session.get(Setting, SETTING_KEY)).value == before, "也不经这里写"
|
|
assert (await load_settings(force=True))["mode"] == "local"
|
|
|
|
await auth_env.make_user("bob", "builder")
|
|
bob = await auth_env.login("bob")
|
|
assert (await bob.get("/api/settings")).json()[APPS_GROUP] == {"on_demand_daily_limit": 200,
|
|
"user_personal_apps": False, "share_links": False}
|
|
assert (await bob.put("/api/settings", json={"values": {APPS_GROUP: {"on_demand_daily_limit": 1}}})
|
|
).status_code == 403, "设置页的应用一组只有管理员能改"
|
|
assert (await bob.put("/api/settings", json={"values": {APPS_GROUP: {"user_personal_apps": True}}})
|
|
).status_code == 403, "个人应用的开关同样只有管理员能改"
|