Show the kiosk label prefix, and let a plugin declare the settings it owns
Three defects, all found on printedparts_label_prefix, all one root cause: nothing in the framework knew that setting existed. The parts kiosk runs logged out. An unauthenticated read of a setting is limited to an allowlist, the key was not on it, so the kiosk got a 404 and fell back to no prefix. An admin previewing the same page while logged in saw the prefix, which is why it looked like it worked. The same setting also looked like it would not save. The row did not exist on a site that installed the plugin before the setting was added, so the first save created it - under the placeholder category the settings API uses for keys it does not recognise, where the plugin's settings page, which lists by category, could no longer see it. The value was in the database the whole time. And the row was missing in the first place because seeding ran from on_install / on_enable, which fire only on a state transition. Neither runs again on an upgrade, so a setting added in a later plugin version never reached a site that installed an earlier one. The comment claiming enable ran every upgrade cycle was simply wrong. A plugin now declares the settings it owns in get_settings_defaults(): key, default, type, category, description, and whether a logged-out page may read it. The framework seeds declared keys at install, at enable, and on every flask plugin upgrade-all; files a first-time write under the declared category; re-homes any row left in the placeholder category, value untouched; and answers an anonymous read for keys marked public. Core carries no list of any plugin's keys. Contract 0.16.0 (additive optional hook). printedparts and printers move to the hook and floor their core_version at 0.16.0. The dev database had two rows in the misfiled state (printedparts_alert_email, employee_db_host); the first repairs itself on the next upgrade pass.
This commit is contained in:
148
tests/test_core/test_settings_plugin_declared.py
Normal file
148
tests/test_core/test_settings_plugin_declared.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Tests for plugin-declared settings (contract 0.16.0).
|
||||
|
||||
Three regressions are pinned here, all found on the 3D-parts kiosk label
|
||||
prefix:
|
||||
|
||||
1. The kiosk runs logged out, so a setting it renders must be readable by an
|
||||
anonymous caller when the owning plugin declares it public - and only then.
|
||||
2. A save that creates the row for the first time must file it under the
|
||||
declared category, or the plugin's own settings page (which lists by
|
||||
category) stops seeing the value and the save looks lost.
|
||||
3. Settings declared by a later plugin version must reach a site that installed
|
||||
an earlier one; on_install/on_enable fire only on a state transition, so the
|
||||
upgrade pass has to seed them.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Setting
|
||||
|
||||
KIOSK_KEY = 'printedparts_label_prefix'
|
||||
PRIVATE_KEY = 'printedparts_alert_email'
|
||||
|
||||
|
||||
def _plugin(app, name='printedparts'):
|
||||
pm = app.extensions.get('plugin_manager')
|
||||
plugin = pm.loader.get_loaded_plugin(name) if pm else None
|
||||
if plugin is None:
|
||||
pytest.skip(f'{name} plugin not loaded in this build')
|
||||
return pm, plugin
|
||||
|
||||
|
||||
def test_plugin_declares_the_kiosk_prefix_public(app):
|
||||
_, plugin = _plugin(app)
|
||||
declared = {entry['key']: entry for entry in plugin.get_settings_defaults()}
|
||||
assert declared[KIOSK_KEY]['public'] is True
|
||||
assert declared[KIOSK_KEY]['category'] == 'printedparts'
|
||||
# Everything else the plugin owns stays behind auth.
|
||||
assert not any(entry.get('public') for key, entry in declared.items()
|
||||
if key != KIOSK_KEY)
|
||||
|
||||
|
||||
def test_anon_can_read_a_public_plugin_setting(client, db):
|
||||
"""The kiosk reads this with no token; a 404 leaves the prefix blank."""
|
||||
Setting.set(KIOSK_KEY, 'WJ', valuetype='string', category='printedparts')
|
||||
|
||||
resp = client.get(f'/api/settings/{KIOSK_KEY}')
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['value'] == 'WJ'
|
||||
|
||||
|
||||
def test_anon_cannot_read_a_nonpublic_plugin_setting(client, db):
|
||||
Setting.set(PRIVATE_KEY, 'lead@example.com', valuetype='string',
|
||||
category='printedparts')
|
||||
|
||||
resp = client.get(f'/api/settings/{PRIVATE_KEY}')
|
||||
|
||||
assert resp.status_code == 404
|
||||
body = resp.get_data(as_text=True)
|
||||
assert 'lead@example.com' not in body
|
||||
|
||||
|
||||
def test_anon_list_includes_public_plugin_settings_only(client, db):
|
||||
Setting.set(KIOSK_KEY, 'WJ', valuetype='string', category='printedparts')
|
||||
Setting.set(PRIVATE_KEY, 'lead@example.com', valuetype='string',
|
||||
category='printedparts')
|
||||
|
||||
resp = client.get('/api/settings?category=printedparts')
|
||||
|
||||
assert resp.status_code == 200
|
||||
keys = {row['key'] for row in resp.get_json()['data']}
|
||||
assert keys == {KIOSK_KEY}
|
||||
|
||||
|
||||
def test_first_save_files_the_key_under_its_declared_category(client, db,
|
||||
auth_headers):
|
||||
"""Save then reload, the way the settings page does it.
|
||||
|
||||
The row does not exist yet (the site installed before the key was added),
|
||||
so the PUT creates it. Filed under a placeholder category it would vanish
|
||||
from the page's category-filtered reload - the "it does not save" bug.
|
||||
"""
|
||||
assert Setting.query.filter_by(key=KIOSK_KEY).first() is None
|
||||
|
||||
resp = client.put(f'/api/settings/{KIOSK_KEY}', json={'value': 'WJ'},
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
|
||||
setting = Setting.query.filter_by(key=KIOSK_KEY).first()
|
||||
assert setting.category == 'printedparts'
|
||||
|
||||
reload = client.get('/api/settings?category=printedparts',
|
||||
headers=auth_headers)
|
||||
values = {row['key']: row['value'] for row in reload.get_json()['data']}
|
||||
assert values[KIOSK_KEY] == 'WJ'
|
||||
|
||||
|
||||
def test_first_save_of_a_core_key_keeps_its_core_category(client, db,
|
||||
auth_headers):
|
||||
resp = client.put('/api/settings/facility_name',
|
||||
json={'value': 'Test Plant'}, headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
|
||||
assert Setting.query.filter_by(key='facility_name').first().category == 'site'
|
||||
|
||||
|
||||
def test_undeclared_key_still_upserts(client, db, auth_headers):
|
||||
"""An ad-hoc key nobody declares keeps the old placeholder behavior."""
|
||||
resp = client.put('/api/settings/some_adhoc_key', json={'value': 'x'},
|
||||
headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert Setting.query.filter_by(key='some_adhoc_key').first().category == 'plugin'
|
||||
|
||||
|
||||
def test_upgrade_seeds_settings_missing_from_an_existing_site(app, db):
|
||||
"""`flask plugin upgrade-all` reaches a site that installed earlier."""
|
||||
pm, _ = _plugin(app)
|
||||
assert Setting.query.filter_by(key=KIOSK_KEY).first() is None
|
||||
|
||||
pm.sync_all_plugin_settings()
|
||||
|
||||
setting = Setting.query.filter_by(key=KIOSK_KEY).first()
|
||||
assert setting is not None
|
||||
assert setting.category == 'printedparts'
|
||||
assert setting.value == ''
|
||||
|
||||
|
||||
def test_sync_never_overwrites_a_configured_value(app, db):
|
||||
pm, _ = _plugin(app)
|
||||
Setting.set(KIOSK_KEY, 'WJ', valuetype='string', category='printedparts')
|
||||
|
||||
pm.sync_all_plugin_settings()
|
||||
|
||||
assert Setting.query.filter_by(key=KIOSK_KEY).first().value == 'WJ'
|
||||
|
||||
|
||||
def test_sync_rehomes_a_row_left_in_the_placeholder_category(app, db):
|
||||
"""Repairs rows an earlier save created before the key was declared."""
|
||||
pm, _ = _plugin(app)
|
||||
Setting.set(PRIVATE_KEY, 'lead@example.com', valuetype='string',
|
||||
category='plugin')
|
||||
|
||||
pm.sync_all_plugin_settings()
|
||||
|
||||
setting = Setting.query.filter_by(key=PRIVATE_KEY).first()
|
||||
assert setting.category == 'printedparts'
|
||||
assert setting.value == 'lead@example.com'
|
||||
Reference in New Issue
Block a user