diff --git a/app/__init__.py b/app/__init__.py index 843056a..2a26f37 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,6 +15,7 @@ Architecture et composants clés : import os import tomllib +from collections.abc import Mapping import sqlalchemy as sa from flask import Flask @@ -36,6 +37,10 @@ def _migrate_db(app): """ import sqlite3 + with app.app_context(): + if db.engine.url.database in (None, ":memory:"): + return # Une base mémoire est initialisée par create_all(). + db_path = os.path.join(app.instance_path, "worklog.db") if not os.path.exists(db_path): return # Nouvelle DB, create_all() s'en charge @@ -120,7 +125,12 @@ def _date_fr(d): return f"{jour} {d.day} {mois} {d.year}" -def create_app(config_path=None): +def create_app( + config_path: str | None = None, + *, + database_uri: str | None = None, + engine_options: Mapping[str, object] | None = None, +) -> Flask: """Factory de création et de configuration de l'application Flask. Cette fonction réalise les étapes suivantes : @@ -136,6 +146,11 @@ def create_app(config_path=None): Paramètres: config_path (str | None): Chemin optionnel vers le fichier de configuration TOML. Par défaut, cherche `config.toml` à la racine du projet. + database_uri (str | None): URI SQLAlchemy à utiliser à la place de la base SQLite + de l'instance. Cette option est appliquée avant l'initialisation + de Flask-SQLAlchemy. + engine_options (Mapping[str, object] | None): Options SQLAlchemy appliquées avant + l'initialisation de Flask-SQLAlchemy. Retourne: Flask: L'instance de l'application Flask configurée et prête à l'emploi. @@ -144,9 +159,11 @@ def create_app(config_path=None): os.makedirs(app.instance_path, exist_ok=True) - app.config["SQLALCHEMY_DATABASE_URI"] = ( - f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}" - ) + if database_uri is None: + database_uri = f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}" + app.config["SQLALCHEMY_DATABASE_URI"] = database_uri + if engine_options is not None: + app.config["SQLALCHEMY_ENGINE_OPTIONS"] = engine_options app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod") diff --git a/tests/conftest.py b/tests/conftest.py index d85b90b..9af670e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest from app import create_app from app import db as _db +from tests.in_memory_db import IN_MEMORY_DATABASE_URI, in_memory_engine_options @pytest.fixture @@ -72,9 +73,12 @@ forfait = 0 encoding="utf-8", ) - application = create_app(config_path=str(config_path)) + application = create_app( + config_path=str(config_path), + database_uri=IN_MEMORY_DATABASE_URI, + engine_options=in_memory_engine_options(), + ) application.config["TESTING"] = True - application.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" with application.app_context(): _db.create_all() diff --git a/tests/in_memory_db.py b/tests/in_memory_db.py new file mode 100644 index 0000000..fc6fc2a --- /dev/null +++ b/tests/in_memory_db.py @@ -0,0 +1,20 @@ +"""Configuration SQLite mémoire partagée entre fixtures et helpers de tests. + +Ce module est volontairement indépendant de ``conftest`` afin d'être +importable de manière portable, y compris sous ``pytest --import-mode=importlib`` +(où ``conftest`` n'est pas importable comme module ordinaire). Il centralise la +configuration mémoire partagée entre la fixture ``app`` et les helpers de tests +qui appellent directement ``create_app(...)``, évitant qu'une factory de test +initialise accidentellement ``instance/worklog.db``. +""" + +from sqlalchemy.pool import StaticPool + +IN_MEMORY_DATABASE_URI = "sqlite:///:memory:" + + +def in_memory_engine_options() -> dict[str, object]: + return { + "poolclass": StaticPool, + "connect_args": {"check_same_thread": False}, + } diff --git a/tests/test_app_factory.py b/tests/test_app_factory.py new file mode 100644 index 0000000..2ab1ebe --- /dev/null +++ b/tests/test_app_factory.py @@ -0,0 +1,11 @@ +import sqlalchemy as sa +from sqlalchemy.pool import StaticPool + +from app import db + + +def test_app_fixture_uses_one_in_memory_database_connection(app): + with app.app_context(): + assert str(db.engine.url) == "sqlite:///:memory:" + assert isinstance(db.engine.pool, StaticPool) + assert sa.inspect(db.engine).has_table("work_entries") diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index c86d35f..b23c56e 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -1,6 +1,7 @@ import pytest from app import create_app +from tests.in_memory_db import IN_MEMORY_DATABASE_URI, in_memory_engine_options _MINIMAL_CONFIG = """ [vehicles.citadine] @@ -26,7 +27,11 @@ default_motor_vehicle_id = "{vehicle_id}" def _create_app_with_home_assistant_config(tmp_path, **values): config_path = tmp_path / "config.toml" config_path.write_text(_MINIMAL_CONFIG.format(**values), encoding="utf-8") - return create_app(config_path=str(config_path)) + return create_app( + config_path=str(config_path), + database_uri=IN_MEMORY_DATABASE_URI, + engine_options=in_memory_engine_options(), + ) def test_get_vehicles_returns_configured_vehicles(app): @@ -164,4 +169,8 @@ timezone = "Europe/Paris" ) with pytest.raises(ValueError, match="clé.*manquante"): - create_app(config_path=str(config_path)) + create_app( + config_path=str(config_path), + database_uri=IN_MEMORY_DATABASE_URI, + engine_options=in_memory_engine_options(), + )