Files
ragflow/test/testcases/restful_api/test_user_tenant_routes_unit.py
Lynn 478c9846a1 Fix: model list (#15860)
### What problem does this PR solve?

Remove tenant_llm call in rag.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2026-06-10 14:59:57 +08:00

1620 lines
65 KiB
Python

#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import base64
import importlib.util
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
import pytest
class _DummyManager:
def route(self, *_args, **_kwargs):
def decorator(func):
return func
return decorator
class _AwaitableValue:
def __init__(self, value):
self._value = value
def __await__(self):
async def _co():
return self._value
return _co().__await__()
class _Field:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return (self.name, other)
class _Invitee:
def __init__(self, user_id="invitee-1", email="invitee@example.com"):
self.id = user_id
self.email = email
def to_dict(self):
return {
"id": self.id,
"avatar": "avatar-url",
"email": self.email,
"nickname": "Invitee",
"password": "ignored",
}
def _run(coro):
return asyncio.run(coro)
def _passthrough_login_required(func):
async def _wrapper(*args, **kwargs):
return await func(*args, **kwargs)
_wrapper.__wrapped__ = func
return _wrapper
def _set_request_json(monkeypatch, module, payload):
async def _request_json():
return payload
monkeypatch.setattr(module, "get_request_json", _request_json)
def _load_tenant_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[3]
api_pkg = ModuleType("api")
api_pkg.__path__ = [str(repo_root / "api")]
monkeypatch.setitem(sys.modules, "api", api_pkg)
apps_mod = ModuleType("api.apps")
apps_mod.__path__ = [str(repo_root / "api" / "apps")]
apps_mod.current_user = SimpleNamespace(id="tenant-1", email="owner@example.com")
apps_mod.login_required = lambda fn: fn
monkeypatch.setitem(sys.modules, "api.apps", apps_mod)
db_mod = ModuleType("api.db")
db_mod.UserTenantRole = SimpleNamespace(NORMAL="normal", OWNER="owner", INVITE="invite")
monkeypatch.setitem(sys.modules, "api.db", db_mod)
db_models_mod = ModuleType("api.db.db_models")
db_models_mod.UserTenant = type(
"UserTenant",
(),
{
"tenant_id": _Field("tenant_id"),
"user_id": _Field("user_id"),
},
)
monkeypatch.setitem(sys.modules, "api.db.db_models", db_models_mod)
services_pkg = ModuleType("api.db.services")
services_pkg.__path__ = []
monkeypatch.setitem(sys.modules, "api.db.services", services_pkg)
user_service_mod = ModuleType("api.db.services.user_service")
class _UserTenantService:
@staticmethod
def get_by_tenant_id(_tenant_id):
return []
@staticmethod
def query(**_kwargs):
return []
@staticmethod
def save(**_kwargs):
return True
@staticmethod
def filter_delete(_conditions):
return True
@staticmethod
def get_tenants_by_user_id(_user_id):
return []
@staticmethod
def filter_update(_conditions, _payload):
return True
class _UserService:
@staticmethod
def query(**_kwargs):
return []
@staticmethod
def get_by_id(_user_id):
return False, None
user_service_mod.UserTenantService = _UserTenantService
user_service_mod.UserService = _UserService
monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod)
api_utils_mod = ModuleType("api.utils.api_utils")
api_utils_mod.get_json_result = lambda data=None, message="", code=0: {"code": code, "message": message, "data": data}
api_utils_mod.get_data_error_result = lambda message="": {"code": 102, "message": message, "data": False}
api_utils_mod.server_error_response = lambda exc: {"code": 100, "message": repr(exc), "data": False}
api_utils_mod.validate_request = lambda *_args, **_kwargs: (lambda fn: fn)
api_utils_mod.get_request_json = lambda: _AwaitableValue({})
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
web_utils_mod = ModuleType("api.utils.web_utils")
web_utils_mod.send_invite_email = lambda **_kwargs: {"ok": True}
monkeypatch.setitem(sys.modules, "api.utils.web_utils", web_utils_mod)
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
constants_mod = ModuleType("common.constants")
constants_mod.RetCode = SimpleNamespace(AUTHENTICATION_ERROR=401, SERVER_ERROR=500, DATA_ERROR=102)
constants_mod.StatusEnum = SimpleNamespace(VALID=SimpleNamespace(value=1))
monkeypatch.setitem(sys.modules, "common.constants", constants_mod)
misc_utils_mod = ModuleType("common.misc_utils")
misc_utils_mod.get_uuid = lambda: "uuid-1"
monkeypatch.setitem(sys.modules, "common.misc_utils", misc_utils_mod)
time_utils_mod = ModuleType("common.time_utils")
time_utils_mod.delta_seconds = lambda _value: 0
monkeypatch.setitem(sys.modules, "common.time_utils", time_utils_mod)
settings_mod = ModuleType("common.settings")
settings_mod.MAIL_FRONTEND_URL = "https://frontend.example/invite"
monkeypatch.setitem(sys.modules, "common.settings", settings_mod)
common_pkg.settings = settings_mod
sys.modules.pop("test_tenant_app_unit_module", None)
module_path = repo_root / "api" / "apps" / "restful_apis" / "tenant_api.py"
spec = importlib.util.spec_from_file_location("test_tenant_app_unit_module", module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
monkeypatch.setitem(sys.modules, "test_tenant_app_unit_module", module)
spec.loader.exec_module(module)
return module
@pytest.mark.p2
def test_user_list_auth_success_exception_matrix_unit(monkeypatch):
module = _load_tenant_module(monkeypatch)
module.current_user.id = "other-user"
res = module.user_list("tenant-1")
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
assert res["message"] == "No authorization.", res
module.current_user.id = "tenant-1"
monkeypatch.setattr(
module.UserTenantService,
"get_by_tenant_id",
lambda _tenant_id: [{"id": "u1", "update_date": "2024-01-01 00:00:00"}],
)
monkeypatch.setattr(module, "delta_seconds", lambda _value: 42)
res = module.user_list("tenant-1")
assert res["code"] == 0, res
assert res["data"][0]["delta_seconds"] == 42, res
monkeypatch.setattr(module.UserTenantService, "get_by_tenant_id", lambda _tenant_id: (_ for _ in ()).throw(RuntimeError("list boom")))
res = module.user_list("tenant-1")
assert res["code"] == 100, res
assert "list boom" in res["message"], res
@pytest.mark.p2
def test_create_invite_role_and_email_failure_matrix_unit(monkeypatch):
module = _load_tenant_module(monkeypatch)
module.current_user.id = "other-user"
_set_request_json(monkeypatch, module, {"email": "invitee@example.com"})
res = _run(module.create("tenant-1"))
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
assert res["message"] == "No authorization.", res
module.current_user.id = "tenant-1"
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
res = _run(module.create("tenant-1"))
assert res["message"] == "User not found.", res
invitee = _Invitee()
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [invitee])
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role=module.UserTenantRole.NORMAL)])
res = _run(module.create("tenant-1"))
assert "already in the team." in res["message"], res
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role=module.UserTenantRole.OWNER)])
res = _run(module.create("tenant-1"))
assert "owner of the team." in res["message"], res
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(role="strange-role")])
res = _run(module.create("tenant-1"))
assert "role: strange-role is invalid." in res["message"], res
saved = []
scheduled = []
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [])
monkeypatch.setattr(module.UserTenantService, "save", lambda **kwargs: saved.append(kwargs) or True)
monkeypatch.setattr(module.UserService, "get_by_id", lambda _user_id: (True, SimpleNamespace(nickname="Inviter Nick")))
monkeypatch.setattr(module, "send_invite_email", lambda **kwargs: kwargs)
monkeypatch.setattr(module.asyncio, "create_task", lambda payload: scheduled.append(payload) or SimpleNamespace())
res = _run(module.create("tenant-1"))
assert res["code"] == 0, res
assert saved and saved[-1]["role"] == module.UserTenantRole.INVITE, saved
assert scheduled and scheduled[-1]["inviter"] == "Inviter Nick", scheduled
assert sorted(res["data"].keys()) == ["avatar", "email", "id", "nickname"], res
monkeypatch.setattr(module.asyncio, "create_task", lambda _payload: (_ for _ in ()).throw(RuntimeError("send boom")))
res = _run(module.create("tenant-1"))
assert res["code"] == module.RetCode.SERVER_ERROR, res
assert "Failed to send invite email." in res["message"], res
@pytest.mark.p2
def test_rm_and_tenant_list_matrix_unit(monkeypatch):
module = _load_tenant_module(monkeypatch)
module.current_user.id = "outsider"
_set_request_json(monkeypatch, module, {"user_id": "user-2"})
res = _run(module.rm("tenant-1"))
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
assert res["message"] == "No authorization.", res
module.current_user.id = "tenant-1"
deleted = []
monkeypatch.setattr(module.UserTenantService, "filter_delete", lambda conditions: deleted.append(conditions) or True)
res = _run(module.rm("tenant-1"))
assert res["code"] == 0, res
assert res["data"] is True, res
assert deleted, "filter_delete should be called"
monkeypatch.setattr(module.UserTenantService, "filter_delete", lambda _conditions: (_ for _ in ()).throw(RuntimeError("rm boom")))
res = _run(module.rm("tenant-1"))
assert res["code"] == 100, res
assert "rm boom" in res["message"], res
monkeypatch.setattr(
module.UserTenantService,
"get_tenants_by_user_id",
lambda _user_id: [{"id": "tenant-1", "update_date": "2024-01-01 00:00:00"}],
)
monkeypatch.setattr(module, "delta_seconds", lambda _value: 9)
res = module.tenant_list()
assert res["code"] == 0, res
assert res["data"][0]["delta_seconds"] == 9, res
monkeypatch.setattr(module.UserTenantService, "get_tenants_by_user_id", lambda _user_id: (_ for _ in ()).throw(RuntimeError("tenant boom")))
res = module.tenant_list()
assert res["code"] == 100, res
assert "tenant boom" in res["message"], res
@pytest.mark.p2
def test_agree_success_and_exception_unit(monkeypatch):
module = _load_tenant_module(monkeypatch)
calls = []
monkeypatch.setattr(module.UserTenantService, "filter_update", lambda conditions, payload: calls.append((conditions, payload)) or True)
res = module.agree("tenant-1")
assert res["code"] == 0, res
assert res["data"] is True, res
assert calls and calls[-1][1]["role"] == module.UserTenantRole.NORMAL
monkeypatch.setattr(module.UserTenantService, "filter_update", lambda _conditions, _payload: (_ for _ in ()).throw(RuntimeError("agree boom")))
res = module.agree("tenant-1")
assert res["code"] == 100, res
assert "agree boom" in res["message"], res
class _Args(dict):
def get(self, key, default=None, type=None):
value = super().get(key, default)
if type is None:
return value
try:
return type(value)
except (TypeError, ValueError):
return default
class _DummyResponse:
def __init__(self, data):
self.data = data
self.headers = {}
class _DummyHTTPResponse:
def __init__(self, payload):
self._payload = payload
def json(self):
return self._payload
class _DummyRedis:
def __init__(self):
self.store = {}
def get(self, key):
return self.store.get(key)
def set(self, key, value, _ttl=None):
self.store[key] = value
def delete(self, key):
self.store.pop(key, None)
class _DummyUser:
def __init__(self, user_id, email, *, password="stored-password", is_active="1", nickname="nick"):
self.id = user_id
self.email = email
self.password = password
self.is_active = is_active
self.nickname = nickname
self.access_token = ""
self.save_calls = 0
def save(self):
self.save_calls += 1
def get_id(self):
return self.id
def to_json(self):
return {"id": self.id, "email": self.email, "nickname": self.nickname}
def to_dict(self):
return {"id": self.id, "email": self.email}
def to_safe_dict(self, *, for_self: bool = False):
_sensitive = {"password", "access_token", "email"}
result = {k: v for k, v in self.to_dict().items() if k not in _sensitive}
if for_self:
result["email"] = self.email
return result
def _set_request_args(monkeypatch, module, args=None):
monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args(args or {})))
@pytest.fixture(scope="session")
def auth():
return "unit-auth"
@pytest.fixture(scope="session", autouse=True)
def set_tenant_info():
return None
def _load_user_app(monkeypatch):
repo_root = Path(__file__).resolve().parents[3]
quart_mod = ModuleType("quart")
quart_mod.session = {}
quart_mod.request = SimpleNamespace(args=_Args({}))
async def _make_response(data):
return _DummyResponse(data)
quart_mod.make_response = _make_response
quart_mod.redirect = lambda url: {"redirect": url}
monkeypatch.setitem(sys.modules, "quart", quart_mod)
api_pkg = ModuleType("api")
api_pkg.__path__ = [str(repo_root / "api")]
monkeypatch.setitem(sys.modules, "api", api_pkg)
apps_mod = ModuleType("api.apps")
apps_mod.__path__ = [str(repo_root / "api" / "apps")]
apps_mod.current_user = _DummyUser("current-user", "current@example.com")
apps_mod.login_required = lambda fn: fn
apps_mod.login_user = lambda _user: True
apps_mod.logout_user = lambda: True
monkeypatch.setitem(sys.modules, "api.apps", apps_mod)
api_pkg.apps = apps_mod
apps_auth_mod = ModuleType("api.apps.auth")
apps_auth_mod.get_auth_client = lambda _config: SimpleNamespace(
get_authorization_url=lambda state: f"https://oauth.example/{state}"
)
monkeypatch.setitem(sys.modules, "api.apps.auth", apps_auth_mod)
db_mod = ModuleType("api.db")
db_mod.FileType = SimpleNamespace(FOLDER=SimpleNamespace(value="folder"))
db_mod.UserTenantRole = SimpleNamespace(OWNER="owner")
monkeypatch.setitem(sys.modules, "api.db", db_mod)
api_pkg.db = db_mod
db_models_mod = ModuleType("api.db.db_models")
class _DummyTenantLLMModel:
tenant_id = _Field("tenant_id")
@staticmethod
def delete():
class _DeleteQuery:
def where(self, *_args, **_kwargs):
return self
def execute(self):
return 1
return _DeleteQuery()
db_models_mod.TenantLLM = _DummyTenantLLMModel
monkeypatch.setitem(sys.modules, "api.db.db_models", db_models_mod)
services_pkg = ModuleType("api.db.services")
services_pkg.__path__ = []
monkeypatch.setitem(sys.modules, "api.db.services", services_pkg)
file_service_mod = ModuleType("api.db.services.file_service")
class _StubFileService:
@staticmethod
def insert(_data):
return True
file_service_mod.FileService = _StubFileService
monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod)
llm_service_mod = ModuleType("api.db.services.llm_service")
llm_service_mod.get_init_tenant_llm = lambda _user_id: []
monkeypatch.setitem(sys.modules, "api.db.services.llm_service", llm_service_mod)
tenant_llm_service_mod = ModuleType("api.db.services.tenant_llm_service")
class _MockTableObject:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def to_dict(self):
return {k: v for k, v in self.__dict__.items()}
class _StubTenantLLMService:
@staticmethod
def insert_many(_payload):
return True
@staticmethod
def get_api_key(tenant_id, model_name, model_type=None):
return _MockTableObject(
id=1,
tenant_id=tenant_id,
llm_factory="",
model_type="chat",
llm_name=model_name,
api_key="fake-api-key",
api_base="https://api.example.com",
max_tokens=8192,
used_tokens=0,
status=1
)
tenant_llm_service_mod.TenantLLMService = _StubTenantLLMService
monkeypatch.setitem(sys.modules, "api.db.services.tenant_llm_service", tenant_llm_service_mod)
user_service_mod = ModuleType("api.db.services.user_service")
class _StubTenantService:
@staticmethod
def insert(**_kwargs):
return True
@staticmethod
def delete_by_id(_tenant_id):
return True
@staticmethod
def get_by_id(_tenant_id):
return True, SimpleNamespace(id=_tenant_id)
@staticmethod
def get_info_by(_user_id):
return []
@staticmethod
def update_by_id(_tenant_id, _payload):
return True
class _StubUserService:
@staticmethod
def query(**_kwargs):
return []
@staticmethod
def query_user(_email, _password):
return None
@staticmethod
def query_user_by_email(**_kwargs):
return []
@staticmethod
def save(**_kwargs):
return True
@staticmethod
def delete_by_id(_user_id):
return True
@staticmethod
def update_by_id(_user_id, _payload):
return True
@staticmethod
def update_user_password(_user_id, _new_password):
return True
class _StubUserTenantService:
@staticmethod
def insert(**_kwargs):
return True
@staticmethod
def query(**_kwargs):
return []
@staticmethod
def delete_by_id(_user_tenant_id):
return True
user_service_mod.TenantService = _StubTenantService
user_service_mod.UserService = _StubUserService
user_service_mod.UserTenantService = _StubUserTenantService
monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod)
api_utils_mod = ModuleType("api.utils.api_utils")
async def _default_request_json():
return {}
def _get_json_result(code=0, message="success", data=None):
return {"code": code, "message": message, "data": data}
def _get_data_error_result(code=102, message="Sorry! Data missing!", data=None):
return {"code": code, "message": message, "data": data}
def _server_error_response(error):
return {"code": 100, "message": repr(error)}
def _validate_request(*_args, **_kwargs):
def _decorator(func):
return func
return _decorator
api_utils_mod.get_request_json = _default_request_json
api_utils_mod.get_json_result = _get_json_result
api_utils_mod.get_data_error_result = _get_data_error_result
api_utils_mod.server_error_response = _server_error_response
api_utils_mod.validate_request = _validate_request
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
crypt_mod = ModuleType("api.utils.crypt")
crypt_mod.decrypt = lambda value: value
monkeypatch.setitem(sys.modules, "api.utils.crypt", crypt_mod)
web_utils_mod = ModuleType("api.utils.web_utils")
web_utils_mod.send_email_html = lambda *_args, **_kwargs: _AwaitableValue(True)
web_utils_mod.OTP_LENGTH = 6
web_utils_mod.OTP_TTL_SECONDS = 600
web_utils_mod.ATTEMPT_LIMIT = 5
web_utils_mod.ATTEMPT_LOCK_SECONDS = 600
web_utils_mod.RESEND_COOLDOWN_SECONDS = 60
web_utils_mod.otp_keys = lambda email: (
f"otp:{email}:code",
f"otp:{email}:attempts",
f"otp:{email}:last",
f"otp:{email}:lock",
)
web_utils_mod.hash_code = lambda code, _salt: f"hash:{code}"
web_utils_mod.captcha_key = lambda email: f"captcha:{email}"
monkeypatch.setitem(sys.modules, "api.utils.web_utils", web_utils_mod)
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
settings_mod = ModuleType("common.settings")
settings_mod.OAUTH_CONFIG = {
"github": {"display_name": "GitHub", "icon": "gh"},
"feishu": {"display_name": "Feishu", "icon": "fs"},
}
settings_mod.GITHUB_OAUTH = {"url": "https://github.example/oauth", "client_id": "cid", "secret_key": "sk"}
settings_mod.FEISHU_OAUTH = {
"app_access_token_url": "https://feishu.example/app_token",
"user_access_token_url": "https://feishu.example/user_token",
"app_id": "app-id",
"app_secret": "app-secret",
"grant_type": "authorization_code",
}
settings_mod.CHAT_MDL = "chat-mdl"
settings_mod.EMBEDDING_MDL = "embd-mdl"
settings_mod.ASR_MDL = "asr-mdl"
settings_mod.PARSERS = []
settings_mod.IMAGE2TEXT_MDL = "img-mdl"
settings_mod.RERANK_MDL = "rerank-mdl"
settings_mod.REGISTER_ENABLED = True
monkeypatch.setitem(sys.modules, "common.settings", settings_mod)
common_pkg.settings = settings_mod
constants_mod = ModuleType("common.constants")
constants_mod.RetCode = SimpleNamespace(
AUTHENTICATION_ERROR=401,
SERVER_ERROR=500,
FORBIDDEN=403,
EXCEPTION_ERROR=100,
OPERATING_ERROR=300,
ARGUMENT_ERROR=101,
DATA_ERROR=102,
NOT_EFFECTIVE=103,
SUCCESS=0,
)
monkeypatch.setitem(sys.modules, "common.constants", constants_mod)
connection_utils_mod = ModuleType("common.connection_utils")
async def _construct_response(data=None, auth=None, message=""):
return {"code": 0, "message": message, "data": data, "auth": auth}
connection_utils_mod.construct_response = _construct_response
monkeypatch.setitem(sys.modules, "common.connection_utils", connection_utils_mod)
time_utils_mod = ModuleType("common.time_utils")
time_utils_mod.current_timestamp = lambda: 111
time_utils_mod.datetime_format = lambda _dt: "2024-01-01 00:00:00"
time_utils_mod.get_format_time = lambda: "2024-01-01 00:00:00"
monkeypatch.setitem(sys.modules, "common.time_utils", time_utils_mod)
misc_utils_mod = ModuleType("common.misc_utils")
misc_utils_mod.download_img = lambda _url: "avatar"
misc_utils_mod.get_uuid = lambda: "uuid-default"
monkeypatch.setitem(sys.modules, "common.misc_utils", misc_utils_mod)
http_client_mod = ModuleType("common.http_client")
async def _async_request(_method, _url, **_kwargs):
return _DummyHTTPResponse({})
http_client_mod.async_request = _async_request
monkeypatch.setitem(sys.modules, "common.http_client", http_client_mod)
rag_pkg = ModuleType("rag")
rag_pkg.__path__ = [str(repo_root / "rag")]
monkeypatch.setitem(sys.modules, "rag", rag_pkg)
rag_utils_pkg = ModuleType("rag.utils")
rag_utils_pkg.__path__ = [str(repo_root / "rag" / "utils")]
monkeypatch.setitem(sys.modules, "rag.utils", rag_utils_pkg)
redis_mod = ModuleType("rag.utils.redis_conn")
redis_mod.REDIS_CONN = _DummyRedis()
monkeypatch.setitem(sys.modules, "rag.utils.redis_conn", redis_mod)
module_name = "test_user_app_unit_module"
module_path = repo_root / "api" / "apps" / "restful_apis" / "user_api.py"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
@pytest.mark.p2
def test_login_route_branch_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
_set_request_json(monkeypatch, module, {})
res = _run(module.login())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "Unauthorized" in res["message"]
_set_request_json(monkeypatch, module, {"email": "unknown@example.com", "password": "enc"})
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
res = _run(module.login())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "not registered" in res["message"]
_set_request_json(monkeypatch, module, {"email": "known@example.com", "password": "enc"})
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [SimpleNamespace(email="known@example.com")])
def _raise_decrypt(_value):
raise RuntimeError("decrypt explode")
monkeypatch.setattr(module, "decrypt", _raise_decrypt)
res = _run(module.login())
assert res["code"] == module.RetCode.SERVER_ERROR
assert "Fail to crypt password" in res["message"]
user_inactive = _DummyUser("u-inactive", "known@example.com", is_active="0")
monkeypatch.setattr(module, "decrypt", lambda value: value)
monkeypatch.setattr(module.UserService, "query_user", lambda _email, _password: user_inactive)
res = _run(module.login())
assert res["code"] == module.RetCode.FORBIDDEN
assert "disabled" in res["message"]
monkeypatch.setattr(module.UserService, "query_user", lambda _email, _password: None)
res = _run(module.login())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "do not match" in res["message"]
@pytest.mark.p2
def test_login_channels_and_oauth_login_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
module.settings.OAUTH_CONFIG = {"github": {"display_name": "GitHub", "icon": "gh"}}
res = _run(module.get_login_channels())
assert res["code"] == 0
assert res["data"][0]["channel"] == "github"
class _BrokenOAuthConfig:
@staticmethod
def items():
raise RuntimeError("broken oauth config")
module.settings.OAUTH_CONFIG = _BrokenOAuthConfig()
res = _run(module.get_login_channels())
assert res["code"] == module.RetCode.EXCEPTION_ERROR
assert "Load channels failure" in res["message"]
module.settings.OAUTH_CONFIG = {"github": {"display_name": "GitHub", "icon": "gh"}}
with pytest.raises(ValueError, match="Invalid channel name: missing"):
_run(module.oauth_login("missing"))
module.session.clear()
monkeypatch.setattr(module, "get_uuid", lambda: "state-123")
class _AuthClient:
@staticmethod
def get_authorization_url(state):
return f"https://oauth.example/{state}"
monkeypatch.setattr(module, "get_auth_client", lambda _config: _AuthClient())
res = _run(module.oauth_login("github"))
assert res["redirect"] == "https://oauth.example/state-123"
assert module.session["oauth_state"] == "state-123"
@pytest.mark.p2
def test_oauth_callback_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
module.settings.OAUTH_CONFIG = {"github": {"display_name": "GitHub", "icon": "gh"}}
class _SyncAuthClient:
def __init__(self, token_info, user_info):
self._token_info = token_info
self._user_info = user_info
def exchange_code_for_token(self, _code):
return self._token_info
def fetch_user_info(self, _token, id_token=None):
_ = id_token
return self._user_info
class _AsyncAuthClient:
def __init__(self, token_info, user_info):
self._token_info = token_info
self._user_info = user_info
async def async_exchange_code_for_token(self, _code):
return self._token_info
async def async_fetch_user_info(self, _token, id_token=None):
_ = id_token
return self._user_info
_set_request_args(monkeypatch, module, {"state": "x", "code": "c"})
module.session.clear()
res = _run(module.oauth_callback("missing"))
assert "Invalid channel name: missing" in res["redirect"]
sync_ok = _SyncAuthClient(
token_info={"access_token": "token-sync", "id_token": "id-sync"},
user_info=SimpleNamespace(email="sync@example.com", avatar_url="http://img", nickname="sync"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: sync_ok)
module.session.clear()
module.session["oauth_state"] = "expected"
_set_request_args(monkeypatch, module, {"state": "wrong", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?error=invalid_state"
module.session.clear()
module.session["oauth_state"] = "ok-state"
_set_request_args(monkeypatch, module, {"state": "ok-state"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?error=missing_code"
sync_missing_token = _SyncAuthClient(
token_info={"id_token": "id-only"},
user_info=SimpleNamespace(email="sync@example.com", avatar_url="http://img", nickname="sync"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: sync_missing_token)
module.session.clear()
module.session["oauth_state"] = "token-state"
_set_request_args(monkeypatch, module, {"state": "token-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?error=token_failed"
sync_missing_email = _SyncAuthClient(
token_info={"access_token": "token-sync", "id_token": "id-sync"},
user_info=SimpleNamespace(email=None, avatar_url="http://img", nickname="sync"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: sync_missing_email)
module.session.clear()
module.session["oauth_state"] = "email-state"
_set_request_args(monkeypatch, module, {"state": "email-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?error=email_missing"
async_new_user = _AsyncAuthClient(
token_info={"access_token": "token-async", "id_token": "id-async"},
user_info=SimpleNamespace(email="new@example.com", avatar_url="http://img", nickname="new-user"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: async_new_user)
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
def _raise_download(_url):
raise RuntimeError("download explode")
monkeypatch.setattr(module, "download_img", _raise_download)
monkeypatch.setattr(module, "user_register", lambda _user_id, _user: None)
rollback_calls = []
monkeypatch.setattr(module, "rollback_user_registration", lambda user_id: rollback_calls.append(user_id))
monkeypatch.setattr(module, "get_uuid", lambda: "new-user-id")
module.session.clear()
module.session["oauth_state"] = "new-user-state"
_set_request_args(monkeypatch, module, {"state": "new-user-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert "Failed to register new@example.com" in res["redirect"]
assert rollback_calls == ["new-user-id"]
monkeypatch.setattr(module, "download_img", lambda _url: "avatar")
monkeypatch.setattr(
module,
"user_register",
lambda _user_id, _user: [_DummyUser("dup-1", "new@example.com"), _DummyUser("dup-2", "new@example.com")],
)
rollback_calls.clear()
module.session.clear()
module.session["oauth_state"] = "dup-user-state"
_set_request_args(monkeypatch, module, {"state": "dup-user-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert "Same email: new@example.com exists!" in res["redirect"]
assert rollback_calls == ["new-user-id"]
new_user = _DummyUser("new-user", "new@example.com")
login_calls = []
monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user))
monkeypatch.setattr(module, "user_register", lambda _user_id, _user: [new_user])
module.session.clear()
module.session["oauth_state"] = "create-user-state"
_set_request_args(monkeypatch, module, {"state": "create-user-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?auth=new-user"
assert login_calls and login_calls[-1] is new_user
async_existing_inactive = _AsyncAuthClient(
token_info={"access_token": "token-existing", "id_token": "id-existing"},
user_info=SimpleNamespace(email="existing@example.com", avatar_url="http://img", nickname="existing"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: async_existing_inactive)
inactive_user = _DummyUser("existing-user", "existing@example.com", is_active="0")
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [inactive_user])
module.session.clear()
module.session["oauth_state"] = "inactive-state"
_set_request_args(monkeypatch, module, {"state": "inactive-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?error=user_inactive"
async_existing_ok = _AsyncAuthClient(
token_info={"access_token": "token-existing", "id_token": "id-existing"},
user_info=SimpleNamespace(email="existing@example.com", avatar_url="http://img", nickname="existing"),
)
monkeypatch.setattr(module, "get_auth_client", lambda _config: async_existing_ok)
existing_user = _DummyUser("existing-user", "existing@example.com")
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [existing_user])
login_calls.clear()
monkeypatch.setattr(module, "login_user", lambda user: login_calls.append(user))
monkeypatch.setattr(module, "get_uuid", lambda: "existing-token")
module.session.clear()
module.session["oauth_state"] = "existing-state"
_set_request_args(monkeypatch, module, {"state": "existing-state", "code": "code"})
res = _run(module.oauth_callback("github"))
assert res["redirect"] == "/?auth=existing-user"
assert existing_user.access_token == "existing-token"
assert existing_user.save_calls == 1
assert login_calls and login_calls[-1] is existing_user
@pytest.mark.p2
def test_logout_setting_profile_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
current_user = _DummyUser("current-user", "current@example.com", password="stored-password")
monkeypatch.setattr(module, "current_user", current_user)
monkeypatch.setattr(module.secrets, "token_hex", lambda _n: "abcdef")
logout_calls = []
monkeypatch.setattr(module, "logout_user", lambda: logout_calls.append(True))
res = _run(module.log_out())
assert res["code"] == 0
assert current_user.access_token == "INVALID_abcdef"
assert current_user.save_calls == 1
assert logout_calls == [True]
_set_request_json(monkeypatch, module, {"password": "old-password", "new_password": "new-password"})
monkeypatch.setattr(module, "decrypt", lambda value: value)
monkeypatch.setattr(module, "check_password_hash", lambda _hashed, _plain: False)
res = _run(module.setting_user())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
assert "Password error" in res["message"]
_set_request_json(
monkeypatch,
module,
{
"password": "old-password",
"new_password": "new-password",
"nickname": "neo",
"email": "blocked@example.com",
"status": "disabled",
"theme": "dark",
},
)
monkeypatch.setattr(module, "check_password_hash", lambda _hashed, _plain: True)
monkeypatch.setattr(module, "decrypt", lambda value: f"dec:{value}")
monkeypatch.setattr(module, "generate_password_hash", lambda value: f"hash:{value}")
update_calls = {}
def _update_by_id(user_id, payload):
update_calls["user_id"] = user_id
update_calls["payload"] = payload
return True
monkeypatch.setattr(module.UserService, "update_by_id", _update_by_id)
res = _run(module.setting_user())
assert res["code"] == 0
assert res["data"] is True
assert update_calls["user_id"] == "current-user"
assert update_calls["payload"]["password"] == "hash:dec:new-password"
assert update_calls["payload"]["nickname"] == "neo"
assert update_calls["payload"]["theme"] == "dark"
assert "email" not in update_calls["payload"]
assert "status" not in update_calls["payload"]
_set_request_json(monkeypatch, module, {"nickname": "neo"})
def _raise_update(_user_id, _payload):
raise RuntimeError("update explode")
monkeypatch.setattr(module.UserService, "update_by_id", _raise_update)
res = _run(module.setting_user())
assert res["code"] == module.RetCode.EXCEPTION_ERROR
assert "Update failure" in res["message"]
res = _run(module.user_profile())
assert res["code"] == 0
assert res["data"] == current_user.to_dict()
@pytest.mark.p2
def test_registration_helpers_and_register_route_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
deleted = {"user": 0, "tenant": 0, "user_tenant": 0}
monkeypatch.setattr(module.UserService, "delete_by_id", lambda _user_id: deleted.__setitem__("user", deleted["user"] + 1))
monkeypatch.setattr(module.TenantService, "delete_by_id", lambda _tenant_id: deleted.__setitem__("tenant", deleted["tenant"] + 1))
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(id="ut-1")])
monkeypatch.setattr(module.UserTenantService, "delete_by_id", lambda _ut_id: deleted.__setitem__("user_tenant", deleted["user_tenant"] + 1))
module.rollback_user_registration("user-1")
assert deleted == {"user": 1, "tenant": 1, "user_tenant": 1}, deleted
monkeypatch.setattr(module.UserService, "delete_by_id", lambda _user_id: (_ for _ in ()).throw(RuntimeError("u boom")))
monkeypatch.setattr(module.TenantService, "delete_by_id", lambda _tenant_id: (_ for _ in ()).throw(RuntimeError("t boom")))
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("ut boom")))
class _RaisingDeleteQuery:
def where(self, *_args, **_kwargs):
raise RuntimeError("llm boom")
module.rollback_user_registration("user-2")
monkeypatch.setattr(module.UserService, "save", lambda **_kwargs: False)
res = module.user_register(
"new-user",
{
"nickname": "new",
"email": "new@example.com",
"password": "pw",
"access_token": "tk",
"login_channel": "password",
"last_login_time": "2024-01-01 00:00:00",
"is_superuser": False,
},
)
assert res is None
monkeypatch.setattr(module.settings, "REGISTER_ENABLED", False)
_set_request_json(monkeypatch, module, {"nickname": "neo", "email": "neo@example.com", "password": "enc"})
res = _run(module.user_add())
assert res["code"] == module.RetCode.OPERATING_ERROR, res
assert "disabled" in res["message"], res
monkeypatch.setattr(module.settings, "REGISTER_ENABLED", True)
_set_request_json(monkeypatch, module, {"nickname": "neo", "email": "bad-email", "password": "enc"})
res = _run(module.user_add())
assert res["code"] == module.RetCode.OPERATING_ERROR, res
assert "Invalid email address" in res["message"], res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
monkeypatch.setattr(module, "decrypt", lambda value: value)
monkeypatch.setattr(module, "get_uuid", lambda: "new-user-id")
rollback_calls = []
monkeypatch.setattr(module, "rollback_user_registration", lambda user_id: rollback_calls.append(user_id))
_set_request_json(monkeypatch, module, {"nickname": "neo", "email": "neo@example.com", "password": "enc"})
monkeypatch.setattr(module, "user_register", lambda _user_id, _payload: None)
res = _run(module.user_add())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
assert "Fail to register neo@example.com." in res["message"], res
assert rollback_calls == ["new-user-id"], rollback_calls
rollback_calls.clear()
monkeypatch.setattr(
module,
"user_register",
lambda _user_id, _payload: [_DummyUser("dup-1", "neo@example.com"), _DummyUser("dup-2", "neo@example.com")],
)
_set_request_json(monkeypatch, module, {"nickname": "neo", "email": "neo@example.com", "password": "enc"})
res = _run(module.user_add())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
assert "Same email: neo@example.com exists!" in res["message"], res
assert rollback_calls == ["new-user-id"], rollback_calls
@pytest.mark.p2
def test_tenant_info_and_set_tenant_info_exception_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
monkeypatch.setattr(module.TenantService, "get_info_by", lambda _uid: [])
res = _run(module.tenant_info())
assert res["code"] == module.RetCode.DATA_ERROR, res
assert "Tenant not found" in res["message"], res
def _raise_tenant_info(_uid):
raise RuntimeError("tenant info boom")
monkeypatch.setattr(module.TenantService, "get_info_by", _raise_tenant_info)
res = _run(module.tenant_info())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
assert "tenant info boom" in res["message"], res
_set_request_json(
monkeypatch,
module,
{"tenant_id": "tenant-1", "llm_id": "l", "embd_id": "e", "asr_id": "a", "img2txt_id": "i"},
)
def _raise_update(_tenant_id, _payload):
raise RuntimeError("tenant update boom")
monkeypatch.setattr(module.TenantService, "update_by_id", _raise_update)
res = _run(module.set_tenant_info())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
assert "tenant update boom" in res["message"], res
@pytest.mark.p2
def test_forget_captcha_and_send_otp_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
class _Headers(dict):
def set(self, key, value):
self[key] = value
async def _make_response(data):
return SimpleNamespace(data=data, headers=_Headers())
monkeypatch.setattr(module, "make_response", _make_response)
captcha_pkg = ModuleType("captcha")
captcha_image_mod = ModuleType("captcha.image")
class _ImageCaptcha:
def __init__(self, **_kwargs):
pass
def generate(self, text):
return SimpleNamespace(read=lambda: f"img:{text}".encode())
captcha_image_mod.ImageCaptcha = _ImageCaptcha
monkeypatch.setitem(sys.modules, "captcha", captcha_pkg)
monkeypatch.setitem(sys.modules, "captcha.image", captcha_image_mod)
_set_request_args(monkeypatch, module, {"email": ""})
res = _run(module.forget_get_captcha())
assert res["code"] == module.RetCode.ARGUMENT_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
_set_request_args(monkeypatch, module, {"email": "nobody@example.com"})
res = _run(module.forget_get_captcha())
assert res["code"] == module.RetCode.DATA_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [_DummyUser("u1", "ok@example.com")])
monkeypatch.setattr(module.secrets, "choice", lambda _allowed: "A")
_set_request_args(monkeypatch, module, {"email": "ok@example.com"})
res = _run(module.forget_get_captcha())
assert res.data.startswith(b"img:"), res
assert res.headers["Content-Type"] == "image/JPEG", res.headers
assert module.REDIS_CONN.get(module.captcha_key("ok@example.com")), module.REDIS_CONN.store
_set_request_json(monkeypatch, module, {"email": "", "captcha": ""})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.ARGUMENT_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
_set_request_json(monkeypatch, module, {"email": "none@example.com", "captcha": "AAAA"})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.DATA_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [_DummyUser("u1", "ok@example.com")])
_set_request_json(monkeypatch, module, {"email": "ok@example.com", "captcha": "AAAA"})
module.REDIS_CONN.store.pop(module.captcha_key("ok@example.com"), None)
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.NOT_EFFECTIVE, res
module.REDIS_CONN.store[module.captcha_key("ok@example.com")] = "ABCD"
_set_request_json(monkeypatch, module, {"email": "ok@example.com", "captcha": "ZZZZ"})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
monkeypatch.setattr(module.time, "time", lambda: 1000)
k_code, k_attempts, k_last, k_lock = module.otp_keys("ok@example.com")
module.REDIS_CONN.store[module.captcha_key("ok@example.com")] = "ABCD"
module.REDIS_CONN.store[k_last] = "990"
_set_request_json(monkeypatch, module, {"email": "ok@example.com", "captcha": "ABCD"})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.NOT_EFFECTIVE, res
assert "wait" in res["message"], res
module.REDIS_CONN.store[module.captcha_key("ok@example.com")] = "ABCD"
module.REDIS_CONN.store[k_last] = "bad-timestamp"
monkeypatch.setattr(module.secrets, "choice", lambda _allowed: "B")
monkeypatch.setattr(module.os, "urandom", lambda _n: b"\x00" * 16)
monkeypatch.setattr(module, "hash_code", lambda code, _salt: f"HASH_{code}")
async def _raise_send_email(*_args, **_kwargs):
raise RuntimeError("send email boom")
monkeypatch.setattr(module, "send_email_html", _raise_send_email)
_set_request_json(monkeypatch, module, {"email": "ok@example.com", "captcha": "ABCD"})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.SERVER_ERROR, res
assert "failed to send email" in res["message"], res
async def _ok_send_email(*_args, **_kwargs):
return True
module.REDIS_CONN.store[module.captcha_key("ok@example.com")] = "ABCD"
module.REDIS_CONN.store.pop(k_last, None)
monkeypatch.setattr(module, "send_email_html", _ok_send_email)
_set_request_json(monkeypatch, module, {"email": "ok@example.com", "captcha": "ABCD"})
res = _run(module.forget_send_otp())
assert res["code"] == module.RetCode.SUCCESS, res
assert res["data"] is True, res
assert module.REDIS_CONN.get(k_code), module.REDIS_CONN.store
assert module.REDIS_CONN.get(k_attempts) == 0, module.REDIS_CONN.store
assert module.REDIS_CONN.get(k_lock) is None, module.REDIS_CONN.store
@pytest.mark.p2
def test_forget_verify_otp_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
email = "ok@example.com"
k_code, k_attempts, k_last, k_lock = module.otp_keys(email)
salt = b"\x01" * 16
monkeypatch.setattr(module, "hash_code", lambda code, _salt: f"HASH_{code}")
_set_request_json(monkeypatch, module, {})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.ARGUMENT_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [])
_set_request_json(monkeypatch, module, {"email": email, "otp": "ABCDEF"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.DATA_ERROR, res
monkeypatch.setattr(module.UserService, "query", lambda **_kwargs: [_DummyUser("u1", email)])
module.REDIS_CONN.store[k_lock] = "1"
_set_request_json(monkeypatch, module, {"email": email, "otp": "ABCDEF"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.NOT_EFFECTIVE, res
module.REDIS_CONN.store.pop(k_lock, None)
module.REDIS_CONN.store.pop(k_code, None)
_set_request_json(monkeypatch, module, {"email": email, "otp": "ABCDEF"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.NOT_EFFECTIVE, res
module.REDIS_CONN.store[k_code] = "broken"
_set_request_json(monkeypatch, module, {"email": email, "otp": "ABCDEF"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
module.REDIS_CONN.store[k_code] = f"HASH_CORRECT:{salt.hex()}"
module.REDIS_CONN.store[k_attempts] = "bad-int"
_set_request_json(monkeypatch, module, {"email": email, "otp": "wrong"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
assert module.REDIS_CONN.get(k_attempts) == 1, module.REDIS_CONN.store
module.REDIS_CONN.store[k_code] = f"HASH_CORRECT:{salt.hex()}"
module.REDIS_CONN.store[k_attempts] = str(module.ATTEMPT_LIMIT - 1)
_set_request_json(monkeypatch, module, {"email": email, "otp": "wrong"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
assert module.REDIS_CONN.get(k_lock) is not None, module.REDIS_CONN.store
module.REDIS_CONN.store.pop(k_lock, None)
module.REDIS_CONN.store[k_code] = f"HASH_ABCDEF:{salt.hex()}"
module.REDIS_CONN.store[k_attempts] = "0"
module.REDIS_CONN.store[k_last] = "1000"
def _set_with_verified_fail(key, value, _ttl=None):
if key == module._verified_key(email):
raise RuntimeError("verified set boom")
module.REDIS_CONN.store[key] = value
monkeypatch.setattr(module.REDIS_CONN, "set", _set_with_verified_fail)
_set_request_json(monkeypatch, module, {"email": email, "otp": "abcdef"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.SERVER_ERROR, res
monkeypatch.setattr(module.REDIS_CONN, "set", lambda key, value, _ttl=None: module.REDIS_CONN.store.__setitem__(key, value))
module.REDIS_CONN.store[k_code] = f"HASH_ABCDEF:{salt.hex()}"
module.REDIS_CONN.store[k_attempts] = "0"
module.REDIS_CONN.store[k_last] = "1000"
_set_request_json(monkeypatch, module, {"email": email, "otp": "abcdef"})
res = _run(module.forget_verify_otp())
assert res["code"] == module.RetCode.SUCCESS, res
assert module.REDIS_CONN.get(k_code) is None, module.REDIS_CONN.store
assert module.REDIS_CONN.get(k_attempts) is None, module.REDIS_CONN.store
assert module.REDIS_CONN.get(k_last) is None, module.REDIS_CONN.store
assert module.REDIS_CONN.get(k_lock) is None, module.REDIS_CONN.store
assert module.REDIS_CONN.get(module._verified_key(email)) == "1", module.REDIS_CONN.store
@pytest.mark.p2
def test_forget_reset_password_matrix_unit(monkeypatch):
module = _load_user_app(monkeypatch)
email = "reset@example.com"
v_key = module._verified_key(email)
user = _DummyUser("u-reset", email, nickname="reset-user")
pwd_a = base64.b64encode(b"new-password").decode()
pwd_b = base64.b64encode(b"confirm-password").decode()
pwd_same = base64.b64encode(b"same-password").decode()
monkeypatch.setattr(module, "decrypt", lambda value: value)
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_same, "confirm_new_password": pwd_same})
module.REDIS_CONN.store.pop(v_key, None)
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR, res
module.REDIS_CONN.store[v_key] = "1"
monkeypatch.setattr(module, "decrypt", lambda _value: "")
_set_request_json(monkeypatch, module, {"email": email, "new_password": "", "confirm_new_password": ""})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.ARGUMENT_ERROR, res
monkeypatch.setattr(module, "decrypt", lambda value: value)
module.REDIS_CONN.store[v_key] = "1"
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_a, "confirm_new_password": pwd_b})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.ARGUMENT_ERROR, res
assert "do not match" in res["message"], res
module.REDIS_CONN.store[v_key] = "1"
monkeypatch.setattr(module.UserService, "query_user_by_email", lambda **_kwargs: [])
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_same, "confirm_new_password": pwd_same})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.DATA_ERROR, res
module.REDIS_CONN.store[v_key] = "1"
monkeypatch.setattr(module.UserService, "query_user_by_email", lambda **_kwargs: [user])
def _raise_update_password(_user_id, _new_pwd):
raise RuntimeError("reset boom")
monkeypatch.setattr(module.UserService, "update_user_password", _raise_update_password)
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_same, "confirm_new_password": pwd_same})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.EXCEPTION_ERROR, res
module.REDIS_CONN.store[v_key] = "1"
monkeypatch.setattr(module.UserService, "update_user_password", lambda _user_id, _new_pwd: True)
monkeypatch.setattr(module.REDIS_CONN, "delete", lambda _key: (_ for _ in ()).throw(RuntimeError("delete boom")))
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_same, "confirm_new_password": pwd_same})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.SUCCESS, res
assert res["auth"] == user.get_id(), res
monkeypatch.setattr(module.REDIS_CONN, "delete", lambda key: module.REDIS_CONN.store.pop(key, None))
module.REDIS_CONN.store[v_key] = "1"
_set_request_json(monkeypatch, module, {"email": email, "new_password": pwd_same, "confirm_new_password": pwd_same})
res = _run(module.forget_reset_password())
assert res["code"] == module.RetCode.SUCCESS, res
assert res["auth"] == user.get_id(), res
assert module.REDIS_CONN.get(v_key) is None, module.REDIS_CONN.store
def _load_chat_routes_unit_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[3]
module_name = "test_chat_restful_routes_unit_module_for_tenant"
module_path = repo_root / "api" / "apps" / "restful_apis" / "chat_api.py"
quart_mod = ModuleType("quart")
quart_mod.request = SimpleNamespace(args=SimpleNamespace(get=lambda _key, default=None: default, getlist=lambda _key: []))
quart_mod.Response = type("_StubResponse", (), {})
monkeypatch.setitem(sys.modules, "quart", quart_mod)
api_pkg = ModuleType("api")
api_pkg.__path__ = [str(repo_root / "api")]
monkeypatch.setitem(sys.modules, "api", api_pkg)
apps_pkg = ModuleType("api.apps")
apps_pkg.__path__ = [str(repo_root / "api" / "apps")]
apps_pkg.current_user = SimpleNamespace(id="tenant-1")
apps_pkg.login_required = _passthrough_login_required
monkeypatch.setitem(sys.modules, "api.apps", apps_pkg)
api_pkg.apps = apps_pkg
common_pkg = ModuleType("common")
common_pkg.__path__ = [str(repo_root / "common")]
monkeypatch.setitem(sys.modules, "common", common_pkg)
settings_mod = ModuleType("common.settings")
settings_mod.STORAGE_IMPL = type("_StorageImpl", (), {"rm": staticmethod(lambda *_args, **_kwargs: None)})()
monkeypatch.setitem(sys.modules, "common.settings", settings_mod)
constants_mod = ModuleType("common.constants")
constants_mod.LLMType = SimpleNamespace(CHAT="chat", IMAGE2TEXT="image2text", RERANK="rerank", SPEECH2TEXT="speech2text", TTS="tts")
constants_mod.RetCode = SimpleNamespace(SUCCESS=0, DATA_ERROR=102, OPERATING_ERROR=103, AUTHENTICATION_ERROR=109)
constants_mod.StatusEnum = SimpleNamespace(VALID=SimpleNamespace(value="1"), INVALID=SimpleNamespace(value="0"))
from common.constants import MAXIMUM_PAGE_NUMBER as _MPN, MAXIMUM_TASK_PAGE_NUMBER as _MTPN
constants_mod.MAXIMUM_PAGE_NUMBER = _MPN
constants_mod.MAXIMUM_TASK_PAGE_NUMBER = _MTPN
monkeypatch.setitem(sys.modules, "common.constants", constants_mod)
misc_utils_mod = ModuleType("common.misc_utils")
misc_utils_mod.get_uuid = lambda: "generated-chat-id"
async def _thread_pool_exec(func, *args, **kwargs):
return func(*args, **kwargs)
misc_utils_mod.thread_pool_exec = _thread_pool_exec
monkeypatch.setitem(sys.modules, "common.misc_utils", misc_utils_mod)
dialog_service_mod = ModuleType("api.db.services.dialog_service")
class _DialogService:
model = SimpleNamespace(_meta=SimpleNamespace(fields={
"id": None,
"tenant_id": None,
"name": None,
"description": None,
"icon": None,
"kb_ids": None,
"llm_id": None,
"llm_setting": None,
"prompt_config": None,
"similarity_threshold": None,
"vector_similarity_weight": None,
"top_n": None,
"top_k": None,
"rerank_id": None,
"meta_data_filter": None,
"created_by": None,
"create_time": None,
"create_date": None,
"update_time": None,
"update_date": None,
"status": None,
}))
@staticmethod
def query(**_kwargs):
return []
@staticmethod
def save(**_kwargs):
return True
@staticmethod
def get_by_id(_chat_id):
return False, None
@staticmethod
def get_by_tenant_ids(*_args, **_kwargs):
return [], 0
dialog_service_mod.DialogService = _DialogService
dialog_service_mod.async_ask = lambda *_args, **_kwargs: None
dialog_service_mod.async_chat = lambda *_args, **_kwargs: None
dialog_service_mod.gen_mindmap = lambda *_args, **_kwargs: None
monkeypatch.setitem(sys.modules, "api.db.services.dialog_service", dialog_service_mod)
conversation_service_mod = ModuleType("api.db.services.conversation_service")
conversation_service_mod.ConversationService = type("ConversationService", (), {})
conversation_service_mod.structure_answer = lambda *_args, **_kwargs: {}
monkeypatch.setitem(sys.modules, "api.db.services.conversation_service", conversation_service_mod)
kb_service_mod = ModuleType("api.db.services.knowledgebase_service")
class _KB:
def __init__(self):
self.id = "kb-1"
self.embd_id = "embd@factory"
self.chunk_num = 1
self.name = "Dataset A"
self.status = "1"
kb_service_mod.KnowledgebaseService = type('KnowledgebaseService', (), {
'accessible': staticmethod(lambda **_kwargs: [SimpleNamespace(id='kb-1')]),
'query': staticmethod(lambda **_kwargs: [_KB()]),
'get_by_id': staticmethod(lambda _id: (True, _KB())),
})
monkeypatch.setitem(sys.modules, "api.db.services.knowledgebase_service", kb_service_mod)
tenant_model_provider_mod = ModuleType("api.db.joint_services.tenant_model_service")
tenant_model_provider_mod.get_model_config_from_provider_instance = lambda *_args, **_kwargs: {}
tenant_model_provider_mod.get_tenant_default_model_by_type = lambda *_args, **_kwargs: {}
def _split_model_name(model_name):
parts = model_name.split("@")
if len(parts) == 1:
return parts[0], "", ""
elif len(parts) == 2:
return parts[0], "default", parts[1]
else:
return parts[0], parts[1], parts[2]
tenant_model_provider_mod.split_model_name = staticmethod(_split_model_name)
tenant_model_provider_mod.get_api_key = lambda *_args, **_kwargs: SimpleNamespace(id=1)
monkeypatch.setitem(sys.modules, "api.db.joint_services.tenant_model_service", tenant_model_provider_mod)
llm_service_mod = ModuleType("api.db.services.llm_service")
llm_service_mod.LLMBundle = lambda *_args, **_kwargs: None
monkeypatch.setitem(sys.modules, "api.db.services.llm_service", llm_service_mod)
search_service_mod = ModuleType("api.db.services.search_service")
search_service_mod.SearchService = SimpleNamespace()
monkeypatch.setitem(sys.modules, "api.db.services.search_service", search_service_mod)
user_service_mod = ModuleType("api.db.services.user_service")
user_service_mod.UserService = type('UserService', (), {})
user_service_mod.TenantService = type('TenantService', (), {
'get_by_id': staticmethod(lambda _tenant_id: (True, SimpleNamespace(llm_id='glm-4'))),
'get_joined_tenants_by_user_id': staticmethod(lambda _user_id: [{'tenant_id': 'tenant-1'}, {'tenant_id': 'team-tenant-2'}]),
})
user_service_mod.UserTenantService = type('UserTenantService', (), {'query': staticmethod(lambda **_kwargs: [])})
monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod)
chunk_feedback_service_mod = ModuleType("api.db.services.chunk_feedback_service")
chunk_feedback_service_mod.ChunkFeedbackService = type('ChunkFeedbackService', (), {'apply_feedback': staticmethod(lambda **_kwargs: {'success_count': 0, 'fail_count': 0, 'chunk_ids': []})})
monkeypatch.setitem(sys.modules, "api.db.services.chunk_feedback_service", chunk_feedback_service_mod)
api_utils_mod = ModuleType("api.utils.api_utils")
api_utils_mod.check_duplicate_ids = lambda ids, _label: (list(dict.fromkeys(ids or [])), [])
api_utils_mod.get_data_error_result = lambda message='': {'code': 102, 'data': None, 'message': message}
api_utils_mod.get_json_result = lambda data=None, message='', code=0: {'code': code, 'data': data, 'message': message}
api_utils_mod.server_error_response = lambda ex: {'code': 500, 'data': None, 'message': str(ex)}
api_utils_mod.validate_request = lambda *_args, **_kwargs: (lambda func: func)
api_utils_mod.get_request_json = lambda: _AwaitableValue({})
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
rag_pkg = ModuleType("rag")
rag_pkg.__path__ = [str(repo_root / 'rag')]
monkeypatch.setitem(sys.modules, 'rag', rag_pkg)
rag_prompts_pkg = ModuleType('rag.prompts')
rag_prompts_pkg.__path__ = [str(repo_root / 'rag' / 'prompts')]
monkeypatch.setitem(sys.modules, 'rag.prompts', rag_prompts_pkg)
rag_prompts_generator_mod = ModuleType('rag.prompts.generator')
rag_prompts_generator_mod.chunks_format = lambda reference: reference.get('chunks', []) if isinstance(reference, dict) else []
monkeypatch.setitem(sys.modules, 'rag.prompts.generator', rag_prompts_generator_mod)
rag_prompts_template_mod = ModuleType('rag.prompts.template')
rag_prompts_template_mod.load_prompt = lambda *_args, **_kwargs: ''
monkeypatch.setitem(sys.modules, 'rag.prompts.template', rag_prompts_template_mod)
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
module.manager = _DummyManager()
monkeypatch.setitem(sys.modules, module_name, module)
spec.loader.exec_module(module)
return module
@pytest.mark.p1
def test_create_chat_uses_tenant_default_llm_when_llm_id_is_null_unit(monkeypatch):
module = _load_chat_routes_unit_module(monkeypatch)
saved = {}
async def _request_json():
return {
'name': 'chat-a',
'dataset_ids': ['kb-1'],
'llm_id': None,
'llm_setting': {'temperature': 0.8},
'prompt_config': {'system': 'Answer with {knowledge}', 'parameters': [{'key': 'knowledge', 'optional': False}]},
}
monkeypatch.setattr(module, 'get_request_json', _request_json)
monkeypatch.setattr(module.DialogService, 'query', lambda **_kwargs: [])
def _save(**kwargs):
saved.update(kwargs)
return True
monkeypatch.setattr(module.DialogService, 'save', _save)
monkeypatch.setattr(module.DialogService, 'get_by_id', lambda _id: (True, SimpleNamespace(to_dict=lambda: saved)))
res = _run(module.create.__wrapped__())
assert res['code'] == 0
assert saved['llm_id'] == 'glm-4'
assert saved['llm_setting']['temperature'] == 0.8
@pytest.mark.p2
def test_list_chats_authorized_multi_tenant_unit(monkeypatch):
module = _load_chat_routes_unit_module(monkeypatch)
captured = {}
monkeypatch.setattr(
module,
'request',
SimpleNamespace(
args=SimpleNamespace(
get=lambda key, default=None: {
'keywords': '', 'page': '1', 'page_size': '10', 'orderby': 'create_time', 'desc': 'true', 'id': None, 'name': None,
}.get(key, default),
getlist=lambda key: ['tenant-1', 'team-tenant-2'] if key == 'owner_ids' else [],
)
),
)
def _get_by_tenant_ids(owner_ids, user_id, *args, **kwargs):
captured['owner_ids'] = owner_ids
captured['user_id'] = user_id
return ([{'id': 'c1', 'tenant_id': 'tenant-1'}, {'id': 'c2', 'tenant_id': 'team-tenant-2'}], 2)
monkeypatch.setattr(module.DialogService, 'get_by_tenant_ids', _get_by_tenant_ids)
monkeypatch.setattr(module.KnowledgebaseService, 'get_by_id', lambda _id: (False, None))
res = _run(module.list_chats.__wrapped__())
assert res['code'] == 0
assert res['data']['total'] == 2
assert {c['id'] for c in res['data']['chats']} == {'c1', 'c2'}
assert set(captured['owner_ids']) == {'tenant-1', 'team-tenant-2'}
assert captured['user_id'] == 'tenant-1'