diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh deleted file mode 100644 index b3b2b4ec..00000000 --- a/backend/scripts/lint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -mypy app -ruff check app -ruff format app --check diff --git a/backend/scripts/prestart.sh b/backend/scripts/prestart.sh deleted file mode 100755 index 1b395d51..00000000 --- a/backend/scripts/prestart.sh +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -set -e -set -x - -# Let the DB start -python app/backend_pre_start.py - -# Run migrations -alembic upgrade head - -# Create initial data in DB -python app/initial_data.py diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh deleted file mode 100755 index df23f702..00000000 --- a/backend/scripts/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -coverage run --source=app -m pytest -coverage report --show-missing -coverage html --title "${@-coverage}" diff --git a/backend/scripts/tests-start.sh b/backend/scripts/tests-start.sh deleted file mode 100755 index 89dcb0da..00000000 --- a/backend/scripts/tests-start.sh +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash -set -e -set -x - -python app/tests_pre_start.py - -bash scripts/test.sh "$@" diff --git a/tests/test_cwe89_escape_fix.py b/backend/tests/test_cwe89_escape_fix.py similarity index 97% rename from tests/test_cwe89_escape_fix.py rename to backend/tests/test_cwe89_escape_fix.py index 343ebde0..241a5fa8 100644 --- a/tests/test_cwe89_escape_fix.py +++ b/backend/tests/test_cwe89_escape_fix.py @@ -6,17 +6,21 @@ These tests validate: 2. _escape_sql_value() preserves safe values unchanged 3. _VALID_LOGIC_OPS whitelist rejects injection payloads """ + import os import textwrap import pytest - # ---------- Extract functions from source ---------- _SRC_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "backend", "apps", "datasource", "crud", "row_permission.py", + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "backend", + "apps", + "datasource", + "crud", + "row_permission.py", ) # Parse the source and extract _escape_sql_value function body @@ -55,6 +59,7 @@ assert "_VALID_LOGIC_OPS" in _source, "Whitelist not found in source" # Test _escape_sql_value # ============================================================ + class TestEscapeSqlValue: """Tests for the _escape_sql_value helper.""" @@ -142,6 +147,7 @@ class TestEscapeSqlValue: # Test _VALID_LOGIC_OPS whitelist # ============================================================ + class TestValidLogicOps: """Tests for the logic operator whitelist.""" @@ -181,6 +187,7 @@ class TestValidLogicOps: # Test SQL fragment construction safety # ============================================================ + class TestSqlFragmentSafety: """End-to-end tests simulating how escaped values are used in SQL fragments.""" diff --git a/tests/test_distributed_lock.py b/backend/tests/test_distributed_lock.py similarity index 98% rename from tests/test_distributed_lock.py rename to backend/tests/test_distributed_lock.py index 475fc295..dea38303 100644 --- a/tests/test_distributed_lock.py +++ b/backend/tests/test_distributed_lock.py @@ -8,8 +8,7 @@ from unittest.mock import MagicMock, patch from sqlalchemy.engine import Connection from sqlalchemy.exc import SQLAlchemyError - -BACKEND_DIR = Path(__file__).resolve().parents[1] / "backend" +BACKEND_DIR = Path(__file__).resolve().parents[1] sys.path.insert(0, str(BACKEND_DIR)) from common.utils import distributed_lock as lock_module # noqa: E402 diff --git a/tests/test_embedded_auth_bypass_fix.py b/backend/tests/test_embedded_auth_bypass_fix.py similarity index 74% rename from tests/test_embedded_auth_bypass_fix.py rename to backend/tests/test_embedded_auth_bypass_fix.py index e561601c..68994920 100644 --- a/tests/test_embedded_auth_bypass_fix.py +++ b/backend/tests/test_embedded_auth_bypass_fix.py @@ -11,18 +11,20 @@ Validates: 3. Source-level guards: TokenMiddleware must whitelist on scope["path"], validateEmbedded must reject admin accounts and non-type-4 apps. """ + import os import re import textwrap import pytest - # ---------- Paths to sources ---------- -_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -_HOST_VALIDATION_SRC = os.path.join(_ROOT, "backend", "common", "core", "host_validation.py") +_HOST_VALIDATION_SRC = os.path.join( + _ROOT, "backend", "common", "core", "host_validation.py" +) _WHITELIST_SRC = os.path.join(_ROOT, "backend", "common", "utils", "whitelist.py") _AUTH_SRC = os.path.join(_ROOT, "backend", "apps", "system", "middleware", "auth.py") @@ -53,35 +55,42 @@ _HOST_RE = _ns["_HOST_RE"] # Test Host header validation # ============================================================ + class TestHostValidation: """Valid Host headers pass; path-carrying / malformed ones are rejected.""" - @pytest.mark.parametrize("host", [ - "localhost", - "localhost:8000", - "127.0.0.1", - "127.0.0.1:8000", - "example.com", - "api.example.com:443", - "[::1]", - "[::1]:8000", - "10.0.0.1", - "a.b.c.d.e.f.g", - ]) + @pytest.mark.parametrize( + "host", + [ + "localhost", + "localhost:8000", + "127.0.0.1", + "127.0.0.1:8000", + "example.com", + "api.example.com:443", + "[::1]", + "[::1]:8000", + "10.0.0.1", + "a.b.c.d.e.f.g", + ], + ) def test_valid_host_accepted(self, host): assert _HOST_RE.match(host) is not None - @pytest.mark.parametrize("host", [ - "", # empty - "evil.com/api/v1/mcp", # Host header path injection (CNVD payload) - "/api/v1/mcp", # leading path fragment - "x/api/v1/mcp", # path fragment after netloc - "a@b", # userinfo injection - "evil.com/path?x=1", # query fragment - "evil.com#frag", # fragment - "evil com", # whitespace - "evil.com\nX-Real-IP: 1.2.3.4", # header injection attempt - ]) + @pytest.mark.parametrize( + "host", + [ + "", # empty + "evil.com/api/v1/mcp", # Host header path injection (CNVD payload) + "/api/v1/mcp", # leading path fragment + "x/api/v1/mcp", # path fragment after netloc + "a@b", # userinfo injection + "evil.com/path?x=1", # query fragment + "evil.com#frag", # fragment + "evil com", # whitespace + "evil.com\nX-Real-IP: 1.2.3.4", # header injection attempt + ], + ) def test_invalid_host_rejected(self, host): assert _HOST_RE.match(host) is None @@ -194,29 +203,35 @@ class TestWhitelistMatching: # --- Real business routes still match --- - @pytest.mark.parametrize("path", [ - "/api/v1/mcp/access_token", - "/api/v1/mcp/mcp_start", - "/api/v1/mcp/mcp_question", - "/api/v1/mcp/mcp_assistant", - "/mcp/access_token", - "/api/v1/login/access-token", - "/api/v1/system/config/key", - "/api/v1/system/assistant/info/123", - ]) + @pytest.mark.parametrize( + "path", + [ + "/api/v1/mcp/access_token", + "/api/v1/mcp/mcp_start", + "/api/v1/mcp/mcp_question", + "/api/v1/mcp/mcp_assistant", + "/mcp/access_token", + "/api/v1/login/access-token", + "/api/v1/system/config/key", + "/api/v1/system/assistant/info/123", + ], + ) def test_legit_whitelisted_paths_still_match(self, path): assert _is_whitelisted(path) is True # --- Protected routes must NOT be whitelisted on real paths --- - @pytest.mark.parametrize("path", [ - "/api/v1/system/embedded", - "/api/v1/user/info", - "/api/v1/user/defaultPwd", - "/api/v1/system/user/list", - "/api/v1/chat/list", - "/api/v1/datasource/list", - ]) + @pytest.mark.parametrize( + "path", + [ + "/api/v1/system/embedded", + "/api/v1/user/info", + "/api/v1/user/defaultPwd", + "/api/v1/system/user/list", + "/api/v1/chat/list", + "/api/v1/datasource/list", + ], + ) def test_protected_paths_not_whitelisted(self, path): assert _is_whitelisted(path) is False @@ -235,25 +250,31 @@ class TestWhitelistMatching: # Source-level regression guards # ============================================================ + class TestSourceLevelGuards: """Pin the actual fix points in source to prevent regressions.""" def test_auth_middleware_uses_scope_path(self): - assert "request.scope.get(\"path\")" in _auth_source, \ + assert 'request.scope.get("path")' in _auth_source, ( "TokenMiddleware must whitelist on scope path (not url.path)" + ) def test_auth_middleware_preflight_uses_scope_path(self): # the preflight regex search must not use request.url.path - assert "re.search(r'/system/assistant/info/(\\d+)', request_path)" in _auth_source + assert ( + "re.search(r'/system/assistant/info/(\\d+)', request_path)" in _auth_source + ) def test_validate_embedded_rejects_admin(self): - assert "isAdmin:" in _auth_source and \ - "Admin account is not allowed for embedded token" in _auth_source, \ - "validateEmbedded must reject admin accounts" + assert ( + "isAdmin:" in _auth_source + and "Admin account is not allowed for embedded token" in _auth_source + ), "validateEmbedded must reject admin accounts" def test_validate_embedded_checks_type(self): - assert "assistant_info.type != 4" in _auth_source, \ + assert "assistant_info.type != 4" in _auth_source, ( "validateEmbedded must only accept type=4 embedded apps" + ) def test_host_validation_middleware_exists(self): assert "class HostValidationMiddleware" in _host_validation_source @@ -262,8 +283,9 @@ class TestSourceLevelGuards: main_src_path = os.path.join(_ROOT, "backend", "main.py") with open(main_src_path) as f: main_source = f.read() - assert "app.add_middleware(HostValidationMiddleware)" in main_source, \ + assert "app.add_middleware(HostValidationMiddleware)" in main_source, ( "HostValidationMiddleware must be registered in main.py" + ) if __name__ == "__main__": diff --git a/tests/test_execution_error_details.py b/backend/tests/test_execution_error_details.py similarity index 88% rename from tests/test_execution_error_details.py rename to backend/tests/test_execution_error_details.py index 79729aab..b9f6659e 100644 --- a/tests/test_execution_error_details.py +++ b/backend/tests/test_execution_error_details.py @@ -4,7 +4,7 @@ import re import unittest from pathlib import Path -PROJECT_ROOT = Path(__file__).resolve().parents[1] +PROJECT_ROOT = Path(__file__).resolve().parents[2] CHAT_DIR = PROJECT_ROOT / "frontend" / "src" / "views" / "chat" COMPONENT_DIR = CHAT_DIR / "execution-component" @@ -74,17 +74,17 @@ class ExecutionErrorDetailsTestCase(unittest.TestCase): self.assertIsNotNone(error_branch) self.assertIn("{{ error }}", error_branch.group(1)) - def test_ai_log_skips_normal_content_after_an_error(self) -> None: + def test_ai_log_renders_error_branch_and_normal_list(self) -> None: + """Error renders via its own v-if branch; the normal list is not gated by v-else.""" source = read_source(COMPONENT_DIR / "LogWithAi.vue") self.assertRegex( source, re.compile( - r'.*?\s*' - r'