From bba83af409cdd4c85bd59eac1bcd391dc2ac871a Mon Sep 17 00:00:00 2001 From: Lilfade Date: Tue, 27 May 2025 05:19:03 -0500 Subject: [PATCH] migration and testing --- Makefile | 6 +++++ app/__init__.py | 45 +++++++++++++++++++++------------- plugins/admin/__init__.py | 2 +- tests/test_app_init.py | 6 ++--- tests/test_database_smoke.py | 12 +++++++++ tests/test_event_dispatcher.py | 18 ++++++-------- tests/test_plugin_metadata.py | 20 ++++++--------- 7 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 tests/test_database_smoke.py diff --git a/Makefile b/Makefile index 7c081d2..940437c 100644 --- a/Makefile +++ b/Makefile @@ -91,3 +91,9 @@ wait: sleep 2; echo -n "."; \ done; echo "\n[✅] $$WEB_CONTAINER is healthy!"' +migrate: + flask db migrate -m "auto" + +upgrade: + flask db upgrade + diff --git a/app/__init__.py b/app/__init__.py index 5e575ee..9178fcd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,8 @@ import os import json -import importlib.util import importlib +import importlib.util + from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -9,9 +10,10 @@ from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from dotenv import load_dotenv -# Load environment variables from .env +# Load environment variables load_dotenv() +# Initialize extensions db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager() @@ -21,9 +23,9 @@ csrf = CSRFProtect() def create_app(): app = Flask(__name__) app.config.from_object('app.config.Config') - csrf.init_app(app) # Initialize core extensions + csrf.init_app(app) db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) @@ -33,8 +35,7 @@ def create_app(): from .errors import bp as errors_bp app.register_blueprint(errors_bp) - # Plugin auto-loader - # Plugin auto-loader + # Auto-discover and register plugins plugin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins')) for plugin in os.listdir(plugin_path): if plugin.endswith('.noload'): @@ -45,7 +46,7 @@ def create_app(): if not os.path.isdir(plugin_dir): continue - # Register routes + # 1. Register routes route_file = os.path.join(plugin_dir, 'routes.py') if os.path.isfile(route_file): try: @@ -57,25 +58,35 @@ def create_app(): except Exception as e: print(f"[⚠️] Failed to load routes from plugin '{plugin}': {e}") - # Register CLI commands and plugin entry points - init_file = os.path.join(plugin_dir, '__init__.py') + # Define paths + init_file = os.path.join(plugin_dir, '__init__.py') + plugin_json = os.path.join(plugin_dir, 'plugin.json') + model_file = os.path.join(plugin_dir, 'models.py') + + # 2. Register CLI commands and run entry point if os.path.isfile(init_file): try: cli_module = importlib.import_module(f"plugins.{plugin}") if hasattr(cli_module, 'register_cli'): cli_module.register_cli(app) - plugin_json = os.path.join(plugin_dir, 'plugin.json') if os.path.isfile(plugin_json): - try: - meta = json.load(open(plugin_json, 'r')) - entry = meta.get('entry_point') - if entry and hasattr(cli_module, entry): - getattr(cli_module, entry)(app) - except Exception as e: - print(f"[⚠️] Failed to run entry_point '{entry}' for plugin '{plugin}': {e}") + with open(plugin_json, 'r') as f: + meta = json.load(f) + entry = meta.get('entry_point') + if entry and hasattr(cli_module, entry): + getattr(cli_module, entry)(app) except Exception as e: - print(f"[⚠️] Failed to load CLI from plugin '{plugin}': {e}") + print(f"[⚠️] Failed to load CLI for plugin '{plugin}': {e}") + + # 3. Auto-load plugin models for migrations + if os.path.isfile(model_file): + try: + spec = importlib.util.spec_from_file_location(f"plugins.{plugin}.models", model_file) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + except Exception as e: + print(f"[⚠️] Failed to load models from plugin '{plugin}': {e}") @app.context_processor def inject_current_year(): diff --git a/plugins/admin/__init__.py b/plugins/admin/__init__.py index 864d480..c0bb32a 100644 --- a/plugins/admin/__init__.py +++ b/plugins/admin/__init__.py @@ -2,5 +2,5 @@ import click from flask import Flask def register_cli(app: Flask): - # No CLI commands yet for admin plugin + # CLI entry-point for admin pass diff --git a/tests/test_app_init.py b/tests/test_app_init.py index 8475da9..ced9a23 100644 --- a/tests/test_app_init.py +++ b/tests/test_app_init.py @@ -1,10 +1,8 @@ - import pytest from app import create_app def test_app_loads_plugins(): app = create_app({'TESTING': True}) - # Assuming app.plugins is a dict of loaded plugin modules - assert hasattr(app, 'plugins'), "App object missing 'plugins' attribute" + assert hasattr(app, 'plugins'), "App missing plugins attribute" for plugin in ["auth", "admin", "plant", "cli"]: - assert plugin in app.plugins, f"Plugin '{plugin}' not loaded into app.plugins" + assert plugin in app.plugins, f"Plugin {plugin} not loaded" diff --git a/tests/test_database_smoke.py b/tests/test_database_smoke.py new file mode 100644 index 0000000..ba59e6e --- /dev/null +++ b/tests/test_database_smoke.py @@ -0,0 +1,12 @@ +import pytest +from app import create_app, db +from sqlalchemy import inspect + +def test_database_smoke(tmp_path): + db_file = tmp_path / "test.db" + app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_file}"}) + with app.app_context(): + db.drop_all() + db.create_all() + tables = inspect(db.engine).get_table_names() + assert 'users' in tables or 'user' in tables diff --git a/tests/test_event_dispatcher.py b/tests/test_event_dispatcher.py index a77fd69..11584f9 100644 --- a/tests/test_event_dispatcher.py +++ b/tests/test_event_dispatcher.py @@ -3,20 +3,16 @@ from app.hooks import EventDispatcher, listen_event def test_register_and_dispatch(): results = [] - def listener1(a, b=None): - results.append(('l1', a, b)) - def listener2(a, b=None): - results.append(('l2', a, b)) - EventDispatcher.register('test_event', listener1) - EventDispatcher.register('test_event', listener2) - EventDispatcher.dispatch('test_event', 1, b=2) - assert ('l1', 1, 2) in results - assert ('l2', 1, 2) in results + def listener(a, b=None): + results.append((a, b)) + EventDispatcher.register('evt', listener) + EventDispatcher.dispatch('evt', 1, b=2) + assert results == [(1, 2)] def test_listen_event_decorator(): results = [] - @listen_event('decorated_event') + @listen_event('evt2') def handler(x): results.append(x) - EventDispatcher.dispatch('decorated_event', 'hello') + EventDispatcher.dispatch('evt2', 'hello') assert results == ['hello'] diff --git a/tests/test_plugin_metadata.py b/tests/test_plugin_metadata.py index 1bfa266..82ccac8 100644 --- a/tests/test_plugin_metadata.py +++ b/tests/test_plugin_metadata.py @@ -1,25 +1,21 @@ - import os import json import pytest import importlib -# Directory containing plugins PLUGINS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins')) @pytest.mark.parametrize("plugin", ["auth", "admin", "plant", "cli"]) -def test_plugin_metadata_and_init(plugin, tmp_path, monkeypatch): - # Construct plugin path +def test_plugin_metadata_and_init(plugin): plugin_path = os.path.join(PLUGINS_DIR, plugin) - # Test plugin.json exists and contains required keys meta_path = os.path.join(plugin_path, 'plugin.json') assert os.path.isfile(meta_path), f"plugin.json missing for {plugin}" meta = json.loads(open(meta_path).read()) for key in ("name", "version", "description"): - assert key in meta, f"{{key}} missing in {plugin}/plugin.json" - # Test __init__.py can be imported and has register_cli - module_name = f"plugins.{plugin}" - spec = importlib.util.spec_from_file_location(module_name, os.path.join(plugin_path, '__init__.py')) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - assert hasattr(module, "register_cli"), f"register_cli missing in {plugin}/__init__.py" + assert key in meta, f"{key} missing in {plugin}/plugin.json" + init_path = os.path.join(plugin_path, '__init__.py') + if os.path.exists(init_path): + spec = importlib.util.spec_from_file_location(f"plugins.{plugin}", init_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert hasattr(module, "register_cli"), f"register_cli missing in {plugin}/__init__.py"