"""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 three behaviors that matter: only not-installed plugins are dropped, the non-empty guard refuses without --force, and a full site (every plugin installed) is a no-op. """ 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 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 'usbdevices' 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 usbdevicetypes (typename, 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 'usbdevicetypes' 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 usbdevicetypes (typename, 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 'usbdevicetypes' not in set(inspect(_db.engine).get_table_names())