network: generate a device's asset number instead of asking twice
Every network device on this fleet already follows one convention, applied by hand: AP-<name>, SW-<name>, SVR-<name>, IDF-<name>. 45 records, no exceptions. The create form demanded the asset number anyway, so the same value was typed twice and the convention held only as long as everyone remembered it. The prefix now lives on the device type, and a blank asset number is generated as <PREFIX>-<name>. Left explicit, an asset number always wins: a device carrying a real identifier of its own - a vendor tag, a controller name, a serial - keeps it. That is the platform rule, adopt where an identifier exists and derive only where none does. The prefix is NOT derived from the type name. "Access Point" and "Access Panel" both initialise to AP, and assetnumber is unique, so the second type would collide with the first on every device it created. It is nullable, so a type that wants no prefix generates the bare name rather than needing one invented. Names are sanitised before they reach a business key - the existing data already shows why, with IDF-Telco-Demarc-#1 carrying a '#' into an identifier. An existing prefix is never stacked: IDF-03 under type IDF stays IDF-03.
This commit is contained in:
@@ -52,7 +52,7 @@ EXPECTED_HEAD_REVISION['measuringtools'] = 'measuringtools0001baseline'
|
||||
EXPECTED_HEAD_REVISION['backups'] = 'backups0003clearlastseen'
|
||||
# geenforce adds the content-addressed blob store (manifestblobs) on top of its
|
||||
# baseline.
|
||||
EXPECTED_HEAD_REVISION['geenforce'] = 'geenforce0003subtype'
|
||||
EXPECTED_HEAD_REVISION['geenforce'] = 'geenforce0004minlib'
|
||||
# printers adds the printersupplyalerts crossing-state table on top of its anchor.
|
||||
EXPECTED_HEAD_REVISION['printers'] = 'printers0002supplyalerts'
|
||||
# machines (renamed from equipment) keeps its original anchor id and adds the
|
||||
@@ -64,7 +64,7 @@ EXPECTED_HEAD_REVISION['employees'] = 'employees0002photo'
|
||||
EXPECTED_HEAD_REVISION['usb'] = 'usb0002dropmachineid'
|
||||
# network links a device to a catalog model, which is where its photo comes
|
||||
# from - machines, PCs and printers already had that link.
|
||||
EXPECTED_HEAD_REVISION['network'] = 'network0002model'
|
||||
EXPECTED_HEAD_REVISION['network'] = 'network0003prefix'
|
||||
# printedparts is post-cutover: its 0001 really creates its tables; 0004 adds
|
||||
# the per-transaction revision column.
|
||||
EXPECTED_HEAD_REVISION['printedparts'] = 'printedparts0004txnrev'
|
||||
|
||||
108
tests/test_plugins/test_network_assetnumber_generation.py
Normal file
108
tests/test_plugins/test_network_assetnumber_generation.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Asset numbers generated from the device type's prefix.
|
||||
|
||||
Every network device on this fleet already followed one convention applied by
|
||||
hand - AP-<name>, SW-<name>, SVR-<name>, IDF-<name>, 45 records, no exceptions.
|
||||
Generation codifies it so the same value is not typed twice, and must reproduce
|
||||
those records exactly rather than inventing a new scheme.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import AssetType
|
||||
from plugins.network.models import NetworkDeviceType
|
||||
|
||||
|
||||
def _devicetype(db, name, prefix):
|
||||
"""A device type, with the core asset type the create path needs present."""
|
||||
if not AssetType.query.filter_by(assettype='network_device').first():
|
||||
db.session.add(AssetType(assettype='network_device'))
|
||||
devicetype = NetworkDeviceType(networkdevicetype=name, prefix=prefix)
|
||||
db.session.add(devicetype)
|
||||
db.session.commit()
|
||||
return devicetype
|
||||
from plugins.network.services.assetnumbers import generate, sanitize
|
||||
|
||||
|
||||
@pytest.mark.parametrize('prefix,name,expected', [
|
||||
('AP', 'AIRwdMUSwestj02', 'AP-AIRwdMUSwestj02'),
|
||||
('SW', 'AIRsdMUSwestj01', 'SW-AIRsdMUSwestj01'),
|
||||
('SVR', 'AVEWP4546V01', 'SVR-AVEWP4546V01'),
|
||||
])
|
||||
def test_reproduces_the_existing_convention(prefix, name, expected):
|
||||
assert generate(prefix, name) == expected
|
||||
|
||||
|
||||
def test_a_type_with_no_prefix_gives_the_bare_name():
|
||||
"""Nullable on purpose - nobody invents a prefix for a type that wants none."""
|
||||
assert generate(None, 'Bare Name') == 'Bare-Name'
|
||||
assert generate('', 'Bare Name') == 'Bare-Name'
|
||||
|
||||
|
||||
def test_an_already_prefixed_name_is_not_stacked():
|
||||
"""IDF-03 under type IDF is IDF-03, never IDF-IDF-03. Real row on this data."""
|
||||
assert generate('IDF', 'IDF-03') == 'IDF-03'
|
||||
assert generate('idf', 'idf-03') == 'idf-03'
|
||||
|
||||
|
||||
def test_characters_that_break_a_business_key_are_stripped():
|
||||
"""assetnumber lands in URLs and in joins. The existing IDF-Telco-Demarc-#1
|
||||
row shows a '#' reaching a unique key, which is what this prevents."""
|
||||
assert generate('IDF', 'Telco Demarc #1') == 'IDF-Telco-Demarc-1'
|
||||
assert generate('APNL', 'Door 4 / West') == 'APNL-Door-4-West'
|
||||
assert sanitize(' spaced out ') == 'spaced-out'
|
||||
|
||||
|
||||
def test_nothing_to_build_from_returns_empty():
|
||||
"""So the caller demands an explicit value instead of inventing one."""
|
||||
assert generate('AP', '') == ''
|
||||
assert generate('AP', None) == ''
|
||||
|
||||
|
||||
def test_create_generates_when_assetnumber_is_blank(client, db, auth_headers):
|
||||
devicetype = _devicetype(db, 'Access Panel', 'APNL')
|
||||
|
||||
resp = client.post('/api/network', json={
|
||||
'name': 'Dock Door 4',
|
||||
'networkdevicetypeid': devicetype.networkdevicetypeid,
|
||||
}, headers=auth_headers)
|
||||
assert resp.status_code == 201, resp.get_json()
|
||||
assert resp.get_json()['data']['assetnumber'] == 'APNL-Dock-Door-4'
|
||||
|
||||
|
||||
def test_an_explicit_assetnumber_always_wins(client, db, auth_headers):
|
||||
"""Adopt an external identifier where one exists - a vendor tag beats a
|
||||
generated value, which is why generation only fills a BLANK field."""
|
||||
devicetype = _devicetype(db, 'Access Panel 2', 'APNL')
|
||||
|
||||
resp = client.post('/api/network', json={
|
||||
'assetnumber': 'VENDORTAG-99',
|
||||
'name': 'Dock Door 5',
|
||||
'networkdevicetypeid': devicetype.networkdevicetypeid,
|
||||
}, headers=auth_headers)
|
||||
assert resp.status_code == 201, resp.get_json()
|
||||
assert resp.get_json()['data']['assetnumber'] == 'VENDORTAG-99'
|
||||
|
||||
|
||||
def test_no_name_and_no_assetnumber_is_refused(client, db, auth_headers):
|
||||
devicetype = _devicetype(db, 'Access Panel 3', 'APNL')
|
||||
|
||||
resp = client.post('/api/network', json={
|
||||
'networkdevicetypeid': devicetype.networkdevicetypeid,
|
||||
}, headers=auth_headers)
|
||||
assert resp.status_code == 400
|
||||
assert 'assetnumber is required' in resp.get_json()['data']['error']['message']
|
||||
|
||||
|
||||
def test_prefix_round_trips_through_the_type_api(client, db, auth_headers):
|
||||
created = client.post('/api/network/types', json={
|
||||
'networkdevicetype': 'Door Controller', 'prefix': 'DOOR',
|
||||
}, headers=auth_headers)
|
||||
assert created.status_code == 201, created.get_json()
|
||||
assert created.get_json()['data']['prefix'] == 'DOOR'
|
||||
|
||||
typeid = created.get_json()['data']['networkdevicetypeid']
|
||||
updated = client.put(f'/api/network/types/{typeid}', json={'prefix': 'DR'},
|
||||
headers=auth_headers)
|
||||
assert updated.status_code == 200, updated.get_json()
|
||||
assert updated.get_json()['data']['prefix'] == 'DR'
|
||||
Reference in New Issue
Block a user