Gate both kiosks behind a device token, and make an unreachable setting a test failure
This commit is contained in:
88
tests/test_plugin_settings_reachable.py
Normal file
88
tests/test_plugin_settings_reachable.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""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)))
|
||||
Reference in New Issue
Block a user