kin/tests/test_no_connection_artifacts.py

113 lines
5 KiB
Python
Raw Permalink Normal View History

"""Regression test — KIN-009.
Проверяет, что в рабочей директории проекта НЕ создаются файлы с именами,
содержащими 'sqlite3.Connection'. Такие артефакты появляются, если путь к БД
формируется передачей объекта sqlite3.Connection вместо строки/Path в
sqlite3.connect().
"""
import os
import sqlite3
from pathlib import Path
import pytest
# Корень проекта — три уровня вверх от этого файла (tests/ → kin/)
PROJECT_ROOT = Path(__file__).parent.parent
def _find_connection_artifacts(search_dir: Path) -> list[Path]:
"""Рекурсивно ищет файлы, чьё имя содержит 'sqlite3.Connection'."""
found = []
try:
for entry in search_dir.rglob("*"):
if entry.is_file() and "sqlite3.Connection" in entry.name:
found.append(entry)
except PermissionError:
pass
return found
# ---------------------------------------------------------------------------
# Тест 1: статическая проверка — артефактов нет прямо сейчас
# ---------------------------------------------------------------------------
def test_no_connection_artifact_files_in_project():
"""В рабочей директории проекта не должно быть файлов с 'sqlite3.Connection' в имени."""
artifacts = _find_connection_artifacts(PROJECT_ROOT)
assert artifacts == [], (
f"Найдены файлы-артефакты sqlite3.Connection:\n"
+ "\n".join(f" {p}" for p in artifacts)
)
def test_no_connection_artifact_files_in_kin_home():
"""В ~/.kin/ тоже не должно быть таких файлов."""
kin_home = Path.home() / ".kin"
if not kin_home.exists():
pytest.skip("~/.kin не существует")
artifacts = _find_connection_artifacts(kin_home)
assert artifacts == [], (
f"Найдены файлы-артефакты sqlite3.Connection в ~/.kin:\n"
+ "\n".join(f" {p}" for p in artifacts)
)
# ---------------------------------------------------------------------------
# Тест 2: динамическая проверка — init_db не создаёт артефактов в tmp_path
# ---------------------------------------------------------------------------
def test_init_db_does_not_create_connection_artifact(tmp_path):
"""init_db() должен создавать файл с нормальным именем, а не 'sqlite3.Connection ...'."""
from core.db import init_db
db_file = tmp_path / "test.db"
conn = init_db(db_file)
conn.close()
artifacts = _find_connection_artifacts(tmp_path)
assert artifacts == [], (
"init_db() создал файл с именем, содержащим 'sqlite3.Connection':\n"
+ "\n".join(f" {p}" for p in artifacts)
)
# Убедимся, что файл БД реально создан с правильным именем
assert db_file.exists(), "Файл БД должен существовать"
# ---------------------------------------------------------------------------
# Тест 3: воспроизводит сценарий утечки — connect(conn) вместо connect(path)
# ---------------------------------------------------------------------------
def test_init_db_passes_string_to_sqlite_connect(tmp_path, monkeypatch):
"""core/db.init_db() должен вызывать sqlite3.connect() со строкой пути, а НЕ с объектом Connection.
Баг-сценарий: если где-то в коде путь к БД перепутан с объектом conn,
sqlite3.connect(str(conn)) создаст файл с именем '<sqlite3.Connection object at 0x...>'.
Этот тест перехватывает вызов и проверяет тип аргумента напрямую.
"""
import core.db as db_module
connect_calls: list = []
real_connect = sqlite3.connect
def mock_connect(database, *args, **kwargs):
connect_calls.append(database)
return real_connect(database, *args, **kwargs)
monkeypatch.setattr(db_module.sqlite3, "connect", mock_connect)
db_file = tmp_path / "test.db"
conn = db_module.init_db(db_file)
conn.close()
assert connect_calls, "sqlite3.connect() должен быть вызван хотя бы один раз"
for call_arg in connect_calls:
assert isinstance(call_arg, str), (
f"sqlite3.connect() получил не строку: {type(call_arg).__name__!r} = {call_arg!r}"
)
assert "sqlite3.Connection" not in call_arg, (
f"sqlite3.connect() получил строку объекта Connection: {call_arg!r}\n"
"Баг: str(conn) передаётся вместо пути к файлу — это создаёт файл-артефакт!"
)