diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dae2114710..fc4233504b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -247,7 +247,7 @@ jobs: echo "Waiting for service to be available... (last exit code: $?)" sleep 5 done - source .venv/bin/activate && set -o pipefail; DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_web_api/test_api_app test/testcases/test_web_api/test_chunk_feedback 2>&1 | tee infinity_web_api_test.log + source .venv/bin/activate && set -o pipefail; DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} test/testcases/test_web_api/test_chunk_feedback 2>&1 | tee infinity_web_api_test.log - name: Run http api tests against Infinity run: | diff --git a/api/apps/api_app.py b/api/apps/api_app.py index 97d7dc9430..0d5d62334e 100644 --- a/api/apps/api_app.py +++ b/api/apps/api_app.py @@ -15,73 +15,11 @@ # from datetime import datetime, timedelta from quart import request -from api.db.db_models import APIToken -from api.db.services.api_service import APITokenService, API4ConversationService +from api.db.services.api_service import API4ConversationService from api.db.services.user_service import UserTenantService -from api.utils.api_utils import generate_confirmation_token, get_data_error_result, get_json_result, get_request_json, server_error_response, validate_request -from common.time_utils import current_timestamp, datetime_format +from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response from api.apps import login_required, current_user - -@manager.route('/new_token', methods=['POST']) # noqa: F821 -@login_required -async def new_token(): - req = await get_request_json() - try: - tenants = UserTenantService.query(user_id=current_user.id) - if not tenants: - return get_data_error_result(message="Tenant not found!") - - tenant_id = tenants[0].tenant_id - obj = {"tenant_id": tenant_id, "token": generate_confirmation_token(), - "create_time": current_timestamp(), - "create_date": datetime_format(datetime.now()), - "update_time": None, - "update_date": None - } - if req.get("canvas_id"): - obj["dialog_id"] = req["canvas_id"] - obj["source"] = "agent" - else: - obj["dialog_id"] = req["dialog_id"] - - if not APITokenService.save(**obj): - return get_data_error_result(message="Fail to new a dialog!") - - return get_json_result(data=obj) - except Exception as e: - return server_error_response(e) - - -@manager.route('/token_list', methods=['GET']) # noqa: F821 -@login_required -def token_list(): - try: - tenants = UserTenantService.query(user_id=current_user.id) - if not tenants: - return get_data_error_result(message="Tenant not found!") - - id = request.args["dialog_id"] if "dialog_id" in request.args else request.args["canvas_id"] - objs = APITokenService.query(tenant_id=tenants[0].tenant_id, dialog_id=id) - return get_json_result(data=[o.to_dict() for o in objs]) - except Exception as e: - return server_error_response(e) - - -@manager.route('/rm', methods=['POST']) # noqa: F821 -@validate_request("tokens", "tenant_id") -@login_required -async def rm(): - req = await get_request_json() - try: - for token in req["tokens"]: - APITokenService.filter_delete( - [APIToken.tenant_id == req["tenant_id"], APIToken.token == token]) - return get_json_result(data=True) - except Exception as e: - return server_error_response(e) - - @manager.route('/stats', methods=['GET']) # noqa: F821 @login_required def stats(): diff --git a/test/testcases/test_web_api/test_api_app/test_api_tokens.py b/test/testcases/test_web_api/test_api_app/test_api_tokens.py deleted file mode 100644 index 6b6a38da1f..0000000000 --- a/test/testcases/test_web_api/test_api_app/test_api_tokens.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright 2025 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 pytest -from test_common import api_new_token, api_rm_token, api_stats, api_token_list, batch_create_chats -from configs import INVALID_API_TOKEN -from libs.auth import RAGFlowWebApiAuth - - -INVALID_AUTH_CASES = [ - (None, 401, "Unauthorized"), - (RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "Unauthorized"), -] - - -class TestAuthorization: - @pytest.mark.p2 - @pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES) - def test_auth_invalid_new_token(self, invalid_auth, expected_code, expected_fragment): - res = api_new_token(invalid_auth, {"dialog_id": "dummy_dialog_id"}) - assert res["code"] == expected_code, res - assert expected_fragment in res["message"], res - - @pytest.mark.p2 - @pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES) - def test_auth_invalid_token_list(self, invalid_auth, expected_code, expected_fragment): - res = api_token_list(invalid_auth, {"dialog_id": "dummy_dialog_id"}) - assert res["code"] == expected_code, res - assert expected_fragment in res["message"], res - - @pytest.mark.p2 - @pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES) - def test_auth_invalid_rm(self, invalid_auth, expected_code, expected_fragment): - res = api_rm_token(invalid_auth, {"tokens": ["dummy_token"], "tenant_id": "dummy_tenant"}) - assert res["code"] == expected_code, res - assert expected_fragment in res["message"], res - - @pytest.mark.p2 - @pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES) - def test_auth_invalid_stats(self, invalid_auth, expected_code, expected_fragment): - res = api_stats(invalid_auth) - assert res["code"] == expected_code, res - assert expected_fragment in res["message"], res - - -@pytest.mark.usefixtures("clear_chats") -class TestApiTokens: - @pytest.mark.p2 - def test_token_lifecycle(self, WebApiAuth): - chat_id = batch_create_chats(WebApiAuth, 1)[0] - create_res = api_new_token(WebApiAuth, {"dialog_id": chat_id}) - assert create_res["code"] == 0, create_res - token = create_res["data"]["token"] - tenant_id = create_res["data"]["tenant_id"] - - list_res = api_token_list(WebApiAuth, {"dialog_id": chat_id}) - assert list_res["code"] == 0, list_res - assert any(item["token"] == token for item in list_res["data"]), list_res - - rm_res = api_rm_token(WebApiAuth, {"tokens": [token], "tenant_id": tenant_id}) - assert rm_res["code"] == 0, rm_res - assert rm_res["data"] is True, rm_res - - @pytest.mark.p2 - def test_stats_basic(self, WebApiAuth): - res = api_stats(WebApiAuth) - assert res["code"] == 0, res - for key in ["pv", "uv", "speed", "tokens", "round", "thumb_up"]: - assert key in res["data"], res - - @pytest.mark.p3 - def test_rm_missing_tokens(self, WebApiAuth): - res = api_rm_token(WebApiAuth, {"tenant_id": "dummy_tenant"}) - assert res["code"] == 101, res - assert "required argument are missing" in res["message"], res diff --git a/test/testcases/test_web_api/test_api_app/test_api_tokens_unit.py b/test/testcases/test_web_api/test_api_app/test_api_tokens_unit.py deleted file mode 100644 index b5c3d56546..0000000000 --- a/test/testcases/test_web_api/test_api_app/test_api_tokens_unit.py +++ /dev/null @@ -1,247 +0,0 @@ -# -# 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 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 _ExprField: - def __init__(self, name): - self.name = name - - def __eq__(self, other): - return (self.name, other) - - -class _DummyAPITokenModel: - tenant_id = _ExprField("tenant_id") - token = _ExprField("token") - - -def _run(coro): - return asyncio.run(coro) - - -def _load_api_app(monkeypatch): - repo_root = Path(__file__).resolve().parents[4] - - quart_mod = ModuleType("quart") - quart_mod.request = SimpleNamespace(args={}) - monkeypatch.setitem(sys.modules, "quart", quart_mod) - - apps_mod = ModuleType("api.apps") - apps_mod.__path__ = [str(repo_root / "api" / "apps")] - apps_mod.login_required = lambda fn: fn - apps_mod.current_user = SimpleNamespace(id="user-1") - monkeypatch.setitem(sys.modules, "api.apps", apps_mod) - - api_utils_mod = ModuleType("api.utils.api_utils") - - async def _get_request_json(): - return {} - - api_utils_mod.generate_confirmation_token = lambda: "token-123" - api_utils_mod.get_request_json = _get_request_json - 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=400, data=None: { - "code": code, - "message": message, - "data": data, - } - api_utils_mod.server_error_response = lambda exc: { - "code": 500, - "message": str(exc), - "data": None, - } - api_utils_mod.validate_request = lambda *_args, **_kwargs: (lambda fn: fn) - monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod) - - api_service_mod = ModuleType("api.db.services.api_service") - - class _StubAPITokenService: - @staticmethod - def save(**_kwargs): - return True - - @staticmethod - def query(**_kwargs): - return [] - - @staticmethod - def filter_delete(_conds): - return True - - class _StubAPI4ConversationService: - @staticmethod - def stats(*_args, **_kwargs): - return [] - - api_service_mod.APITokenService = _StubAPITokenService - api_service_mod.API4ConversationService = _StubAPI4ConversationService - monkeypatch.setitem(sys.modules, "api.db.services.api_service", api_service_mod) - - user_service_mod = ModuleType("api.db.services.user_service") - - class _StubUserTenantService: - @staticmethod - def query(**_kwargs): - return [SimpleNamespace(tenant_id="tenant-1")] - - user_service_mod.UserTenantService = _StubUserTenantService - monkeypatch.setitem(sys.modules, "api.db.services.user_service", user_service_mod) - - db_models_mod = ModuleType("api.db.db_models") - db_models_mod.APIToken = _DummyAPITokenModel - monkeypatch.setitem(sys.modules, "api.db.db_models", db_models_mod) - - time_utils_mod = ModuleType("common.time_utils") - time_utils_mod.current_timestamp = lambda: 123 - time_utils_mod.datetime_format = lambda _dt: "2026-01-01 00:00:00" - monkeypatch.setitem(sys.modules, "common.time_utils", time_utils_mod) - - module_path = repo_root / "api" / "apps" / "api_app.py" - spec = importlib.util.spec_from_file_location("test_api_tokens_unit_module", module_path) - module = importlib.util.module_from_spec(spec) - module.manager = _DummyManager() - spec.loader.exec_module(module) - return module - - -@pytest.mark.p2 -def test_new_token_branches_and_error_paths(monkeypatch): - module = _load_api_app(monkeypatch) - - async def req_canvas(): - return {"canvas_id": "canvas-1"} - - monkeypatch.setattr(module, "get_request_json", req_canvas) - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - res = _run(module.new_token()) - assert res["message"] == "Tenant not found!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")]) - monkeypatch.setattr(module.APITokenService, "save", lambda **_kwargs: True) - res = _run(module.new_token()) - assert res["code"] == 0 - assert res["data"]["tenant_id"] == "tenant-1" - assert res["data"]["dialog_id"] == "canvas-1" - assert res["data"]["source"] == "agent" - - monkeypatch.setattr(module.APITokenService, "save", lambda **_kwargs: False) - res = _run(module.new_token()) - assert res["message"] == "Fail to new a dialog!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("query failed"))) - res = _run(module.new_token()) - assert res["code"] == 500 - assert "query failed" in res["message"] - - -@pytest.mark.p2 -def test_token_list_tenant_guard_and_exception(monkeypatch): - module = _load_api_app(monkeypatch) - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - monkeypatch.setattr(module, "request", SimpleNamespace(args={"dialog_id": "d1"})) - res = module.token_list() - assert res["message"] == "Tenant not found!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")]) - monkeypatch.setattr(module, "request", SimpleNamespace(args={})) - res = module.token_list() - assert res["code"] == 500 - assert "canvas_id" in res["message"] - - -@pytest.mark.p2 -def test_rm_exception_path(monkeypatch): - module = _load_api_app(monkeypatch) - - async def req_rm(): - return {"tokens": ["tok-1"], "tenant_id": "tenant-1"} - - monkeypatch.setattr(module, "get_request_json", req_rm) - monkeypatch.setattr( - module.APITokenService, - "filter_delete", - lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("delete failed")), - ) - - res = _run(module.rm()) - assert res["code"] == 500 - assert "delete failed" in res["message"] - - -@pytest.mark.p2 -def test_stats_aggregation_and_error_paths(monkeypatch): - module = _load_api_app(monkeypatch) - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: []) - monkeypatch.setattr(module, "request", SimpleNamespace(args={})) - res = module.stats() - assert res["message"] == "Tenant not found!" - - monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")]) - monkeypatch.setattr(module, "request", SimpleNamespace(args={"canvas_id": "canvas-1"})) - monkeypatch.setattr( - module.API4ConversationService, - "stats", - lambda *_args, **_kwargs: [ - { - "dt": "2026-01-01", - "pv": 3, - "uv": 2, - "tokens": 100, - "duration": 9.9, - "round": 1, - "thumb_up": 0, - } - ], - ) - res = module.stats() - assert res["code"] == 0 - assert res["data"]["pv"] == [("2026-01-01", 3)] - assert res["data"]["uv"] == [("2026-01-01", 2)] - assert res["data"]["round"] == [("2026-01-01", 1)] - assert res["data"]["thumb_up"] == [("2026-01-01", 0)] - assert res["data"]["tokens"] == [("2026-01-01", 0.1)] - assert res["data"]["speed"] == [("2026-01-01", 10.0)] - - monkeypatch.setattr( - module.API4ConversationService, - "stats", - lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("stats failed")), - ) - res = module.stats() - assert res["code"] == 500 - assert "stats failed" in res["message"] diff --git a/test/testcases/test_web_api/test_common.py b/test/testcases/test_web_api/test_common.py index 490ff5b0a6..f1aca63446 100644 --- a/test/testcases/test_web_api/test_common.py +++ b/test/testcases/test_web_api/test_common.py @@ -68,25 +68,6 @@ def _log_http_debug(method, url, req_id, payload, status, text, resp_json, elaps print(f"[HTTP DEBUG] response_text={text}") print(f"[HTTP DEBUG] response_json={json.dumps(resp_json, default=str) if resp_json is not None else None}") - -# API APP -def api_new_token(auth, payload=None, *, headers=HEADERS, data=None): - if payload is None: - payload = {} - res = requests.post(url=f"{HOST_ADDRESS}{API_APP_URL}/new_token", headers=headers, auth=auth, json=payload, data=data) - return res.json() - - -def api_token_list(auth, params=None, *, headers=HEADERS): - res = requests.get(url=f"{HOST_ADDRESS}{API_APP_URL}/token_list", headers=headers, auth=auth, params=params) - return res.json() - - -def api_rm_token(auth, payload=None, *, headers=HEADERS, data=None): - res = requests.post(url=f"{HOST_ADDRESS}{API_APP_URL}/rm", headers=headers, auth=auth, json=payload, data=data) - return res.json() - - def api_stats(auth, params=None, *, headers=HEADERS): res = requests.get(url=f"{HOST_ADDRESS}{API_APP_URL}/stats", headers=headers, auth=auth, params=params) return res.json() diff --git a/web/src/pages/dataset/dataset-overview/hook.ts b/web/src/pages/dataset/dataset-overview/hook.ts index 5afbd258e5..679d90be04 100644 --- a/web/src/pages/dataset/dataset-overview/hook.ts +++ b/web/src/pages/dataset/dataset-overview/hook.ts @@ -13,7 +13,7 @@ import { useParams, useSearchParams } from 'react-router'; import { LogTabs } from './dataset-common'; import { IFileLogList, IOverviewTotal } from './interface'; -const useFetchOverviewTital = () => { +const useFetchOverviewTotal = () => { const [searchParams] = useSearchParams(); const { id } = useParams(); const knowledgeBaseId = searchParams.get('id') || id; @@ -95,4 +95,4 @@ const useFetchFileLogList = () => { }; }; -export { useFetchFileLogList, useFetchOverviewTital }; +export { useFetchFileLogList, useFetchOverviewTotal }; diff --git a/web/src/pages/dataset/dataset-overview/index.tsx b/web/src/pages/dataset/dataset-overview/index.tsx index 78f450d712..fdcf6c20ac 100644 --- a/web/src/pages/dataset/dataset-overview/index.tsx +++ b/web/src/pages/dataset/dataset-overview/index.tsx @@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next'; import { RunningStatus } from '../dataset/constant'; import { LogTabs } from './dataset-common'; import { DatasetFilter } from './dataset-filter'; -import { useFetchFileLogList, useFetchOverviewTital } from './hook'; +import { useFetchFileLogList, useFetchOverviewTotal } from './hook'; import { DocumentLog, IFileLogItem } from './interface'; import FileLogsTable from './overview-table'; @@ -133,7 +133,7 @@ const FileLogsPage: FC = () => { failed: 0, }, }); - const { data: topData } = useFetchOverviewTital(); + const { data: topData } = useFetchOverviewTotal(); const { pagination: { total: fileTotal }, } = useFetchDocumentList(false);