"""Tests for `flask plugin prune-schema` (ADR-014 schema-lean per-site). The shared core Alembic baseline creates every plugin's tables, so a lean site that omits a plugin still carries that plugin's (empty) tables. prune-schema drops the tables of every not-installed plugin, leaving core + chosen-plugin tables. These tests pin the behaviors that matter: only not-installed plugins are dropped, the non-empty guard refuses without --force, a full site (every plugin installed) is a no-op, and an ORPHAN_TABLE_OWNERS table - one the core chain creates for a plugin that no longer ships - is dropped too, because there is no plugin left to "install" that would ever spare it. """ import pytest from sqlalchemy import inspect, text from shopdb.extensions import db as _db from shopdb.plugins.cli import plugin_cli from shopdb.plugins.alembic_template import ( ORPHAN_TABLE_OWNERS, PLUGIN_TABLE_OWNERS) def _install_only(app, monkeypatch, installed_names): """Force discover_available to report exactly `installed_names` installed.""" pm = app.extensions['plugin_manager'] listing = [{'name': name, 'installed': name in installed_names} for name in PLUGIN_TABLE_OWNERS] monkeypatch.setattr(pm, 'discover_available', lambda: listing) def test_prune_drops_only_not_installed(app, db, runner, monkeypatch): """A lean site keeps its plugins' tables and drops every other plugin's.""" _install_only(app, monkeypatch, {'machines', 'printers'}) before = set(inspect(_db.engine).get_table_names()) assert 'guides' in before and 'machines' in before # baseline sanity result = runner.invoke(plugin_cli, ['prune-schema', '--yes']) assert result.exit_code == 0, result.output after = set(inspect(_db.engine).get_table_names()) # Kept: machines + printers owned tables. for keep in PLUGIN_TABLE_OWNERS['machines'] + PLUGIN_TABLE_OWNERS['printers']: assert keep in after, f'{keep} should have been kept' # Dropped: every other plugin's tables. for plugin, tables in PLUGIN_TABLE_OWNERS.items(): if plugin in ('machines', 'printers'): continue for tablename in tables: assert tablename not in after, f'{tablename} should have been dropped' # Core tables untouched. for core in ('assets', 'users', 'settings'): assert core in after def test_prune_full_site_is_noop(app, db, runner, monkeypatch): """Every plugin installed -> nothing to prune, schema unchanged.""" _install_only(app, monkeypatch, set(PLUGIN_TABLE_OWNERS)) before = set(inspect(_db.engine).get_table_names()) result = runner.invoke(plugin_cli, ['prune-schema', '--yes', '--force']) assert result.exit_code == 0, result.output assert 'Nothing to prune' in result.output assert set(inspect(_db.engine).get_table_names()) == before def test_prune_refuses_non_empty_without_force(app, db, runner, monkeypatch): """A not-installed table that holds rows is not dropped without --force.""" _install_only(app, monkeypatch, {'machines'}) _db.session.execute(text( "INSERT INTO printertypes (printertype, createddate, modifieddate, " "isactive) VALUES ('kept-by-guard', '2026-01-01', '2026-01-01', 1)")) _db.session.commit() result = runner.invoke(plugin_cli, ['prune-schema', '--yes']) assert result.exit_code == 1 assert 'REFUSING' in result.output # The row-bearing table survives the refusal. assert 'printertypes' in set(inspect(_db.engine).get_table_names()) def test_prune_force_drops_non_empty(app, db, runner, monkeypatch): """--force drops even a table that holds rows.""" _install_only(app, monkeypatch, {'machines'}) _db.session.execute(text( "INSERT INTO printertypes (printertype, createddate, modifieddate, " "isactive) VALUES ('dropped-by-force', '2026-01-01', '2026-01-01', 1)")) _db.session.commit() result = runner.invoke(plugin_cli, ['prune-schema', '--yes', '--force']) assert result.exit_code == 0, result.output assert 'printertypes' not in set(inspect(_db.engine).get_table_names()) def test_prune_drops_an_orphan_table_no_plugin_owns(app, db, runner, monkeypatch): """A table the core chain creates for a plugin that no longer ships. This is the case with no plugin to install, so nothing can ever spare it: the core chain builds it on every site and only prune-schema takes it away. Built here by hand because the test database is created from the MODELS, and by definition no model declares this table any more. """ _install_only(app, monkeypatch, set(PLUGIN_TABLE_OWNERS)) orphan = ORPHAN_TABLE_OWNERS['employees'][0] _db.session.execute(text(f'CREATE TABLE {orphan} (sso INTEGER PRIMARY KEY)')) _db.session.commit() assert orphan in set(inspect(_db.engine).get_table_names()) result = runner.invoke(plugin_cli, ['prune-schema', '--yes']) assert result.exit_code == 0, result.output assert orphan not in set(inspect(_db.engine).get_table_names())