Files
learningcircuit--local-deep…/tests/security/test_settings_manager_thread_safety_hook.py
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

364 lines
11 KiB
Python

"""
Tests for the check-settings-manager-thread-safety pre-commit hook.
Ensures the hook correctly detects get_settings_manager() calls without
db_session= in background-thread functions (identified by @thread_cleanup
decorator or _background_/_auto_/_worker naming), while allowing legitimate
usage in Flask request context and when db_session= is passed explicitly.
"""
import ast
import sys
from importlib import import_module
from pathlib import Path
HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks"
sys.path.insert(0, str(HOOKS_DIR))
hook_module = import_module("check-settings-manager-thread-safety") # noqa: E402
SettingsManagerThreadSafetyChecker = (
hook_module.SettingsManagerThreadSafetyChecker
)
def _check_code(code: str, filename: str = "src/module.py") -> list:
"""Parse code and return the checker's issues list (line_no, message)."""
tree = ast.parse(code)
checker = SettingsManagerThreadSafetyChecker(filename)
checker.visit(tree)
return checker.issues
class TestThreadFunctionDetection:
"""Detection tests — functions the hook should classify as background threads."""
def test_detects_thread_cleanup_decorator(self):
code = """
@thread_cleanup
def some_func():
get_settings_manager(username="alice")
"""
issues = _check_code(code)
assert len(issues) == 1
assert issues[0][0] == 4
def test_detects_thread_cleanup_decorator_call_form(self):
"""@thread_cleanup() with parentheses should also trigger."""
code = """
@thread_cleanup()
def some_func():
get_settings_manager(username="alice")
"""
issues = _check_code(code)
assert len(issues) == 1
def test_detects_module_qualified_thread_cleanup_decorator(self):
"""@module.thread_cleanup should be detected via ast.Attribute."""
code = """
@db.thread_cleanup
def some_func():
get_settings_manager(username="alice")
"""
issues = _check_code(code)
assert len(issues) == 1
def test_detects_background_prefix(self):
code = """
def _background_index_worker(username):
get_settings_manager(username=username)
"""
issues = _check_code(code)
assert len(issues) == 1
def test_detects_auto_prefix(self):
code = """
def _auto_index_documents_worker(username):
get_settings_manager(username=username)
"""
issues = _check_code(code)
assert len(issues) == 1
def test_detects_worker_suffix(self):
code = """
def my_worker(username):
get_settings_manager(username=username)
"""
issues = _check_code(code)
assert len(issues) == 1
def test_detects_async_thread_function(self):
"""async def _background_* should be visited via visit_AsyncFunctionDef."""
code = """
async def _background_evaluation(queue):
get_settings_manager(username="alice")
"""
issues = _check_code(code)
assert len(issues) == 1
class TestCallPatternDetection:
"""Tests for how the hook recognises get_settings_manager() call shapes."""
def test_flags_bare_call_without_db_session(self):
code = """
@thread_cleanup
def worker():
get_settings_manager()
"""
assert len(_check_code(code)) == 1
def test_flags_username_only_call(self):
code = """
@thread_cleanup
def worker():
get_settings_manager(username="alice")
"""
assert len(_check_code(code)) == 1
def test_flags_attribute_style_call(self):
"""module.get_settings_manager() should be caught via ast.Attribute."""
code = """
@thread_cleanup
def worker():
db_utils.get_settings_manager(username="alice")
"""
assert len(_check_code(code)) == 1
class TestAllowances:
"""Cases that should NOT be flagged."""
def test_allows_db_session_kwarg(self):
code = """
@thread_cleanup
def worker(db_session):
get_settings_manager(db_session=db_session, username="alice")
"""
assert _check_code(code) == []
def test_allows_db_session_only(self):
code = """
@thread_cleanup
def worker(db_session):
get_settings_manager(db_session=db_session)
"""
assert _check_code(code) == []
def test_allows_positional_db_session(self):
"""db_session is the first positional param - a positional arg counts."""
code = """
@thread_cleanup
def worker(db_session, username):
get_settings_manager(db_session, username)
"""
assert _check_code(code) == []
def test_allows_positional_db_session_only(self):
code = """
@thread_cleanup
def worker(db_session):
get_settings_manager(db_session)
"""
assert _check_code(code) == []
def test_allows_non_thread_function(self):
"""Flask route / non-thread code may call get_settings_manager() freely."""
code = """
def route_handler():
get_settings_manager(username="alice")
"""
assert _check_code(code) == []
def test_allows_unrelated_call(self):
"""Calls to other functions should never be flagged."""
code = """
@thread_cleanup
def worker():
get_other_thing(username="alice")
"""
assert _check_code(code) == []
def test_allows_thread_function_without_call(self):
"""A thread function that never calls get_settings_manager is fine."""
code = """
@thread_cleanup
def worker():
do_something_else()
"""
assert _check_code(code) == []
class TestMultipleAndPositional:
"""Multi-issue reporting and line-number correctness."""
def test_reports_multiple_violations(self):
code = """
@thread_cleanup
def worker_one():
get_settings_manager(username="a")
get_settings_manager(username="b")
"""
issues = _check_code(code)
assert len(issues) == 2
def test_reports_correct_line_numbers(self):
code = """
@thread_cleanup
def worker():
x = 1
get_settings_manager(username="a")
"""
issues = _check_code(code)
assert len(issues) == 1
assert issues[0][0] == 5
def test_mixed_safe_and_unsafe_in_same_function(self):
code = """
@thread_cleanup
def worker(db_session):
get_settings_manager(db_session=db_session) # safe
get_settings_manager(username="x") # unsafe
"""
issues = _check_code(code)
assert len(issues) == 1
assert issues[0][0] == 5
class TestCheckFileIntegration:
"""check_file() integration tests using tmp_path."""
def test_clean_file_passes(self, tmp_path):
clean = tmp_path / "clean.py"
clean.write_text("""
@thread_cleanup
def worker(db_session):
get_settings_manager(db_session=db_session, username="x")
""")
assert hook_module.check_file(clean) == []
def test_violation_file_fails(self, tmp_path):
bad = tmp_path / "bad.py"
bad.write_text("""
@thread_cleanup
def worker():
get_settings_manager(username="x")
""")
issues = hook_module.check_file(bad)
assert len(issues) == 1
filepath, lineno, _msg = issues[0]
assert filepath == str(bad)
assert lineno == 4
def test_syntax_error_is_tolerated(self, tmp_path, capsys):
"""Files with syntax errors should not raise; they print to stderr."""
broken = tmp_path / "broken.py"
broken.write_text("def bad(:\n pass")
issues = hook_module.check_file(broken)
assert issues == []
captured = capsys.readouterr()
assert "Syntax error" in captured.err
def test_non_python_file_via_main_is_skipped(self, tmp_path, monkeypatch):
"""main() should skip non-.py files without inspecting them."""
non_py = tmp_path / "README.md"
non_py.write_text("not python @thread_cleanup def worker():")
monkeypatch.setattr(sys, "argv", ["hook", str(non_py)])
assert hook_module.main() == 0
def test_main_returns_zero_on_clean(self, tmp_path, monkeypatch):
clean = tmp_path / "clean.py"
clean.write_text("""
def route():
get_settings_manager(username="x")
""")
monkeypatch.setattr(sys, "argv", ["hook", str(clean)])
assert hook_module.main() == 0
def test_main_returns_one_on_violation(self, tmp_path, monkeypatch, capsys):
bad = tmp_path / "bad.py"
bad.write_text("""
@thread_cleanup
def worker():
get_settings_manager(username="x")
""")
monkeypatch.setattr(sys, "argv", ["hook", str(bad)])
assert hook_module.main() == 1
captured = capsys.readouterr()
assert "get_settings_manager()" in captured.out
assert "get_user_db_session" in captured.out
def test_main_with_no_files_returns_zero(self, monkeypatch):
monkeypatch.setattr(sys, "argv", ["hook"])
assert hook_module.main() == 0
class TestNestedFunctions:
"""Tests for the nested-function walker that prevents double-reporting."""
def test_nested_thread_in_thread_reports_once(self):
"""Unsafe call inside a nested thread fn must be reported exactly once,
not twice (once from the outer walk + once from the inner visit)."""
code = """
@thread_cleanup
def outer_worker(db_session):
get_settings_manager(db_session) # safe
@thread_cleanup
def inner_worker():
get_settings_manager(username="x") # unsafe, reported once
"""
issues = _check_code(code)
assert len(issues) == 1
def test_nested_non_thread_function_not_checked(self):
"""An unsafe call inside a nested non-thread fn should not be flagged
just because the outer function is a thread fn."""
code = """
@thread_cleanup
def outer_worker():
def inner_helper():
get_settings_manager(username="x") # NOT flagged: inner is not a thread fn
"""
assert _check_code(code) == []
def test_lambda_inside_thread_not_checked(self):
"""A lambda body inside a thread fn is not walked (lambdas have no name
to classify)."""
code = """
@thread_cleanup
def outer_worker():
cb = lambda: get_settings_manager(username="x")
"""
assert _check_code(code) == []
class TestThreadFunctionClassifier:
"""Unit tests for SettingsManagerThreadSafetyChecker._is_thread_function."""
def _classify(self, code: str) -> bool:
tree = ast.parse(code)
func = tree.body[0]
checker = SettingsManagerThreadSafetyChecker("x.py")
return checker._is_thread_function(func)
def test_plain_function_is_not_thread(self):
assert self._classify("def foo(): pass") is False
def test_background_prefix_is_thread(self):
assert self._classify("def _background_x(): pass") is True
def test_auto_prefix_is_thread(self):
assert self._classify("def _auto_x(): pass") is True
def test_worker_suffix_is_thread(self):
assert self._classify("def foo_worker(): pass") is True
def test_decorator_is_thread(self):
code = "@thread_cleanup\ndef foo(): pass"
assert self._classify(code) is True
def test_unrelated_decorator_is_not_thread(self):
code = "@staticmethod\ndef foo(): pass"
assert self._classify(code) is False