Files
AgentLab/backend/tests/test_settings_apps_auth.py
T
ThearchyHelios 86d2840e41 W3-d 邮件渠道、待办汇总、访问密钥到期提醒(后端)
邮件(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 工厂下的收件人、失败投递、重试、口令、
设置与权限、自动建渠道;汇总按时、只发一次、重启不重发;密钥两档、谁收、不该推的几种。
2026-10-16 16:21:41 +08:00

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, "个人应用的开关同样只有管理员能改"