Files
ss-tools/backend/tests/test_constants_audit_fixes.py
busya a9a453109c security: critical auth fixes, test migration to SQLite+FK, 44 test fixes
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
2026-05-26 14:58:49 +03:00

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