CI failed one test that passes here: "no such table: settings", seeding a
Setting into a database with no tables. The test was not wrong about the
product, it was asking the process-wide plugin_manager to do the work.
create_app() re-points that singleton, and eight test modules build their own
app, so it belongs to whichever ran last on the xdist worker.
_seed_plugin_settings then opens `with self._app.app_context()` on a different
app's in-memory SQLite. A sibling symptom is the registry coming back None
("'NoneType' object has no attribute 'get_all'"). Both land on whichever test
follows, never on the module that caused them, so they move with the split -
green on one machine, red on another.
The test now builds a manager for itself, sets _app and _db, and calls
_seed_plugin_settings directly. That is the routine `flask plugin upgrade-all`
runs, so what is under test has not changed, and the assertion no longer depends
on worker order. init_app is deliberately not called: it re-registers the plugin
blueprints and a second call on one app raises "The name 'backups' is already
registered".
This does NOT fix the underlying fragility, and that is a separate decision.
Running tests/test_plugins serially today gives 115 failed and 542 errors, from
the same cause; under -n auto it is nearly invisible because modules land in
separate processes. The repair is to stop create_app mutating a module-level
singleton, and it belongs in its own change rather than riding along here.
163 lines
6.7 KiB
Python
163 lines
6.7 KiB
Python
"""The two endpoints switches are declared, seeded, and have a control.
|
|
|
|
Both were readable by their enforcement code before they were declared:
|
|
|
|
plugin.endpoints.revokeddeniessharedkey agentauth.revoked_denies_shared_key
|
|
plugin.endpoints.bootstrapquerytoken bootstrap._query_token_allowed
|
|
|
|
An undeclared key has no row until somebody writes one, and until it was
|
|
declared the settings API upserted it with valuetype 'string' - so
|
|
bool(Setting.get(key, True)) read the text 'false' as True and the switch did
|
|
nothing. Declaring it fixes both halves: the row is seeded at plugin
|
|
install/enable, and a PUT lands as a real boolean.
|
|
|
|
The UI half is checked by reading the pages. A switch with no control on an
|
|
air-gapped box can only be changed with SQL, which is not a setting.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from plugins.endpoints.api import agentauth, bootstrap
|
|
from plugins.endpoints.plugin import EndpointsPlugin
|
|
from shopdb.core.models import Setting
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
VIEWS = REPO / 'plugins' / 'endpoints' / 'frontend' / 'views'
|
|
|
|
REVOKED_KEY = agentauth.REVOKED_DENIES_SHARED_KEY_SETTING
|
|
QUERY_KEY = bootstrap.QUERY_TOKEN_SETTING
|
|
|
|
|
|
def _declared():
|
|
return {entry['key']: entry for entry in
|
|
EndpointsPlugin().get_settings_defaults()}
|
|
|
|
|
|
@pytest.mark.parametrize('key,value', [(REVOKED_KEY, 'false'),
|
|
(QUERY_KEY, 'true')])
|
|
def test_the_switch_is_declared_with_todays_behaviour(key, value):
|
|
entry = _declared().get(key)
|
|
|
|
assert entry is not None, key + ' is read but never declared'
|
|
assert entry['valuetype'] == 'boolean'
|
|
assert entry['category'] == 'plugin.endpoints'
|
|
# The declared default must be what the code does with no row at all, or
|
|
# seeding a site silently changes its behaviour.
|
|
assert entry['value'] == value
|
|
|
|
|
|
@pytest.mark.parametrize('key', [REVOKED_KEY, QUERY_KEY])
|
|
def test_the_description_names_what_turning_it_on_costs(key):
|
|
"""An operator on the shop floor decides from this text alone."""
|
|
description = _declared()[key]['description']
|
|
|
|
assert len(description) > 120, description
|
|
if key == REVOKED_KEY:
|
|
# The revoke + blockreenroll rollback onto an older agent stops working.
|
|
assert 'rollback' in description
|
|
assert 'enrolls again' in description
|
|
else:
|
|
assert 're-issued' in description or 're-issue' in description
|
|
|
|
|
|
def test_the_declared_defaults_match_what_the_code_reads_with_no_row(db):
|
|
"""No Setting rows at all: the readers fall back to these same values."""
|
|
assert Setting.query.filter_by(key=REVOKED_KEY).first() is None
|
|
assert Setting.query.filter_by(key=QUERY_KEY).first() is None
|
|
|
|
assert agentauth.revoked_denies_shared_key() is False
|
|
assert bootstrap._query_token_allowed() is True
|
|
|
|
|
|
def test_the_upgrade_pass_seeds_them_on_a_site_that_installed_earlier(app, db):
|
|
"""install/enable fire only on a state transition, so a fleet already
|
|
running this plugin gets these rows from `flask plugin upgrade-all`, which
|
|
calls sync_all_plugin_settings -> _seed_plugin_settings.
|
|
|
|
This drives _seed_plugin_settings on a manager built for this test rather
|
|
than the process-wide plugin_manager singleton, and that is deliberate.
|
|
create_app() re-points that singleton, eight test modules build their own
|
|
app, and whatever ran last on this xdist worker owns it: the symptoms are
|
|
"no such table: settings" (seeding against another app's in-memory SQLite)
|
|
and "'NoneType' object has no attribute 'get_all'" (registry left None).
|
|
Both land on whichever test follows, so they move with the split - this one
|
|
passed locally and failed on CI for exactly that reason.
|
|
|
|
init_app is not called here either: it re-registers the plugin blueprints,
|
|
and twice on one app raises "The name 'backups' is already registered".
|
|
Setting _app and _db is all the seeding routine reads.
|
|
"""
|
|
from shopdb.plugins import PluginManager
|
|
from plugins.endpoints.plugin import EndpointsPlugin
|
|
|
|
assert Setting.query.filter_by(key=QUERY_KEY).first() is None
|
|
|
|
manager = PluginManager()
|
|
manager._app = app
|
|
manager._db = db
|
|
assert manager._seed_plugin_settings(EndpointsPlugin()) is True
|
|
|
|
for key, value in ((REVOKED_KEY, False), (QUERY_KEY, True)):
|
|
row = Setting.query.filter_by(key=key).one()
|
|
assert row.valuetype == 'boolean'
|
|
assert row.category == 'plugin.endpoints'
|
|
assert row.get_typed_value() is value
|
|
|
|
|
|
def test_a_put_lands_as_a_boolean_and_flips_the_query_token(client, db,
|
|
auth_headers):
|
|
"""End to end for bootstrapquerytoken through the API the page uses.
|
|
|
|
The declaration is what makes the upsert boolean; as a string row the read
|
|
below would stay True whatever was saved.
|
|
"""
|
|
saved = client.put('/api/settings/' + QUERY_KEY, json={'value': False},
|
|
headers=auth_headers)
|
|
|
|
assert saved.status_code == 200, saved.get_json()
|
|
body = saved.get_json()['data']
|
|
assert body['value'] is False
|
|
assert body['valuetype'] == 'boolean'
|
|
assert body['category'] == 'plugin.endpoints'
|
|
assert bootstrap._query_token_allowed() is False
|
|
|
|
back = client.put('/api/settings/' + QUERY_KEY, json={'value': True},
|
|
headers=auth_headers)
|
|
assert back.get_json()['data']['value'] is True
|
|
assert bootstrap._query_token_allowed() is True
|
|
|
|
|
|
def test_the_settings_api_lists_the_declared_switch(client, db, auth_headers):
|
|
"""The agent-key page reads GET /api/settings/<key> to render the box."""
|
|
client.put('/api/settings/' + QUERY_KEY, json={'value': True},
|
|
headers=auth_headers)
|
|
|
|
resp = client.get('/api/settings/' + QUERY_KEY, headers=auth_headers)
|
|
|
|
assert resp.status_code == 200, resp.get_json()
|
|
assert resp.get_json()['data']['value'] is True
|
|
|
|
|
|
def test_the_enrollment_page_carries_the_eviction_checkbox():
|
|
page = (VIEWS / 'EnrollmentSettings.vue').read_text(encoding='utf-8')
|
|
|
|
assert 'toggle-revokeddenies' in page, 'no checkbox for the eviction switch'
|
|
assert 'onRevokedDeniesToggle' in page
|
|
# It sits with the two switches it belongs beside.
|
|
assert 'toggle-sharedkey' in page and 'toggle-autoenroll' in page
|
|
# The operator has to be told what it breaks before clicking it.
|
|
assert 'rollback' in page
|
|
assert 'enrolls again' in page
|
|
|
|
|
|
def test_the_agent_key_page_carries_the_query_token_control():
|
|
page = (VIEWS / 'AgentKeySettings.vue').read_text(encoding='utf-8')
|
|
|
|
assert QUERY_KEY in page, 'the page does not touch the setting'
|
|
assert 'toggle-querytoken' in page
|
|
assert 'onQueryTokenToggle' in page
|
|
# Turning it off breaks commands already handed out, so say so.
|
|
assert 're-issue' in page
|