mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-09-23 14:24:57 -04:00
FE+BE: performance improvements
This commit is contained in:
1 parent
cff3ddfc38
commit
f52de6cbdc
23 files changed
+428
-31
No files matched your search
@@ -208,6 +208,53 @@ def test_devices_by_status(client, api_token, test_mac):
|
||||
delete_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
def test_devices_by_status_pagination(client, api_token):
|
||||
"""limit/offset must page through the same set ORDER BY devMac gives
|
||||
unpaginated, with no gaps or duplicates, and must reject invalid values.
|
||||
Doesn't assume an otherwise-empty DB: reconstructs the full 'my' list from
|
||||
pages and compares it to the unpaginated response instead of asserting
|
||||
exact positions for the 3 dummies.
|
||||
"""
|
||||
macs = [f"aa:bb:cc:dd:ee:0{i}" for i in (1, 2, 3)]
|
||||
for mac in macs:
|
||||
create_dummy(client, api_token, mac)
|
||||
|
||||
try:
|
||||
full_resp = client.get("/devices/by-status?status=my", headers=auth_headers(api_token))
|
||||
assert full_resp.status_code == 200
|
||||
full_macs = [d["id"] for d in full_resp.json]
|
||||
assert set(macs).issubset(set(full_macs))
|
||||
|
||||
# Page through the full set in halves and confirm the reassembled
|
||||
# list matches the unpaginated one exactly (no gaps/duplicates).
|
||||
total = len(full_macs)
|
||||
half = (total + 1) // 2
|
||||
page1 = client.get(
|
||||
f"/devices/by-status?status=my&limit={half}&offset=0",
|
||||
headers=auth_headers(api_token),
|
||||
).json
|
||||
page2 = client.get(
|
||||
f"/devices/by-status?status=my&limit={total - half}&offset={half}",
|
||||
headers=auth_headers(api_token),
|
||||
).json
|
||||
paged_macs = [d["id"] for d in page1] + [d["id"] for d in page2]
|
||||
assert paged_macs == full_macs
|
||||
|
||||
# Invalid limit/offset are rejected, not silently clamped.
|
||||
resp_bad_limit = client.get(
|
||||
"/devices/by-status?status=my&limit=0", headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp_bad_limit.status_code == 422
|
||||
|
||||
resp_bad_offset = client.get(
|
||||
"/devices/by-status?status=my&offset=-1", headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp_bad_offset.status_code == 422
|
||||
finally:
|
||||
for mac in macs:
|
||||
delete_dummy(client, api_token, mac)
|
||||
|
||||
|
||||
def test_delete_test_devices(client, api_token):
|
||||
|
||||
# Delete by MAC
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from helper import get_setting_value, count_children_by_parent_mac # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@@ -79,6 +79,47 @@ def test_graphql_post_devices(client, api_token):
|
||||
assert isinstance(data["devices"]["count"], int)
|
||||
|
||||
|
||||
def test_graphql_devices_parent_children_count_matches_recount(client, api_token):
|
||||
"""devParentChildrenCount for every returned device must match an independent
|
||||
recount of the same response (regression guard for the O(n^2)->O(n) rewrite of
|
||||
resolve_devices()/count_children_by_parent_mac() in graphql_endpoint.py/helper.py).
|
||||
|
||||
Not seeded against a freshly-created fixture pair: table_devices.json (what
|
||||
resolve_devices() reads) is only refreshed by the periodic update_api() loop,
|
||||
not synchronously on a POST /device/<mac> create - a create-then-query test
|
||||
would be flaky against snapshot staleness. Recomputing from the response
|
||||
itself avoids that while still exercising the real resolver wiring.
|
||||
"""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
devices {
|
||||
devices {
|
||||
devMac
|
||||
devParentMAC
|
||||
devParentChildrenCount
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
resp = client.post("/graphql", json=query, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
devices = resp.get_json()["data"]["devices"]["devices"]
|
||||
expected_counts = count_children_by_parent_mac(
|
||||
[{"devParentMAC": d["devParentMAC"]} for d in devices]
|
||||
)
|
||||
|
||||
for device in devices:
|
||||
expected = expected_counts.get((device["devMac"] or "").strip(), 0)
|
||||
assert device["devParentChildrenCount"] == expected, (
|
||||
f"devMac={device['devMac']}: expected {expected}, "
|
||||
f"got {device['devParentChildrenCount']}"
|
||||
)
|
||||
|
||||
|
||||
# --- SETTINGS TESTS ---
|
||||
def test_graphql_post_settings(client, api_token):
|
||||
"""POST /graphql should return settings data"""
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Unit tests for helper.py's count_children_by_parent_mac().
|
||||
|
||||
Tests verify the O(n) bucket-count replacement for the old per-device
|
||||
O(n) scan (get_number_of_children) produces identical results.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import count_children_by_parent_mac # noqa: E402
|
||||
|
||||
|
||||
class TestCountChildrenByParentMac:
|
||||
"""Test suite for count_children_by_parent_mac()"""
|
||||
|
||||
def test_empty_list_returns_empty_dict(self):
|
||||
assert count_children_by_parent_mac([]) == {}
|
||||
|
||||
def test_parent_with_two_children_and_a_grandchild(self):
|
||||
devices = [
|
||||
{"devMac": "aa:aa:aa:aa:aa:aa", "devParentMAC": ""},
|
||||
{"devMac": "bb:bb:bb:bb:bb:bb", "devParentMAC": "aa:aa:aa:aa:aa:aa"},
|
||||
{"devMac": "cc:cc:cc:cc:cc:cc", "devParentMAC": "aa:aa:aa:aa:aa:aa"},
|
||||
{"devMac": "dd:dd:dd:dd:dd:dd", "devParentMAC": "bb:bb:bb:bb:bb:bb"},
|
||||
]
|
||||
|
||||
counts = count_children_by_parent_mac(devices)
|
||||
|
||||
assert counts["aa:aa:aa:aa:aa:aa"] == 2
|
||||
assert counts["bb:bb:bb:bb:bb:bb"] == 1
|
||||
# leaf devices are absent from the dict, not present with a 0 value
|
||||
assert "cc:cc:cc:cc:cc:cc" not in counts
|
||||
assert "dd:dd:dd:dd:dd:dd" not in counts
|
||||
|
||||
def test_missing_or_empty_parent_mac_excluded(self):
|
||||
devices = [
|
||||
{"devMac": "aa:aa:aa:aa:aa:aa", "devParentMAC": ""},
|
||||
{"devMac": "bb:bb:bb:bb:bb:bb"}, # devParentMAC key absent entirely
|
||||
]
|
||||
|
||||
assert count_children_by_parent_mac(devices) == {}
|
||||
|
||||
def test_result_independent_of_input_order(self):
|
||||
devices = [
|
||||
{"devMac": "bb:bb:bb:bb:bb:bb", "devParentMAC": "aa:aa:aa:aa:aa:aa"},
|
||||
{"devMac": "aa:aa:aa:aa:aa:aa", "devParentMAC": ""},
|
||||
{"devMac": "cc:cc:cc:cc:cc:cc", "devParentMAC": "aa:aa:aa:aa:aa:aa"},
|
||||
]
|
||||
|
||||
assert count_children_by_parent_mac(devices) == count_children_by_parent_mac(
|
||||
list(reversed(devices))
|
||||
)
|
||||
|
||||
def test_lookup_uses_stripped_devmac_like_original(self):
|
||||
# Caller strips devMac before lookup (graphql_endpoint.py); the count
|
||||
# dict's keys must match that stripped form for the lookup to hit.
|
||||
devices = [
|
||||
{"devMac": "aa:aa:aa:aa:aa:aa", "devParentMAC": ""},
|
||||
{"devMac": "bb:bb:bb:bb:bb:bb", "devParentMAC": " aa:aa:aa:aa:aa:aa "},
|
||||
]
|
||||
|
||||
counts = count_children_by_parent_mac(devices)
|
||||
|
||||
assert counts["aa:aa:aa:aa:aa:aa".strip()] == 1
|
||||
Reference in new issue
Block a user