"""Every setting a plugin declares must be reachable in its settings page. A setting exists so a site can change it. One that is seeded into the database but rendered by no page is not configurable - it is a constant with extra steps, and the only way to change it is direct SQL. This was written after shipping exactly that twice in one afternoon: printedparts_kiosk_enabled / _kiosk_token and then usb_kiosk_token were declared in get_settings(), seeded correctly, and read correctly by the API - and none of them appeared on any page, because each settings view carries a hardcoded KEYS list that nobody remembered to extend. The instructions for turning the kiosk back on were impossible to follow. The check is deliberately crude: it greps the plugin's frontend for the key as a literal. That is enough to catch the failure it exists for, and it cannot be satisfied by accident. """ import pathlib import re import pytest REPO = pathlib.Path(__file__).resolve().parents[1] PLUGINS = REPO / 'plugins' # Settings a site is not meant to edit through a page. Each needs a reason. EXEMPT = { # Written by the app itself when the agent key is minted, not typed. 'plugin.endpoints.agentkeyprovisioned', 'plugin.endpoints.envkeyfallback', } def _declared_settings(plugin_dir): """Keys the plugin declares in get_settings_defaults(), read from the source. Importing the plugin would need an app context and a database; the declaration is a literal list, so reading it is enough and much cheaper. """ source = (plugin_dir / 'plugin.py') if not source.is_file(): return [] text = source.read_text(encoding='utf-8') match = re.search(r'def get_settings_defaults\(self\).*?(?=\n def |\Z)', text, re.S) if not match: return [] return re.findall(r"'key':\s*'([^']+)'", match.group(0)) def _frontend_text(plugin_dir): frontend = plugin_dir / 'frontend' if not frontend.is_dir(): return '' parts = [] for path in frontend.rglob('*'): if path.suffix in ('.vue', '.js') and path.is_file(): parts.append(path.read_text(encoding='utf-8')) return '\n'.join(parts) PLUGIN_DIRS = sorted( d for d in PLUGINS.iterdir() if d.is_dir() and (d / 'manifest.json').is_file() ) @pytest.mark.parametrize('plugin_dir', PLUGIN_DIRS, ids=lambda d: d.name) def test_every_declared_setting_is_reachable_in_the_ui(plugin_dir): declared = [k for k in _declared_settings(plugin_dir) if k not in EXEMPT] if not declared: pytest.skip('declares no settings') frontend = _frontend_text(plugin_dir) if not frontend: pytest.skip('ships no frontend') missing = [key for key in declared if key not in frontend] assert not missing, ( '%s declares %d setting(s) that appear nowhere in its frontend, so a ' 'site cannot change them without writing SQL:\n %s\n' 'Add them to the plugin settings view (its KEYS list AND a field), or ' 'to EXEMPT in this file with a reason. There is no backlog list: the ' 'seven this check found when it was written were all fixed, and it ' 'stays that way.' % (plugin_dir.name, len(missing), '\n '.join(missing)))