Generate the collector script per site, and bring EventSaver into the repo
A site adopting ShopDB had to be handed two files and told what to edit in them. Both are now the product's, and one of them the server writes for you. GET /api/computers/client-script (admin) returns Report-AssetToShopDB.ps1 with this site's values already in it: site_base_url becomes the -ApiUrl default and the new computers_routableranges setting becomes -AllowedRanges. Only the PARAMETER DEFAULTS are substituted - the copy in plugins/computers/client/ stays runnable, so there is no second version to drift from the first - and everything stamped stays overridable by argument or registry, because a bay may need to differ from its site. Settings > Computers > Asset reporter edits the ranges, downloads the script and shows its SHA-256. The collector key is deliberately not stamped in, and a test fails if it ever is. That file lands on every shop-floor PC, and a token spread across hundreds of bays cannot be rotated quietly; it stays in the registry, provisioned per ADOPTING-AT-ANOTHER-SITE.md. The routable ranges are the last thing that was hardcoded in that script. They are now a setting, so West Jefferson's two CIDRs move out of source code and into that site's own configuration - which is what ADR-015 asks for - and a site that sets nothing still works, because the script falls back to the NIC carrying the default route. EventSaver joins it in plugins/slides/client/, source only: EventSaver.cs and EventSaver.ini, no compiled .scr - a binary is a release asset, like the installer exe. The share path that was compiled into Config.Folder is gone. It used to be the fallback when the ini was missing, which silently pointed a new site at the reference site's file server; it is now empty, and failing visibly beats displaying another site's slides. Verified by compiling the edited source in the Windows VM with the in-box csc.exe: 15,872 bytes, exit 0. Also: the DSC example in the adoption guide gains a CollectorRanges resource and stops passing -ApiUrl to a script that already reads BaseUrl from the registry the same example writes, and the guide points at the generated download instead of hand-editing a URL. The contract test caught the endpoint importing shopdb directly for the version string, which ADR-002 forbids a plugin from doing. The product and contract versions are in app.config now, which a plugin reads through current_app. Adds docs/proposals/printer-assignment.md: assign printers to a PC in ShopDB and let the bay install them, with what the fleet data says about drivers - HP and Xerox cover 41 of 44 printers with universal drivers, there are no Brother printers at all despite 208 files of Brother inkjet drivers in the installer, and printerdrivers holds one row pointing at a per-model folder instead of a universal driver.
This commit is contained in:
122
tests/test_plugins/test_collector_client_script.py
Normal file
122
tests/test_plugins/test_collector_client_script.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""The collector reporter, generated with a site's own values.
|
||||
|
||||
A site used to receive a script with the reference site's server and VLANs in
|
||||
it, which it had to find and edit. The server now stamps its own values into the
|
||||
parameter defaults, so the file downloads ready to deploy.
|
||||
|
||||
What must stay true:
|
||||
|
||||
- ONLY the defaults are substituted. The copy in the repo stays runnable, so
|
||||
there is never a second version to drift from the first.
|
||||
- The collector key is NEVER in the file. It lands on every shop-floor PC, and a
|
||||
token spread across hundreds of bays cannot be rotated quietly.
|
||||
- Everything stamped is still overridable, because a bay may need to differ from
|
||||
its site.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Setting
|
||||
|
||||
|
||||
SCRIPT = '/api/computers/client-script'
|
||||
|
||||
|
||||
def _set(db, key, value):
|
||||
row = Setting.query.filter_by(key=key).first()
|
||||
if row is None:
|
||||
row = Setting(key=key, value=value, valuetype='string', category='computers')
|
||||
db.session.add(row)
|
||||
else:
|
||||
row.value = value
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_an_anonymous_caller_gets_nothing(client):
|
||||
"""It states a site's URL and internal ranges - configuration, not a
|
||||
handout."""
|
||||
assert client.get(SCRIPT).status_code in (401, 422)
|
||||
|
||||
|
||||
def test_it_serves_the_reporter_as_a_download(client, auth_headers):
|
||||
resp = client.get(SCRIPT, headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_data(as_text=True)[:200]
|
||||
assert 'attachment' in resp.headers['Content-Disposition']
|
||||
assert 'Report-AssetToShopDB.ps1' in resp.headers['Content-Disposition']
|
||||
body = resp.get_data(as_text=True)
|
||||
assert 'param(' in body
|
||||
assert 'api/collector/computers' in body
|
||||
|
||||
|
||||
def test_it_publishes_a_hash_of_what_it_served(client, auth_headers):
|
||||
"""So a deployment can verify what it fetched, like the installer does."""
|
||||
import hashlib
|
||||
resp = client.get(SCRIPT, headers=auth_headers)
|
||||
digest = hashlib.sha256(resp.get_data()).hexdigest()
|
||||
assert resp.headers['X-Script-Sha256'] == digest
|
||||
|
||||
|
||||
def test_it_stamps_the_sites_own_url(client, db, auth_headers):
|
||||
_set(db, 'site_base_url', 'https://shopdb.example.net')
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert "[string]$ApiUrl = 'https://shopdb.example.net/api/collector/computers'" in body
|
||||
assert 'for https://shopdb.example.net' in body
|
||||
|
||||
|
||||
def test_a_site_with_no_url_configured_still_gets_a_usable_script(client, db,
|
||||
auth_headers):
|
||||
"""Blank site_base_url falls back to the origin the admin is talking to,
|
||||
which is by definition a reachable address for this server."""
|
||||
_set(db, 'site_base_url', '')
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert "[string]$ApiUrl = ''" not in body
|
||||
assert 'api/collector/computers' in body
|
||||
|
||||
|
||||
def test_it_stamps_the_sites_ranges(client, db, auth_headers):
|
||||
"""The replacement for the two VLANs that used to be source code."""
|
||||
_set(db, 'computers_routableranges', '10.20.0.0/23,10.21.4.0/26')
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert "[string]$AllowedRanges = '10.20.0.0/23,10.21.4.0/26'" in body
|
||||
|
||||
|
||||
def test_no_ranges_configured_leaves_the_default_route_fallback(client, db,
|
||||
auth_headers):
|
||||
"""An unconfigured site must not be given someone else's addressing: the
|
||||
script falls back to the NIC carrying the default route."""
|
||||
_set(db, 'computers_routableranges', '')
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert "[string]$AllowedRanges = ''" in body
|
||||
assert 'default route' in body
|
||||
|
||||
|
||||
def test_the_collector_key_is_never_in_the_file(client, db, auth_headers):
|
||||
"""The one thing that must not be stamped in. A token in a file on every bay
|
||||
is a token nobody can rotate quietly."""
|
||||
_set(db, 'site_base_url', 'https://shopdb.example.net')
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert 'shopdb_pat_' not in body
|
||||
assert "$ApiKey = ''" in body or "[string]$ApiKey = ''" in body
|
||||
# It says where the key comes from instead.
|
||||
assert 'CollectorKey' in body
|
||||
|
||||
|
||||
def test_the_body_is_not_rewritten_only_the_defaults(client, auth_headers):
|
||||
"""The repo copy stays runnable. If generation started editing the body,
|
||||
the file on disk and the file a site runs would diverge."""
|
||||
from plugins.computers.api.routes import _client_script_path
|
||||
with open(_client_script_path(), 'r', encoding='utf-8') as handle:
|
||||
source = handle.read()
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
|
||||
# Every line of the source survives except the two parameter defaults.
|
||||
changed = [line for line in source.splitlines()
|
||||
if line.strip() and line not in body.splitlines()]
|
||||
assert all('$ApiUrl' in line or '$AllowedRanges' in line for line in changed), changed
|
||||
|
||||
|
||||
def test_it_names_the_version_that_generated_it(client, auth_headers):
|
||||
"""So a script found on a bay can be traced to the server that made it."""
|
||||
from shopdb import __version__
|
||||
body = client.get(SCRIPT, headers=auth_headers).get_data(as_text=True)
|
||||
assert __version__ in body
|
||||
Reference in New Issue
Block a user