89 lines
3.2 KiB
Python
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)))
|