Files
shopdb-flask/tests/test_plugin_settings_reachable.py

89 lines
3.2 KiB
Python

"""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)))