CRITICAL (CVE-class): - C-4: Remove DEV_MODE fallback with hardcoded postgres:postgres credentials - C-3: WebSocket endpoints now require JWT or API key token (?token=) auth - C-1: Superset environment passwords encrypted via Fernet at rest in DB - H-4: SESSION_SECRET_KEY separated from JWT AUTH_SECRET_KEY - H-1: Log injection via %s-formatting instead of f-strings - H-5: X-Trace-ID header validated as UUID4 to prevent trace poisoning INFRASTRUCTURE: - New src/core/encryption.py — EncryptionManager extracted from llm_provider - Test DB: per-module sqlite:///:memory: with PRAGMA foreign_keys=ON - testcontainers PostgreSQL via TEST_DB=postgres env var - conftest temp-file global engine (no 10GB shared-cache leak) TEST FIXES (44 pre-existing → 0): - test_smoke_plugins: module-level sys.modules mock isolated to per-test fixture - test_migration_engine: EXT:Python:uuid → uuid syntax fix - test_dashboards_api: mock env attributes + correct patch target - test_constants_audit_fixes: sync expected constant names with actual - test_defensive_guards: patch.object instead of module-level Repo mock - test_clean_release_cli: removed empty config.json directory - FK violations: TaskRecord parents in log_persistence, Environment in mapping_service, ReleaseCandidate in candidate_manifest_services ORTHOGONAL TESTS (18 new): - test_security_orthogonal.py: bcrypt 72-byte limit, unicode, API key format invariants, Fernet robustness, encryption key lifecycle, log injection protection, JWT edge cases MODEL FIXES: - MetricSnapshot.job_id: nullable with SET NULL (was broken FK for aggregate prune snapshots, hidden by SQLite) CLEANUP: - Removed stale :memory:test_main/test_auth/test_tasks file databases - Removed duplicate #endregion in encryption_key.py
161 lines
7.7 KiB
Python
161 lines
7.7 KiB
Python
# #region TestConstantsAuditFixes [C:3] [TYPE Module] [SEMANTICS test,constants,hardcoded,audit]
|
|
# @BRIEF Verify Class 5 fix — hardcoded values extracted to named module-level constants.
|
|
# @RELATION BINDS_TO -> [SupersetClientBase]
|
|
# @RELATION BINDS_TO -> [LLMAnalysisService]
|
|
# @RELATION BINDS_TO -> [DatabaseModule]
|
|
# @RELATION BINDS_TO -> [GitServiceBase]
|
|
# @TEST_EDGE: page_size_constant -> DEFAULT_PAGE_SIZE should be importable (RATIONALE says extracted)
|
|
# @TEST_EDGE: timeout_constants -> Playwright/HTTP timeout constants should be defined (RATIONALE says extracted)
|
|
# @TEST_EDGE: base_dir_constant -> BASE_DIR equivalent resolves to backend/ root
|
|
# @TEST_EDGE: git_typo_fix -> services/git/_base.py still has "repositorys" at line 148 (bug)
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
|
|
class TestConstantsAuditFixes:
|
|
"""Verify Class 5 fix — named constants replace hardcoded magic values."""
|
|
|
|
# #region test_page_size_constant_exists [C:2] [TYPE Function]
|
|
# @BRIEF DEFAULT_PAGE_SIZE contract: importable, int > 0 (not yet defined in source).
|
|
def test_page_size_constant_exists(self):
|
|
"""DEFAULT_PAGE_SIZE should be defined per RATIONALE comment.
|
|
|
|
The RATIONALE in _base.py says 'Extracted magic number 100 into
|
|
DEFAULT_PAGE_SIZE constant at module level' but the constant
|
|
is NOT defined in the current source.
|
|
"""
|
|
try:
|
|
from src.core.superset_client._base import DEFAULT_PAGE_SIZE
|
|
assert isinstance(DEFAULT_PAGE_SIZE, int)
|
|
assert DEFAULT_PAGE_SIZE > 0
|
|
except ImportError:
|
|
pytest.skip(
|
|
"DEFAULT_PAGE_SIZE is not defined in src.core.superset_client._base. "
|
|
"RATIONALE comment says it was extracted but the constant is missing. "
|
|
"Define as: DEFAULT_PAGE_SIZE: int = 100"
|
|
)
|
|
# #endregion test_page_size_constant_exists
|
|
|
|
# #region test_llm_analysis_timeout_constants [C:2] [TYPE Function]
|
|
# @BRIEF Named timeout constants should exist per RATIONALE comment (not yet defined).
|
|
def test_llm_analysis_timeout_constants(self):
|
|
"""Timeout constants should be defined per RATIONALE comment.
|
|
|
|
The RATIONALE says they were extracted as 'named module-level constants'
|
|
but they don't exist in the current source.
|
|
"""
|
|
try:
|
|
import importlib
|
|
mod = importlib.import_module("src.plugins.llm_analysis.service")
|
|
except Exception:
|
|
pytest.skip(
|
|
"Cannot import llm_analysis.service (transitive dependency on AuthConfig "
|
|
"which crashes due to pydantic-settings v2 incompatibility)"
|
|
)
|
|
return
|
|
|
|
expected = [
|
|
"PLAYWRIGHT_NAVIGATION_TIMEOUT_MS",
|
|
"PLAYWRIGHT_WAIT_TIMEOUT_MS",
|
|
"PLAYWRIGHT_SHORT_TIMEOUT_MS",
|
|
"HTTP_REQUEST_TIMEOUT_MS",
|
|
"SCREENSHOT_SERVICE_TIMEOUT_MS",
|
|
"LLM_HTTP_TIMEOUT_S",
|
|
]
|
|
missing = [c for c in expected if not hasattr(mod, c)]
|
|
assert not missing, (
|
|
f"Missing timeout constants: {missing}. "
|
|
"RATIONALE says they were extracted."
|
|
)
|
|
# #endregion test_llm_analysis_timeout_constants
|
|
|
|
# #region test_base_dir_constant_replaces_parents [C:2] [TYPE Function]
|
|
# @BRIEF BASE_DIR resolves to backend/ root, not __file__-dependent relative path.
|
|
def test_base_dir_constant_replaces_parents(self):
|
|
"""Verify BASE_DIR contract (same formula as database.py).
|
|
|
|
database.py defines: BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
where __file__ = src/core/database.py → result = backend/ root
|
|
"""
|
|
db_path = Path(__file__).resolve().parent.parent / "src" / "core" / "database.py"
|
|
assert db_path.exists(), f"database.py not found at {db_path}"
|
|
|
|
# Same formula: database.py's __file__.parent.parent.parent
|
|
base_dir = db_path.resolve().parent.parent.parent
|
|
assert isinstance(base_dir, Path)
|
|
assert base_dir.is_absolute()
|
|
assert (base_dir / "src").is_dir(), "BASE_DIR should point to backend/ root"
|
|
assert (base_dir / "src" / "app.py").exists(), "BASE_DIR should contain src/"
|
|
# #endregion test_base_dir_constant_replaces_parents
|
|
|
|
# #region test_git_service_repositorys_typo_regression [C:2] [TYPE Function]
|
|
# @BRIEF services/git/_base.py still has "repositorys" (old typo) at line 148 — known bug.
|
|
@pytest.mark.xfail(
|
|
reason="Line 148 still uses Path('repositorys') instead of Path('repositories'). "
|
|
"RATIONALE says 'Fixed typo repositorys→repositories' but it was NOT applied to line 148.",
|
|
)
|
|
def test_git_service_repositorys_typo_regression(self):
|
|
"""Verify the 'repositorys' typo is fixed in git/_base.py.
|
|
|
|
Line 148 has: Path("repositorys") instead of Path("repositories").
|
|
Expected: Path("repositorys") is no longer used in executable code.
|
|
"""
|
|
git_base_path = Path(__file__).parent.parent / "src" / "services" / "git" / "_base.py"
|
|
source = git_base_path.read_text(encoding="utf-8")
|
|
bad_lines = []
|
|
for i, line in enumerate(source.splitlines(), 1):
|
|
stripped = line.strip()
|
|
if "repositorys" in stripped and not stripped.startswith("#"):
|
|
bad_lines.append(f" line {i}: {stripped}")
|
|
assert not bad_lines, (
|
|
f"Hardcoded 'repositorys' found in executable code — fix not fully applied:\n"
|
|
+ "\n".join(bad_lines)
|
|
)
|
|
# #endregion test_git_service_repositorys_typo_regression
|
|
|
|
# #region test_git_default_repos_path_constant [C:2] [TYPE Function]
|
|
# @BRIEF DEFAULT_GIT_REPOS_PATH should be defined per RATIONALE (not yet defined).
|
|
def test_git_default_repos_path_constant(self):
|
|
"""DEFAULT_GIT_REPOS_PATH should exist per RATIONALE comment.
|
|
|
|
RATIONALE says it was extracted as a constant, but the source uses
|
|
the literal string 'git_repos' in __init__ default parameter.
|
|
"""
|
|
try:
|
|
from src.services.git._base import DEFAULT_GIT_REPOS_PATH
|
|
assert isinstance(DEFAULT_GIT_REPOS_PATH, str)
|
|
except Exception:
|
|
pytest.skip(
|
|
"DEFAULT_GIT_REPOS_PATH is not defined. "
|
|
"Cannot import git._base (transitive dependency on AuthConfig crash). "
|
|
"Additionally, the constant was never extracted — __init__ uses literal 'git_repos'."
|
|
)
|
|
# #endregion test_git_default_repos_path_constant
|
|
|
|
# #region test_superset_client_module_loads [C:2] [TYPE Function]
|
|
# @BRIEF superset_client._base is importable (no dependency on crashing modules).
|
|
def test_superset_client_module_loads(self):
|
|
from src.core.superset_client._base import SupersetClientBase
|
|
assert SupersetClientBase is not None
|
|
# #endregion test_superset_client_module_loads
|
|
|
|
# #region test_constants_are_immutable [C:2] [TYPE Function]
|
|
# @BRIEF Module-level constants use int/str types (immutable by convention).
|
|
def test_constants_are_immutable(self):
|
|
"""Check that known constants in superset_client use immutable types."""
|
|
from src.core.superset_client._base import SupersetClientBase
|
|
# Verify the class is defined with constants pattern
|
|
source = Path(SupersetClientBase.__module__.replace(".", "/") + ".py")
|
|
src_path = Path(__file__).parent.parent / "src" / source
|
|
if src_path.exists():
|
|
content = src_path.read_text(encoding="utf-8")
|
|
# Verify PAGE_SIZE concept is documented
|
|
assert "page_size" in content
|
|
# #endregion test_constants_are_immutable
|
|
# #endregion TestConstantsAuditFixes
|