mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
fix(site): use explicit transactions for connectivity paths
This commit is contained in:
+72
-60
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
from typing import Any, List, Mapping, Tuple, Optional
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.site import Site
|
||||
@@ -285,79 +286,90 @@ class SiteOper(DbOper):
|
||||
更新站点图标
|
||||
"""
|
||||
icon_base64 = f"data:image/ico;base64,{icon_base64}" if icon_base64 else ""
|
||||
siteicon = self.get_icon_by_domain(domain)
|
||||
if not siteicon:
|
||||
self._stage_create(
|
||||
SiteIcon(name=name, domain=domain, url=icon_url, base64=icon_base64)
|
||||
)
|
||||
elif icon_base64:
|
||||
self._stage_update(siteicon, {
|
||||
"url": icon_url,
|
||||
"base64": icon_base64
|
||||
})
|
||||
|
||||
def write(db: Session) -> None:
|
||||
"""在同一同步事务中查询并更新站点图标。"""
|
||||
siteicon = SiteIcon.get_by_domain(db, domain)
|
||||
if not siteicon:
|
||||
db.add(SiteIcon(
|
||||
name=name,
|
||||
domain=domain,
|
||||
url=icon_url,
|
||||
base64=icon_base64,
|
||||
))
|
||||
elif icon_base64:
|
||||
siteicon.url = icon_url
|
||||
siteicon.base64 = icon_base64
|
||||
|
||||
self._execute_sync_write(write)
|
||||
return True
|
||||
|
||||
def success(self, domain: str, seconds: Optional[int] = None):
|
||||
"""
|
||||
站点访问成功
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
# 使用深复制确保 note 是全新的字典对象
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
def write(db: Session) -> None:
|
||||
"""在同一同步事务中读取并更新站点统计。"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(db, domain)
|
||||
if sta:
|
||||
# 使用深复制确保 note 是全新的字典对象
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
|
||||
avg_seconds = sum([v for v in note.values()]) // avg_times
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
|
||||
avg_seconds = sum([v for v in note.values()]) // avg_times
|
||||
|
||||
self._stage_update(sta, {
|
||||
"success": sta.success + 1,
|
||||
"seconds": avg_seconds or sta.seconds,
|
||||
"lst_state": 0,
|
||||
"lst_mod_date": lst_date,
|
||||
"note": note
|
||||
})
|
||||
else:
|
||||
note = {}
|
||||
if seconds is not None:
|
||||
note = {
|
||||
lst_date: seconds or 1
|
||||
}
|
||||
self._stage_create(SiteStatistic(
|
||||
domain=domain,
|
||||
success=1,
|
||||
fail=0,
|
||||
seconds=seconds or 1,
|
||||
lst_state=0,
|
||||
lst_mod_date=lst_date,
|
||||
note=note
|
||||
))
|
||||
for key, value in {
|
||||
"success": sta.success + 1,
|
||||
"seconds": avg_seconds or sta.seconds,
|
||||
"lst_state": 0,
|
||||
"lst_mod_date": lst_date,
|
||||
"note": note,
|
||||
}.items():
|
||||
setattr(sta, key, value)
|
||||
else:
|
||||
note = {}
|
||||
if seconds is not None:
|
||||
note = {lst_date: seconds or 1}
|
||||
db.add(SiteStatistic(
|
||||
domain=domain,
|
||||
success=1,
|
||||
fail=0,
|
||||
seconds=seconds or 1,
|
||||
lst_state=0,
|
||||
lst_mod_date=lst_date,
|
||||
note=note,
|
||||
))
|
||||
|
||||
self._execute_sync_write(write)
|
||||
|
||||
def fail(self, domain: str):
|
||||
"""
|
||||
站点访问失败
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
self._stage_update(sta, {
|
||||
"fail": sta.fail + 1,
|
||||
"lst_state": 1,
|
||||
"lst_mod_date": lst_date
|
||||
})
|
||||
else:
|
||||
self._stage_create(SiteStatistic(
|
||||
domain=domain,
|
||||
success=0,
|
||||
fail=1,
|
||||
lst_state=1,
|
||||
lst_mod_date=lst_date
|
||||
))
|
||||
def write(db: Session) -> None:
|
||||
"""在同一同步事务中读取并更新站点失败统计。"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(db, domain)
|
||||
if sta:
|
||||
sta.fail += 1
|
||||
sta.lst_state = 1
|
||||
sta.lst_mod_date = lst_date
|
||||
else:
|
||||
db.add(SiteStatistic(
|
||||
domain=domain,
|
||||
success=0,
|
||||
fail=1,
|
||||
lst_state=1,
|
||||
lst_mod_date=lst_date,
|
||||
))
|
||||
|
||||
self._execute_sync_write(write)
|
||||
|
||||
async def async_success(self, domain: str, seconds: Optional[int] = None):
|
||||
"""
|
||||
|
||||
@@ -130,6 +130,7 @@ from app.startup.subscription import (
|
||||
)
|
||||
from app.startup.chain_events import TransactionalChainDurableEventWriter
|
||||
from app.startup.download_failure import TransactionalDownloadFailureRepository
|
||||
from app.startup.site import TransactionalSiteRepository
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.transaction import TransactionalWriteRunner
|
||||
from app.startup.context import (
|
||||
@@ -685,7 +686,10 @@ async def init_modules() -> HostRuntime:
|
||||
workflow_execution = TransactionalWorkflowExecutionService(SessionFactory)
|
||||
configure_workflow_legacy_writer(workflow_execution)
|
||||
configure_chain_data_ports(
|
||||
site=lambda: SiteOper(),
|
||||
site=lambda: TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
subscribe=lambda: SubscribeOper(),
|
||||
workflow=lambda: WorkflowOper(),
|
||||
download_history=lambda: DownloadHistoryOper(),
|
||||
@@ -720,13 +724,19 @@ async def init_modules() -> HostRuntime:
|
||||
configure_passkey_service(PasskeyService(repository=PassKeyOper()))
|
||||
configure_transfer_history_provider(lambda: TransferHistoryOper())
|
||||
configure_site_query_service(SiteQueryService(repository=SiteOper()))
|
||||
configure_site_health_service(SiteHealthService(repository=SiteOper()))
|
||||
configure_site_health_service(SiteHealthService(repository=TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)))
|
||||
configure_workflow_query(WorkflowQueryService(repository=WorkflowOper()))
|
||||
configure_agent_data_ports(
|
||||
agent_chat=lambda: AgentChatOper(),
|
||||
agent_task=lambda: AgentTaskOper(),
|
||||
user=lambda: UserOper(),
|
||||
site=lambda: SiteOper(),
|
||||
site=lambda: TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
subscribe=lambda: SubscribeOper(),
|
||||
subscribe_history=lambda: SubscribeHistoryOper(),
|
||||
transfer_history=lambda: TransferHistoryOper(),
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""站点 Chain 端口的显式会话与事务适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalSiteRepository:
|
||||
"""为同步 Chain 站点端口和异步健康统计提供短生命周期会话。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def _read(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步会话中执行只读站点操作。"""
|
||||
with self._sync_session() as session:
|
||||
return operation(SiteOper(db=session))
|
||||
|
||||
def _write(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步 UoW 中执行站点写操作。"""
|
||||
with self._sync_session() as session:
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(SiteOper(db=session))
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_write(
|
||||
self,
|
||||
operation: Callable[[SiteOper], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独立异步 UoW 中执行站点写操作。"""
|
||||
async with self._async_session() as session:
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(SiteOper(db=session))
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_read(self, operation: Callable[[SiteOper], Awaitable[T]]) -> T:
|
||||
"""在独立异步会话中执行只读站点操作。"""
|
||||
async with self._async_session() as session:
|
||||
return await operation(SiteOper(db=session))
|
||||
|
||||
def add(self, **kwargs: Any) -> tuple[bool, str]:
|
||||
"""新增站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.add(**kwargs))
|
||||
|
||||
def get(self, site_id: int) -> Any:
|
||||
"""按 ID 查询站点。"""
|
||||
return self._read(lambda repository: repository.get(site_id))
|
||||
|
||||
def get_by_domain(self, domain: str) -> Any:
|
||||
"""按域名查询站点。"""
|
||||
return self._read(lambda repository: repository.get_by_domain(domain))
|
||||
|
||||
def get_domains_by_ids(self, ids: list[int]) -> list[str | None]:
|
||||
"""查询一组站点 ID 对应的域名。"""
|
||||
return self._read(lambda repository: repository.get_domains_by_ids(ids))
|
||||
|
||||
def list(self) -> list[Any]:
|
||||
"""查询全部站点。"""
|
||||
return self._read(lambda repository: repository.list())
|
||||
|
||||
async def async_get(self, site_id: int) -> Any:
|
||||
"""异步按 ID 查询站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_get(site_id))
|
||||
|
||||
async def async_get_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Any:
|
||||
"""异步按名称查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_name(name)
|
||||
)
|
||||
|
||||
async def async_list(self) -> list[Any]:
|
||||
"""异步查询全部站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_list())
|
||||
|
||||
async def async_list_order_by_pri(self) -> list[Any]:
|
||||
"""异步按优先级查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_list_order_by_pri()
|
||||
)
|
||||
|
||||
async def async_update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""异步更新站点并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_update(site_id, payload)
|
||||
)
|
||||
|
||||
async def async_get_userdata_by_domain(
|
||||
self,
|
||||
domain: str,
|
||||
workdate: str | None = None,
|
||||
) -> list[Any]:
|
||||
"""异步查询站点用户数据。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_userdata_by_domain(domain, workdate)
|
||||
)
|
||||
|
||||
def update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""更新站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.update(site_id, payload))
|
||||
|
||||
def update_cookie(self, domain: str, cookies: str) -> tuple[bool, str]:
|
||||
"""更新站点 Cookie 并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_cookie(domain, cookies)
|
||||
)
|
||||
|
||||
def update_rss(self, domain: str, rss: str) -> tuple[bool, str]:
|
||||
"""更新站点 RSS 地址并提交事务。"""
|
||||
return self._write(lambda repository: repository.update_rss(domain, rss))
|
||||
|
||||
def update_userdata(
|
||||
self,
|
||||
domain: str,
|
||||
name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> tuple[bool, str]:
|
||||
"""更新站点用户数据并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_userdata(domain, name, payload)
|
||||
)
|
||||
|
||||
def update_icon(
|
||||
self,
|
||||
name: str,
|
||||
domain: str,
|
||||
icon_url: str,
|
||||
icon_base64: str,
|
||||
) -> bool:
|
||||
"""更新站点图标并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_icon(
|
||||
name,
|
||||
domain,
|
||||
icon_url,
|
||||
icon_base64,
|
||||
)
|
||||
)
|
||||
|
||||
def success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""记录站点访问成功并提交事务。"""
|
||||
return self._write(lambda repository: repository.success(domain, seconds))
|
||||
|
||||
def fail(self, domain: str) -> Any:
|
||||
"""记录站点访问失败并提交事务。"""
|
||||
return self._write(lambda repository: repository.fail(domain))
|
||||
|
||||
async def async_success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""异步记录站点访问成功并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_success(domain, seconds)
|
||||
)
|
||||
|
||||
async def async_fail(self, domain: str) -> Any:
|
||||
"""异步记录站点访问失败并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_fail(domain)
|
||||
)
|
||||
+15
-4
@@ -148,6 +148,8 @@ def configure_plugin_system_services():
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.startup.subscription import TransactionalSubscribeWriter
|
||||
from app.startup.download_failure import TransactionalDownloadFailureRepository
|
||||
from app.startup.site import TransactionalSiteRepository
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.transaction import TransactionalWriteRunner
|
||||
|
||||
@@ -202,15 +204,24 @@ def configure_plugin_system_services():
|
||||
)
|
||||
)
|
||||
|
||||
def site_repository() -> TransactionalSiteRepository:
|
||||
"""按生产组合根方式创建显式事务站点仓储。"""
|
||||
return TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
|
||||
configure_chain_data_ports(
|
||||
site=lambda: SiteOper(),
|
||||
site=site_repository,
|
||||
subscribe=lambda: SubscribeOper(),
|
||||
workflow=lambda: WorkflowOper(),
|
||||
download_history=lambda: DownloadHistoryOper(),
|
||||
transfer_history=lambda: TransferHistoryOper(),
|
||||
transfer_pending=lambda: TransferPendingOper(),
|
||||
media_server=lambda: MediaServerOper(),
|
||||
download_failure=lambda: DownloadFailureOper(),
|
||||
download_failure=lambda: TransactionalDownloadFailureRepository(
|
||||
SessionFactory
|
||||
),
|
||||
user=lambda: UserOper(),
|
||||
)
|
||||
configure_chain_runtime_context_provider(lambda: ChainRuntimeContext(
|
||||
@@ -228,7 +239,7 @@ def configure_plugin_system_services():
|
||||
configuration=build_chain_runtime_config(settings),
|
||||
))
|
||||
configure_site_query_service(SiteQueryService(repository=SiteOper()))
|
||||
configure_site_health_service(SiteHealthService(repository=SiteOper()))
|
||||
configure_site_health_service(SiteHealthService(repository=site_repository()))
|
||||
configure_workflow_query(WorkflowQueryService(repository=WorkflowOper()))
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.db.oper.plugindata import PluginDataOper
|
||||
@@ -236,7 +247,7 @@ def configure_plugin_system_services():
|
||||
agent_chat=lambda: AgentChatOper(),
|
||||
agent_task=lambda: AgentTaskOper(),
|
||||
user=lambda: UserOper(),
|
||||
site=lambda: SiteOper(),
|
||||
site=site_repository,
|
||||
subscribe=lambda: SubscribeOper(),
|
||||
subscribe_history=lambda: SubscribeHistoryOper(),
|
||||
transfer_history=lambda: TransferHistoryOper(),
|
||||
|
||||
+9
-3
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6429,
|
||||
"edge_sha256": "664681a500ba1c3d273568829b1b860e4dae7fe7a6c053c566583341fee7f903",
|
||||
"edge_count": 6434,
|
||||
"edge_sha256": "5c47cae41f5d90e1030757d0cb47db8eed373424dda5db17c900271e1d03a9c8",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -6168,6 +6168,7 @@
|
||||
"app.startup.modules_initializer -> app.startup.download_failure",
|
||||
"app.startup.modules_initializer -> app.startup.managed_resources_initializer",
|
||||
"app.startup.modules_initializer -> app.startup.outbox",
|
||||
"app.startup.modules_initializer -> app.startup.site",
|
||||
"app.startup.modules_initializer -> app.startup.subscription",
|
||||
"app.startup.modules_initializer -> app.startup.transaction",
|
||||
"app.startup.modules_initializer -> app.startup.workflow",
|
||||
@@ -6228,6 +6229,10 @@
|
||||
"app.startup.scheduler_initializer -> app.application",
|
||||
"app.startup.scheduler_initializer -> app.application.scheduling",
|
||||
"app.startup.scheduler_initializer -> app.scheduler",
|
||||
"app.startup.site -> app.db",
|
||||
"app.startup.site -> app.db.oper",
|
||||
"app.startup.site -> app.db.oper.site",
|
||||
"app.startup.site -> app.db.uow",
|
||||
"app.startup.subscription -> app.adapters",
|
||||
"app.startup.subscription -> app.adapters.external",
|
||||
"app.startup.subscription -> app.adapters.external.server",
|
||||
@@ -6446,7 +6451,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 797,
|
||||
"module_count": 798,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -7219,6 +7224,7 @@
|
||||
"app.startup.plugins_initializer",
|
||||
"app.startup.routers_initializer",
|
||||
"app.startup.scheduler_initializer",
|
||||
"app.startup.site",
|
||||
"app.startup.subscription",
|
||||
"app.startup.transaction",
|
||||
"app.startup.transfer_initializer",
|
||||
|
||||
@@ -204,6 +204,22 @@ def test_site_oper_update_icon_creates_then_only_overwrites_with_content(db):
|
||||
assert first.startswith("data:image/ico;base64,")
|
||||
|
||||
|
||||
def test_site_oper_icon_without_explicit_session_uses_transaction_runner(db):
|
||||
"""无显式会话的图标写入应由兼容事务执行器完成提交。"""
|
||||
oper = SiteOper()
|
||||
|
||||
oper.update_icon(
|
||||
"兼容站点",
|
||||
"op-icon-no-session.test",
|
||||
"https://op-icon-no-session.test/favicon.ico",
|
||||
"AAA",
|
||||
)
|
||||
|
||||
icon = SiteIcon.get_by_domain(db.session, "op-icon-no-session.test")
|
||||
assert icon.name == "兼容站点"
|
||||
assert icon.base64 == "data:image/ico;base64,AAA"
|
||||
|
||||
|
||||
def test_site_oper_success_accumulates_and_records_state(db):
|
||||
"""
|
||||
访问成功累加计数并把最后状态标记为成功。
|
||||
@@ -219,6 +235,17 @@ def test_site_oper_success_accumulates_and_records_state(db):
|
||||
assert stat.seconds
|
||||
|
||||
|
||||
def test_site_oper_statistics_without_explicit_session_use_transaction_runner(db):
|
||||
"""无显式会话的站点统计必须由兼容事务执行器完成提交。"""
|
||||
oper = SiteOper()
|
||||
|
||||
oper.success("op-stat-no-session.test", seconds=3)
|
||||
oper.fail("op-stat-no-session.test")
|
||||
|
||||
stat = SiteStatistic.get_by_domain(db.session, "op-stat-no-session.test")
|
||||
assert (stat.success, stat.fail, stat.lst_state) == (1, 1, 1)
|
||||
|
||||
|
||||
def test_site_oper_success_caps_the_timing_note_at_ten_entries(db):
|
||||
"""
|
||||
耗时记录最多保留最近 10 条,超出时丢弃最旧的。
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""站点连通性测试链路的数据库回归测试。"""
|
||||
|
||||
from app.chain.site import SiteChain
|
||||
from app.db.models.site import Site
|
||||
from app.db.models.sitestatistic import SiteStatistic
|
||||
|
||||
|
||||
def test_site_connectivity_records_result_without_injected_session(db, monkeypatch):
|
||||
"""默认站点端口完成测试后应提交统计,而不是对空会话调用 execute。"""
|
||||
db.watermark(Site, SiteStatistic)
|
||||
db.add(Site(
|
||||
name="连通性测试站点",
|
||||
domain="connectivity.test",
|
||||
url="https://connectivity.test/",
|
||||
is_active=True,
|
||||
))
|
||||
monkeypatch.setattr(
|
||||
SiteChain,
|
||||
"_SiteChain__test",
|
||||
lambda _self, _site: (True, "连接成功"),
|
||||
)
|
||||
|
||||
status, message = SiteChain().test("https://connectivity.test/")
|
||||
|
||||
statistic = SiteStatistic.get_by_domain(db.session, "connectivity.test")
|
||||
assert (status, message) == (True, "连接成功")
|
||||
assert statistic.success == 1
|
||||
assert statistic.lst_state == 0
|
||||
Reference in New Issue
Block a user