add mapped drives card

Mapped drives existed only as a comma-joined line in the user context card, which is fine for three drives and unreadable for a dozen. They now get their own list card in Monitors: one row per letter, remote path as meta, health as a badge. Reuses the statecolor/levels vocabulary the tracked-services cards already declare rather than new CSS. Only unhealthy drives are coloured. Green on every working drive makes the broken one just another badge in a row of badges, and the card exists to make the exception findable at a glance. Unhealthy rows sort first for the same reason - a user with a dozen mappings should not hunt for the broken one - and the sort is on health alone, so drive-letter order survives within each group and the list does not reshuffle between refreshes. The merge logic is untouched. _mapped_drive_rows now returns structured rows and the user-context summary line is built from them, so the card and the line cannot drift apart about a drive's status. The two silent-failure paths are mutation-tested: forcing every badge green failed 7 tests, and the first sort test passed a mutation that added a secondary key because every broken drive in it said the same word - it now uses statuses that would reorder under a text sort. Also corrects a stale warning claiming the merge is untested; it has been covered since Phase 1.
This commit is contained in:
benjamin.baker
2026-09-05 07:12:21 -04:00
parent e00bb44d6b
commit 0c9bd1ed32
5 changed files with 230 additions and 20 deletions

View File

@@ -0,0 +1,118 @@
"""The mapped drives card: structured rows, badges, and row order.
The merge logic itself is pinned in test_endpoints_mapped_drives.py. These
tests cover what the CARD adds on top: a per-drive badge colour and the row
order that decides what an operator sees first.
The failure mode this guards is silent: a broken drive rendered green, or
sorted below the fold, leaves the card looking healthy and tells nobody.
"""
from plugins.endpoints.api.routes import (
_mapped_drive_rows, _mapped_drives, _mapped_drives_unhealthy_first,
)
def _context(netuse=None, smbmapping=None):
context = {}
if netuse is not None:
context['netuse'] = netuse
if smbmapping is not None:
context['smbmapping'] = smbmapping
return context
def _netuse(local, remote, status):
return {'localname': local, 'remotename': remote, 'status': status}
def test_a_healthy_drive_is_green():
rows = _mapped_drive_rows(_context(netuse=[_netuse('Z:', r'\\srv\share', 'OK')]))
assert rows[0]['statecolor'] == 'green'
assert rows[0]['statuslabel'] == 'OK'
def test_a_disconnected_drive_is_red():
rows = _mapped_drive_rows(
_context(netuse=[_netuse('Z:', r'\\srv\share', 'Disconnected')]))
assert rows[0]['statecolor'] == 'red'
def test_connected_counts_as_healthy_alongside_ok():
"""net use and Get-SmbMapping use different words for the same state."""
rows = _mapped_drive_rows(_context(netuse=[_netuse('Z:', r'\\srv\s', 'Connected')]))
assert rows[0]['statecolor'] == 'green'
def test_a_drive_with_no_status_is_red_not_green():
"""An uncoloured row reads as working. Unknown health is not health."""
rows = _mapped_drive_rows(_context(netuse=[_netuse('Z:', r'\\srv\s', None)]))
assert rows[0]['statuslabel'] == 'Unknown'
assert rows[0]['statecolor'] == 'red'
def test_an_unrecognised_status_is_not_coloured_green():
rows = _mapped_drive_rows(_context(netuse=[_netuse('Z:', r'\\srv\s', 'Weird')]))
assert rows[0]['statecolor'] == 'red'
def test_the_worse_of_two_sources_drives_the_badge_colour():
"""The merge already picks the worse status; the badge must follow it."""
rows = _mapped_drive_rows(_context(
netuse=[_netuse('Z:', r'\\srv\share', 'OK')],
smbmapping=[{'localpath': 'Z:', 'remotepath': r'\\srv\share',
'status': 'Disconnected'}],
))
assert len(rows) == 1
assert rows[0]['statecolor'] == 'red'
def test_the_remote_path_is_carried_as_its_own_field():
"""The card shows the path separately rather than glued into the title."""
rows = _mapped_drive_rows(_context(netuse=[_netuse('Z:', r'\\srv\share', 'OK')]))
assert rows[0]['drive'] == 'Z:'
assert rows[0]['remote'] == r'\\srv\share'
def test_the_summary_line_and_the_card_agree_about_a_broken_drive():
"""Both are built from the same rows, so they cannot drift apart."""
context = _context(netuse=[_netuse('Z:', r'\\srv\share', 'Disconnected')])
assert _mapped_drive_rows(context)[0]['statecolor'] == 'red'
assert '(Disconnected)' in _mapped_drives(context)[0]
def test_a_healthy_drive_is_not_annotated_in_the_summary_line():
context = _context(netuse=[_netuse('Z:', r'\\srv\share', 'OK')])
assert _mapped_drives(context) == [r'Z: \\srv\share']
def test_no_user_context_yields_no_rows():
assert _mapped_drive_rows({}) == []
def test_broken_drives_sort_above_healthy_ones():
"""The broken one must be on screen without scrolling past nine good ones."""
rows = _mapped_drive_rows(_context(netuse=[
_netuse('H:', r'\\srv\home', 'OK'),
_netuse('S:', r'\\srv\shared', 'OK'),
_netuse('Z:', r'\\srv\dead', 'Disconnected'),
]))
ordered = _mapped_drives_unhealthy_first(rows)
assert [row['drive'] for row in ordered] == ['Z:', 'H:', 'S:']
def test_drive_letter_order_survives_within_each_health_group():
"""Sorting only on health keeps the list from reshuffling on refresh.
The unhealthy drives here carry DIFFERENT status words, chosen so that
sorting on the status text would reorder them (Disconnected before
Unavailable) against drive-letter order. A secondary sort key would pass
a test where every broken drive said the same thing.
"""
rows = _mapped_drive_rows(_context(netuse=[
_netuse('A:', r'\\srv\a', 'OK'),
_netuse('B:', r'\\srv\b', 'Unavailable'),
_netuse('C:', r'\\srv\c', 'Connected'),
_netuse('D:', r'\\srv\d', 'Disconnected'),
]))
ordered = _mapped_drives_unhealthy_first(rows)
assert [row['drive'] for row in ordered] == ['B:', 'D:', 'A:', 'C:']

View File

@@ -69,7 +69,7 @@ def test_endpoints_asset_panels_are_computer_scoped():
"""Every asset panel targets computer assets only, with an endpoint."""
plugin = EndpointsPlugin()
panels = plugin.get_asset_panels()
assert len(panels) == 12
assert len(panels) == 13
for panel in panels:
assert panel['assettypes'] == ['computer']
assert panel['endpoint'].startswith('/api/endpoints/')