mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-03 16:51:24 -07:00
Coderabbit fixes:
- Mac - Flask debug - Threaded flask - propagate token in GET requests - enhance spec docs - normalize MAC x2 - mcp disablement redundant private attribute - run all tests imports
This commit is contained in:
70
test/api_endpoints/test_device_update_normalization.py
Normal file
70
test/api_endpoints/test_device_update_normalization.py
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
import pytest
|
||||
import random
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from models.device_instance import DeviceInstance
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_token():
|
||||
return get_setting_value("API_TOKEN")
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
def test_mac_norm():
|
||||
# Normalized MAC
|
||||
return "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
@pytest.fixture
|
||||
def test_parent_mac_input():
|
||||
# Lowercase input MAC
|
||||
return "aa:bb:cc:dd:ee:00"
|
||||
|
||||
@pytest.fixture
|
||||
def test_parent_mac_norm():
|
||||
# Normalized expected MAC
|
||||
return "AA:BB:CC:DD:EE:00"
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
def test_update_normalization(client, api_token, test_mac_norm, test_parent_mac_input, test_parent_mac_norm):
|
||||
# 1. Create a device (using normalized MAC)
|
||||
create_payload = {
|
||||
"createNew": True,
|
||||
"devName": "Normalization Test Device",
|
||||
"devOwner": "Unit Test",
|
||||
}
|
||||
resp = client.post(f"/device/{test_mac_norm}", json=create_payload, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
# 2. Update the device using LOWERCASE MAC in URL
|
||||
# And set devParentMAC to LOWERCASE
|
||||
update_payload = {
|
||||
"devParentMAC": test_parent_mac_input,
|
||||
"devName": "Updated Device"
|
||||
}
|
||||
# Using lowercase MAC in URL: aa:bb:cc:dd:ee:ff
|
||||
lowercase_mac = test_mac_norm.lower()
|
||||
|
||||
resp = client.post(f"/device/{lowercase_mac}", json=update_payload, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
# 3. Verify in DB that devParentMAC is NORMALIZED
|
||||
device_handler = DeviceInstance()
|
||||
device = device_handler.getDeviceData(test_mac_norm)
|
||||
|
||||
assert device is not None
|
||||
assert device["devName"] == "Updated Device"
|
||||
# This is the critical check:
|
||||
assert device["devParentMAC"] == test_parent_mac_norm
|
||||
assert device["devParentMAC"] != test_parent_mac_input # Should verify it changed from input if input was different case
|
||||
|
||||
# Cleanup
|
||||
device_handler.deleteDeviceByMAC(test_mac_norm)
|
||||
112
test/server/test_api_server_start.py
Normal file
112
test/server/test_api_server_start.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from server.api_server import api_server_start as api_mod
|
||||
|
||||
|
||||
def _make_fake_thread(recorder):
|
||||
class FakeThread:
|
||||
def __init__(self, target=None):
|
||||
self._target = target
|
||||
|
||||
def start(self):
|
||||
# call target synchronously for test
|
||||
if self._target:
|
||||
self._target()
|
||||
|
||||
return FakeThread
|
||||
|
||||
|
||||
def test_start_server_passes_debug_true(monkeypatch):
|
||||
# Arrange
|
||||
# Use the settings helper to provide the value
|
||||
monkeypatch.setattr(api_mod, 'get_setting_value', lambda k: True if k == 'FLASK_DEBUG' else None)
|
||||
|
||||
called = {}
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
called['args'] = args
|
||||
called['kwargs'] = kwargs
|
||||
|
||||
monkeypatch.setattr(api_mod, 'app', api_mod.app)
|
||||
monkeypatch.setattr(api_mod.app, 'run', fake_run)
|
||||
|
||||
# Replace threading.Thread with a fake that executes target immediately
|
||||
FakeThread = _make_fake_thread(called)
|
||||
monkeypatch.setattr(api_mod.threading, 'Thread', FakeThread)
|
||||
|
||||
# Prevent updateState side effects
|
||||
monkeypatch.setattr(api_mod, 'updateState', lambda *a, **k: None)
|
||||
|
||||
app_state = SimpleNamespace(graphQLServerStarted=0)
|
||||
|
||||
# Act
|
||||
api_mod.start_server(12345, app_state)
|
||||
|
||||
# Assert
|
||||
assert 'kwargs' in called
|
||||
assert called['kwargs']['debug'] is True
|
||||
assert called['kwargs']['host'] == '0.0.0.0'
|
||||
assert called['kwargs']['port'] == 12345
|
||||
|
||||
|
||||
def test_start_server_passes_debug_false(monkeypatch):
|
||||
# Arrange
|
||||
monkeypatch.setattr(api_mod, 'get_setting_value', lambda k: False if k == 'FLASK_DEBUG' else None)
|
||||
|
||||
called = {}
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
called['args'] = args
|
||||
called['kwargs'] = kwargs
|
||||
|
||||
monkeypatch.setattr(api_mod, 'app', api_mod.app)
|
||||
monkeypatch.setattr(api_mod.app, 'run', fake_run)
|
||||
|
||||
FakeThread = _make_fake_thread(called)
|
||||
monkeypatch.setattr(api_mod.threading, 'Thread', FakeThread)
|
||||
|
||||
monkeypatch.setattr(api_mod, 'updateState', lambda *a, **k: None)
|
||||
|
||||
app_state = SimpleNamespace(graphQLServerStarted=0)
|
||||
|
||||
# Act
|
||||
api_mod.start_server(22222, app_state)
|
||||
|
||||
# Assert
|
||||
assert 'kwargs' in called
|
||||
assert called['kwargs']['debug'] is False
|
||||
assert called['kwargs']['host'] == '0.0.0.0'
|
||||
assert called['kwargs']['port'] == 22222
|
||||
|
||||
|
||||
def test_env_var_overrides_setting(monkeypatch):
|
||||
# Arrange
|
||||
# Ensure env override is present
|
||||
monkeypatch.setenv('FLASK_DEBUG', '1')
|
||||
# And the stored setting is False to ensure env takes precedence
|
||||
monkeypatch.setattr(api_mod, 'get_setting_value', lambda k: False if k == 'FLASK_DEBUG' else None)
|
||||
|
||||
called = {}
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
called['args'] = args
|
||||
called['kwargs'] = kwargs
|
||||
|
||||
monkeypatch.setattr(api_mod, 'app', api_mod.app)
|
||||
monkeypatch.setattr(api_mod.app, 'run', fake_run)
|
||||
|
||||
FakeThread = _make_fake_thread(called)
|
||||
monkeypatch.setattr(api_mod.threading, 'Thread', FakeThread)
|
||||
|
||||
monkeypatch.setattr(api_mod, 'updateState', lambda *a, **k: None)
|
||||
|
||||
app_state = SimpleNamespace(graphQLServerStarted=0)
|
||||
|
||||
# Act
|
||||
api_mod.start_server(33333, app_state)
|
||||
|
||||
# Assert
|
||||
assert 'kwargs' in called
|
||||
assert called['kwargs']['debug'] is True
|
||||
assert called['kwargs']['host'] == '0.0.0.0'
|
||||
assert called['kwargs']['port'] == 33333
|
||||
@@ -9,10 +9,8 @@ from server.api_server import mcp_endpoint
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_registry():
|
||||
registry.clear_registry()
|
||||
registry._disabled_tools.clear()
|
||||
yield
|
||||
registry.clear_registry()
|
||||
registry._disabled_tools.clear()
|
||||
|
||||
|
||||
def test_disable_tool_management():
|
||||
|
||||
@@ -5,15 +5,8 @@ Runs all page-specific UI tests and provides summary
|
||||
"""
|
||||
|
||||
import sys
|
||||
# Import all test modules
|
||||
from .test_helpers import test_ui_dashboard
|
||||
from .test_helpers import test_ui_devices
|
||||
from .test_helpers import test_ui_network
|
||||
from .test_helpers import test_ui_maintenance
|
||||
from .test_helpers import test_ui_multi_edit
|
||||
from .test_helpers import test_ui_notifications
|
||||
from .test_helpers import test_ui_settings
|
||||
from .test_helpers import test_ui_plugins
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
def main():
|
||||
@@ -22,22 +15,28 @@ def main():
|
||||
print("NetAlertX UI Test Suite")
|
||||
print("=" * 70)
|
||||
|
||||
# Get directory of this script
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
test_modules = [
|
||||
("Dashboard", test_ui_dashboard),
|
||||
("Devices", test_ui_devices),
|
||||
("Network", test_ui_network),
|
||||
("Maintenance", test_ui_maintenance),
|
||||
("Multi-Edit", test_ui_multi_edit),
|
||||
("Notifications", test_ui_notifications),
|
||||
("Settings", test_ui_settings),
|
||||
("Plugins", test_ui_plugins),
|
||||
("Dashboard", "test_ui_dashboard.py"),
|
||||
("Devices", "test_ui_devices.py"),
|
||||
("Network", "test_ui_network.py"),
|
||||
("Maintenance", "test_ui_maintenance.py"),
|
||||
("Multi-Edit", "test_ui_multi_edit.py"),
|
||||
("Notifications", "test_ui_notifications.py"),
|
||||
("Settings", "test_ui_settings.py"),
|
||||
("Plugins", "test_ui_plugins.py"),
|
||||
]
|
||||
|
||||
results = {}
|
||||
|
||||
for name, module in test_modules:
|
||||
for name, filename in test_modules:
|
||||
try:
|
||||
result = module.run_tests()
|
||||
print(f"\nRunning {name} tests...")
|
||||
file_path = os.path.join(base_dir, filename)
|
||||
# Run pytest
|
||||
result = pytest.main([file_path, "-v"])
|
||||
results[name] = result == 0
|
||||
except Exception as e:
|
||||
print(f"\n✗ {name} tests failed with exception: {e}")
|
||||
|
||||
@@ -82,13 +82,21 @@ def test_add_device_with_generated_mac_ip(driver, api_token):
|
||||
wait_for_page_load(driver, timeout=10)
|
||||
|
||||
# --- Click "Add Device" ---
|
||||
add_buttons = driver.find_elements(By.CSS_SELECTOR, "button#btnAddDevice, button[onclick*='addDevice'], a[href*='deviceDetails.php?mac='], .btn-add-device")
|
||||
if not add_buttons:
|
||||
# Wait for the "New Device" link specifically to ensure it's loaded
|
||||
add_selector = "a[href*='deviceDetails.php?mac=new'], button#btnAddDevice, .btn-add-device"
|
||||
try:
|
||||
add_button = wait_for_element_by_css(driver, add_selector, timeout=10)
|
||||
except Exception:
|
||||
# Fallback to broader search if specific selector fails
|
||||
add_buttons = driver.find_elements(By.XPATH, "//button[contains(text(),'Add') or contains(text(),'New')] | //a[contains(text(),'Add') or contains(text(),'New')]")
|
||||
if not add_buttons:
|
||||
assert True, "Add device button not found, skipping test"
|
||||
return
|
||||
add_buttons[0].click()
|
||||
if add_buttons:
|
||||
add_button = add_buttons[0]
|
||||
else:
|
||||
assert True, "Add device button not found, skipping test"
|
||||
return
|
||||
|
||||
# Use JavaScript click to bypass any transparent overlays from the chart
|
||||
driver.execute_script("arguments[0].click();", add_button)
|
||||
|
||||
# Wait for the device form to appear (use the NEWDEV_devMac field as indicator)
|
||||
wait_for_element_by_css(driver, "#NEWDEV_devMac", timeout=10)
|
||||
|
||||
@@ -6,6 +6,7 @@ Tests CSV export/import, delete operations, database tools
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from .test_helpers import BASE_URL, api_get, wait_for_page_load # noqa: E402
|
||||
|
||||
@@ -30,7 +31,10 @@ def test_export_csv_button_works(driver):
|
||||
import os
|
||||
import glob
|
||||
|
||||
driver.get(f"{BASE_URL}/maintenance.php")
|
||||
# Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues in the browser
|
||||
# which can lead to "Failed to fetch" if the server is only listening on IPv4.
|
||||
target_url = f"{BASE_URL}/maintenance.php".replace("localhost", "127.0.0.1")
|
||||
driver.get(target_url)
|
||||
wait_for_page_load(driver, timeout=10)
|
||||
|
||||
# Clear any existing downloads
|
||||
@@ -38,13 +42,22 @@ def test_export_csv_button_works(driver):
|
||||
for f in glob.glob(f"{download_dir}/*.csv"):
|
||||
os.remove(f)
|
||||
|
||||
# Ensure the Backup/Restore tab is active so the button is in a clickable state
|
||||
try:
|
||||
tab = WebDriverWait(driver, 5).until(
|
||||
EC.element_to_be_clickable((By.ID, "tab_BackupRestore_id"))
|
||||
)
|
||||
tab.click()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Find the export button
|
||||
export_btns = driver.find_elements(By.ID, "btnExportCSV")
|
||||
try:
|
||||
export_btn = WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "btnExportCSV"))
|
||||
)
|
||||
|
||||
if len(export_btns) > 0:
|
||||
export_btn = export_btns[0]
|
||||
|
||||
# Click it (JavaScript click works even if CSS hides it)
|
||||
# Click it (JavaScript click works even if CSS hides it or if it's overlapped)
|
||||
driver.execute_script("arguments[0].click();", export_btn)
|
||||
|
||||
# Wait for download to complete (up to 10 seconds)
|
||||
@@ -70,9 +83,15 @@ def test_export_csv_button_works(driver):
|
||||
# Download via blob/JavaScript - can't verify file in headless mode
|
||||
# Just verify button click didn't cause errors
|
||||
assert "error" not in driver.page_source.lower(), "Button click should not cause errors"
|
||||
else:
|
||||
# Button doesn't exist on this page
|
||||
assert True, "Export button not found on this page"
|
||||
except Exception as e:
|
||||
# Check for alerts that might be blocking page_source access
|
||||
try:
|
||||
alert = driver.switch_to.alert
|
||||
alert_text = alert.text
|
||||
alert.accept()
|
||||
assert False, f"Alert present: {alert_text}"
|
||||
except Exception:
|
||||
raise e
|
||||
|
||||
|
||||
def test_import_section_present(driver):
|
||||
|
||||
Reference in New Issue
Block a user