The network device form had no IP field, so the one thing people look up a switch for could not be entered. The API had accepted an ipaddress since the legacy import work - create, update and every read already carried it - but nothing in the UI ever sent one, which also left the device off the "Devices on this network" list, since that matches on the IP a device does not have. The field sits next to Hostname and round-trips through the Communication row the platform keeps IPs in, the same way the PC and printer forms do. Clearing it now clears the stored address. The upsert helper returned early on a blank value despite a docstring promising it cleared - harmless while no form could submit one, wrong the moment this field existed.
102 lines
4.5 KiB
Python
102 lines
4.5 KiB
Python
"""Network device create/update attaches a primary IP Communication.
|
|
|
|
Classic network devices carry their IP in the communications table, and the
|
|
legacy import needs an HTTP path to set it. The network create/update routes
|
|
accept an `ipaddress` and materialize a primary Communication (mirroring the
|
|
printer route); GET surfaces it back.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def network_setup(db):
|
|
from shopdb.core.models import AssetType, CommunicationType
|
|
db.session.add(AssetType(assettype='network_device', pluginname='network',
|
|
tablename='networkdevices', description='Network devices'))
|
|
db.session.add(CommunicationType(comtype='IP', description='IP'))
|
|
db.session.commit()
|
|
|
|
|
|
def test_create_network_device_with_ipaddress(client, db, auth_headers, network_setup):
|
|
"""POST /api/network with an ipaddress creates a primary Communication and
|
|
echoes the IP back."""
|
|
from shopdb.core.models import Communication
|
|
resp = client.post('/api/network', json={
|
|
'assetnumber': 'SW-CORE-01', 'name': 'Core switch',
|
|
'hostname': 'sw-core-01', 'ipaddress': '10.129.22.101',
|
|
}, headers=auth_headers)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
data = resp.get_json()['data']
|
|
assert data['ipaddress'] == '10.129.22.101'
|
|
|
|
assetid = data['assetid']
|
|
comm = Communication.query.filter_by(assetid=assetid, isprimary=True).first()
|
|
assert comm is not None and comm.ipaddress == '10.129.22.101'
|
|
|
|
# GET surfaces it too.
|
|
got = client.get(f"/api/network/{data['networkdevice']['networkdeviceid']}",
|
|
headers=auth_headers)
|
|
assert got.get_json()['data']['ipaddress'] == '10.129.22.101'
|
|
|
|
|
|
def test_update_network_device_ipaddress_upserts(client, db, auth_headers, network_setup):
|
|
"""PUT changes the IP in place without creating a second Communication."""
|
|
from shopdb.core.models import Communication
|
|
created = client.post('/api/network', json={
|
|
'assetnumber': 'AP-01', 'name': 'AP', 'ipaddress': '10.0.0.5',
|
|
}, headers=auth_headers).get_json()['data']
|
|
devid = created['networkdevice']['networkdeviceid']
|
|
|
|
updated = client.put(f'/api/network/{devid}', json={'ipaddress': '10.0.0.9'},
|
|
headers=auth_headers)
|
|
assert updated.status_code == 200, updated.get_json()
|
|
assert updated.get_json()['data']['ipaddress'] == '10.0.0.9'
|
|
|
|
comms = Communication.query.filter_by(assetid=created['assetid']).all()
|
|
assert len(comms) == 1 and comms[0].ipaddress == '10.0.0.9'
|
|
|
|
|
|
def test_clearing_the_ip_takes_it_back_out(client, db, auth_headers, network_setup):
|
|
"""Blanking the form field must clear the stored IP.
|
|
|
|
The upsert helper returned early on a blank value, so an address could be
|
|
typed and never removed - invisible until the form grew an IP field.
|
|
"""
|
|
from shopdb.core.models import Communication
|
|
created = client.post('/api/network', json={
|
|
'assetnumber': 'SW-09', 'name': 'Switch', 'ipaddress': '10.0.0.5',
|
|
}, headers=auth_headers).get_json()['data']
|
|
devid = created['networkdevice']['networkdeviceid']
|
|
|
|
cleared = client.put(f'/api/network/{devid}', json={'ipaddress': ''},
|
|
headers=auth_headers)
|
|
assert cleared.status_code == 200, cleared.get_json()
|
|
assert not cleared.get_json()['data']['ipaddress']
|
|
|
|
comms = Communication.query.filter_by(assetid=created['assetid']).all()
|
|
assert len(comms) == 1 and comms[0].ipaddress is None
|
|
|
|
|
|
def test_omitting_ipaddress_leaves_it_alone(client, db, auth_headers, network_setup):
|
|
"""A PUT that never mentions the field must not wipe the stored IP."""
|
|
created = client.post('/api/network', json={
|
|
'assetnumber': 'SW-10', 'name': 'Switch', 'ipaddress': '10.0.0.7',
|
|
}, headers=auth_headers).get_json()['data']
|
|
devid = created['networkdevice']['networkdeviceid']
|
|
|
|
updated = client.put(f'/api/network/{devid}', json={'hostname': 'sw-10'},
|
|
headers=auth_headers)
|
|
assert updated.get_json()['data']['ipaddress'] == '10.0.0.7'
|
|
|
|
|
|
def test_seed_reference_data_creates_ip_commtype(app, db):
|
|
"""The reference-data seed now creates the IP communication type the asset
|
|
import depends on."""
|
|
from shopdb.core.models import CommunicationType
|
|
runner = app.test_cli_runner()
|
|
assert runner.invoke(args=['seed', 'reference-data']).exit_code in (0, None)
|
|
with app.app_context():
|
|
assert CommunicationType.query.filter_by(comtype='IP').first() is not None
|
|
assert CommunicationType.query.count() >= 8
|