Files
gaokao-volunteer-system/data/channel_sync/tests/test_webhook_server_db_scoping.py

120 lines
4.2 KiB
Python
Raw Normal View History

"""P1-4 regression: webhook server must isolate connections per ``db_path``.
The previous implementation cached a single module-level
``_DB_CONN`` and reused it across ``make_server`` calls regardless of
the path argument. That let two webhook server instances point at the
same database while each thought it was talking to a different one.
The fix keeps a per-path connection cache; this test locks the
behaviour so future regressions collapse immediately.
"""
from __future__ import annotations
import threading
import time
from pathlib import Path
import pytest
from data.channel_sync import webhook_server
from data.orders.dao import OrdersDAO
@pytest.fixture
def two_dbs(tmp_path: Path):
db_a = tmp_path / "a.db"
db_b = tmp_path / "b.db"
# Trigger schema bootstrap for both, so the per-key connection
# cache is filled when ``_get_db`` is first called.
for db in (db_a, db_b):
OrdersDAO.connect(db).close()
# Always start each test from a clean per-path cache so a leak
# from one test cannot poison another.
webhook_server.close_db_for_tests()
yield db_a, db_b
webhook_server.close_db_for_tests()
def test_get_db_returns_distinct_connections_per_path(two_dbs):
db_a, db_b = two_dbs
conn_a = webhook_server._get_db(str(db_a))
conn_b = webhook_server._get_db(str(db_b))
try:
assert conn_a is not conn_b
# Each connection is bound to its own path; an insert via
# connection ``A`` must not be visible via connection ``B``.
conn_a.execute(
"INSERT INTO orders(id, source, service_version, amount_cents, status, created_at, status_updated_at) "
"VALUES(?, ?, ?, ?, ?, ?, ?)",
("ORD-A-1", "web", "standard", 9900, "pending", "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"),
)
conn_a.commit()
cur_b = conn_b.execute("SELECT COUNT(*) FROM orders").fetchone()
assert cur_b[0] == 0
finally:
webhook_server.close_db_for_path(str(db_a))
webhook_server.close_db_for_path(str(db_b))
def test_release_db_does_not_close_other_paths_connections(two_dbs):
db_a, db_b = two_dbs
webhook_server._get_db(str(db_a))
webhook_server._get_db(str(db_b))
webhook_server.close_db_for_path(str(db_a))
# ``close_db_for_path`` for path ``A`` must not affect the cache
# for ``B``. A subsequent request for path ``B`` should return the
# still-cached connection, not reopen one.
conn_b = webhook_server._get_db(str(db_b))
assert conn_b is webhook_server._get_db(str(db_b))
webhook_server.close_db_for_path(str(db_b))
def test_get_db_is_thread_safe_under_concurrent_access(two_dbs):
db_a, db_b = two_dbs
errors: list[BaseException] = []
def worker(path: str) -> None:
try:
for _ in range(25):
conn = webhook_server._get_db(path)
conn.execute("SELECT 1").fetchone()
time.sleep(0.001)
except BaseException as exc: # pragma: no cover - test
errors.append(exc)
finally:
webhook_server.close_db_for_path(path)
threads = [
threading.Thread(target=worker, args=(str(db_a),)),
threading.Thread(target=worker, args=(str(db_b),)),
threading.Thread(target=worker, args=(str(db_a),)),
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, f"thread-safety regression: {errors!r}"
def test_release_all_closes_every_cached_connection(two_dbs):
"""Calling ``close_db_for_tests`` (the module's teardown helper)
must close every cached connection. After teardown, the next
``_get_db`` call must reopen a fresh connection, not silently
reuse a closed handle.
"""
db_a, db_b = two_dbs
webhook_server._get_db(str(db_a))
webhook_server._get_db(str(db_b))
webhook_server.close_db_for_tests()
fresh_a = webhook_server._get_db(str(db_a))
fresh_b = webhook_server._get_db(str(db_b))
try:
# Both connections must be freshly opened, not the same handle
# as before the teardown.
assert fresh_a.execute("SELECT 1").fetchone() == (1,)
assert fresh_b.execute("SELECT 1").fetchone() == (1,)
finally:
webhook_server.close_db_for_tests()