mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
@@ -1 +1 @@
|
||||
""" tests for NetAlertX """
|
||||
""" tests for NetAlertX """
|
||||
|
||||
@@ -7,9 +7,9 @@ import pytest
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from utils.datetime_utils import timeNowDB
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -26,7 +26,7 @@ def client():
|
||||
@pytest.fixture(scope="session")
|
||||
def test_mac():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
# import pathlib
|
||||
# import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
# import string
|
||||
# import uuid
|
||||
import os
|
||||
import pytest
|
||||
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -28,7 +28,7 @@ def client():
|
||||
@pytest.fixture
|
||||
def test_mac():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
@@ -38,7 +38,6 @@ def auth_headers(token):
|
||||
def test_create_device(client, api_token, test_mac):
|
||||
payload = {
|
||||
"createNew": True,
|
||||
"devType": "Test Device",
|
||||
"devOwner": "Unit Test",
|
||||
"devType": "Router",
|
||||
"devVendor": "TestVendor",
|
||||
@@ -103,7 +102,7 @@ def test_copy_device(client, api_token, test_mac):
|
||||
|
||||
# Step 2: Generate a target MAC
|
||||
target_mac = "AA:BB:CC:" + ":".join(
|
||||
f"{random.randint(0,255):02X}" for _ in range(3)
|
||||
f"{random.randint(0, 255):02X}" for _ in range(3)
|
||||
)
|
||||
|
||||
# Step 3: Copy device
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
# import pathlib
|
||||
# import sqlite3
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
# import string
|
||||
# import uuid
|
||||
import os
|
||||
import pytest
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
@@ -40,12 +44,13 @@ def create_dummy(client, api_token, test_mac):
|
||||
"devType": "Router",
|
||||
"devVendor": "TestVendor",
|
||||
}
|
||||
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def test_get_all_devices(client, api_token, test_mac):
|
||||
# Ensure there is at least one device
|
||||
create_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
# Fetch all devices
|
||||
resp = client.get("/devices", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
@@ -59,7 +64,7 @@ def test_get_all_devices(client, api_token, test_mac):
|
||||
def test_delete_devices_with_macs(client, api_token, test_mac):
|
||||
# First create device so it exists
|
||||
create_dummy(client, api_token, test_mac)
|
||||
|
||||
|
||||
client.post(f"/device/{test_mac}", json={"createNew": True}, headers=auth_headers(api_token))
|
||||
|
||||
# Delete by MAC
|
||||
@@ -67,6 +72,7 @@ def test_delete_devices_with_macs(client, api_token, test_mac):
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_delete_all_empty_macs(client, api_token):
|
||||
resp = client.delete("/devices/empty-macs", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
@@ -79,6 +85,7 @@ def test_delete_unknown_devices(client, api_token):
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_export_devices_csv(client, api_token, test_mac):
|
||||
# Create a device first
|
||||
create_dummy(client, api_token, test_mac)
|
||||
@@ -92,6 +99,7 @@ def test_export_devices_csv(client, api_token, test_mac):
|
||||
# CSV should contain test_mac
|
||||
assert test_mac in resp.data.decode()
|
||||
|
||||
|
||||
def test_export_devices_json(client, api_token, test_mac):
|
||||
# Create a device first
|
||||
create_dummy(client, api_token, test_mac)
|
||||
@@ -101,7 +109,7 @@ def test_export_devices_json(client, api_token, test_mac):
|
||||
assert resp.status_code == 200
|
||||
assert resp.is_json
|
||||
data = resp.get_json()
|
||||
assert any(dev.get("devMac") == test_mac for dev in data["data"])
|
||||
assert any(dev.get("devMac") == test_mac for dev in data["data"])
|
||||
|
||||
|
||||
def test_export_devices_invalid_format(client, api_token):
|
||||
@@ -143,6 +151,7 @@ def test_export_import_cycle_base64(client, api_token, test_mac):
|
||||
assert resp.json.get("inserted") >= 1
|
||||
assert resp.json.get("skipped_lines") == []
|
||||
|
||||
|
||||
def test_devices_totals(client, api_token, test_mac):
|
||||
# 1. Create a dummy device
|
||||
create_dummy(client, api_token, test_mac)
|
||||
@@ -189,9 +198,10 @@ def test_devices_by_status(client, api_token, test_mac):
|
||||
assert fav_data is not None
|
||||
assert "★" in fav_data["title"]
|
||||
|
||||
|
||||
def test_delete_test_devices(client, api_token, test_mac):
|
||||
|
||||
# Delete by MAC
|
||||
resp = client.delete("/devices", json={"macs": ["AA:BB:CC:*"]}, headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from utils.datetime_utils import timeNowTZ
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowTZ # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def create_event(client, api_token, mac, event="UnitTest Event", days_old=None):
|
||||
payload = {"ip": "0.0.0.0", "event_type": event}
|
||||
|
||||
@@ -43,10 +44,12 @@ def create_event(client, api_token, mac, event="UnitTest Event", days_old=None):
|
||||
|
||||
return client.post(f"/events/create/{mac}", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def list_events(client, api_token, mac=None):
|
||||
url = "/events" if mac is None else f"/events?mac={mac}"
|
||||
return client.get(url, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def test_create_event(client, api_token, test_mac):
|
||||
# create event
|
||||
resp = create_event(client, api_token, test_mac)
|
||||
@@ -82,6 +85,7 @@ def test_delete_events_for_mac(client, api_token, test_mac):
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json.get("events", [])) == 0
|
||||
|
||||
|
||||
def test_get_events_totals(client, api_token):
|
||||
# 1. Request totals with default period
|
||||
resp = client.get(
|
||||
@@ -108,7 +112,6 @@ def test_get_events_totals(client, api_token):
|
||||
assert len(data_month) == 6
|
||||
|
||||
|
||||
|
||||
def test_delete_all_events(client, api_token, test_mac):
|
||||
# create two events
|
||||
create_event(client, api_token, test_mac)
|
||||
@@ -146,5 +149,3 @@ def test_delete_events_dynamic_days(client, api_token, test_mac):
|
||||
events = resp.get_json().get("events", [])
|
||||
mac_events = [ev for ev in events if ev.get("eve_MAC") == test_mac]
|
||||
assert len(mac_events) == 1
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
@@ -37,6 +36,7 @@ def test_graphql_debug_get(client):
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.decode() == "NetAlertX GraphQL server running."
|
||||
|
||||
|
||||
def test_graphql_post_unauthorized(client):
|
||||
"""POST /graphql without token should return 401"""
|
||||
query = {"query": "{ devices { devName devMac } }"}
|
||||
@@ -47,13 +47,14 @@ def test_graphql_post_unauthorized(client):
|
||||
|
||||
# --- DEVICES TESTS ---
|
||||
|
||||
|
||||
def test_graphql_post_devices(client, api_token):
|
||||
"""POST /graphql with a valid token should return device data"""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
devices {
|
||||
devices {
|
||||
devices {
|
||||
devGUID
|
||||
devGroup
|
||||
devIsRandomMac
|
||||
@@ -77,8 +78,8 @@ def test_graphql_post_devices(client, api_token):
|
||||
assert isinstance(data["devices"]["devices"], list)
|
||||
assert isinstance(data["devices"]["count"], int)
|
||||
|
||||
# --- SETTINGS TESTS ---
|
||||
|
||||
# --- SETTINGS TESTS ---
|
||||
def test_graphql_post_settings(client, api_token):
|
||||
"""POST /graphql should return settings data"""
|
||||
query = {
|
||||
@@ -97,8 +98,8 @@ def test_graphql_post_settings(client, api_token):
|
||||
assert "settings" in data
|
||||
assert isinstance(data["settings"]["settings"], list)
|
||||
|
||||
# --- LANGSTRINGS TESTS ---
|
||||
|
||||
# --- LANGSTRINGS TESTS ---
|
||||
def test_graphql_post_langstrings_specific(client, api_token):
|
||||
"""Retrieve a specific langString in a given language"""
|
||||
query = {
|
||||
@@ -167,4 +168,4 @@ def test_graphql_post_langstrings_all_languages(client, api_token):
|
||||
assert data["enStrings"]["count"] >= 1
|
||||
assert data["deStrings"]["count"] >= 1
|
||||
# Ensure langCode matches
|
||||
assert all(e["langCode"] == "en_us" for e in data["enStrings"]["langStrings"])
|
||||
assert all(e["langCode"] == "en_us" for e in data["enStrings"]["langStrings"])
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -28,7 +24,7 @@ def client():
|
||||
@pytest.fixture
|
||||
def test_mac():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
@@ -36,6 +32,6 @@ def auth_headers(token):
|
||||
|
||||
|
||||
def test_delete_history(client, api_token):
|
||||
resp = client.delete(f"/history", headers=auth_headers(api_token))
|
||||
resp = client.delete("/history", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
@@ -5,8 +5,9 @@ import pytest
|
||||
INSTALL_PATH = "/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Fixtures
|
||||
@@ -15,14 +16,17 @@ from api_server.api_server_start import app
|
||||
def api_token():
|
||||
return get_setting_value("API_TOKEN")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Logs Endpoint Tests
|
||||
# ----------------------------
|
||||
@@ -31,16 +35,18 @@ def test_clean_log(client, api_token):
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_clean_log_not_allowed(client, api_token):
|
||||
resp = client.delete("/logs?file=not_allowed.log", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 400
|
||||
assert resp.json.get("success") is False
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Execution Queue Endpoint Tests
|
||||
# ----------------------------
|
||||
def test_add_to_execution_queue(client, api_token):
|
||||
action_name = f"test_action_{random.randint(0,9999)}"
|
||||
action_name = f"test_action_{random.randint(0, 9999)}"
|
||||
resp = client.post(
|
||||
"/logs/add-to-execution-queue",
|
||||
json={"action": action_name},
|
||||
@@ -50,6 +56,7 @@ def test_add_to_execution_queue(client, api_token):
|
||||
assert resp.json.get("success") is True
|
||||
assert action_name in resp.json.get("message", "")
|
||||
|
||||
|
||||
def test_add_to_execution_queue_missing_action(client, api_token):
|
||||
resp = client.post(
|
||||
"/logs/add-to-execution-queue",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
# -----------------------------
|
||||
# In-app notifications tests with cleanup
|
||||
# -----------------------------
|
||||
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
@@ -14,26 +11,31 @@ import sys
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from api_server.api_server_start import app
|
||||
from messaging.in_app import NOTIFICATION_API_FILE # Import the path to notifications file
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
from messaging.in_app import NOTIFICATION_API_FILE # noqa: E402 [flake8 lint suppression]
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_content():
|
||||
return "Test Notification " + "".join(random.choices(string.ascii_letters + string.digits, k=6))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def notification_guid(client, api_token, random_content):
|
||||
# Write a notification and return its GUID
|
||||
@@ -50,6 +52,7 @@ def notification_guid(client, api_token, random_content):
|
||||
assert guid is not None
|
||||
return guid
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_notifications():
|
||||
# Runs before and after each test
|
||||
@@ -70,6 +73,7 @@ def cleanup_notifications():
|
||||
with open(NOTIFICATION_API_FILE, "w") as f:
|
||||
f.write(backup)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
def test_write_notification(client, api_token, random_content):
|
||||
resp = client.post(
|
||||
@@ -80,6 +84,7 @@ def test_write_notification(client, api_token, random_content):
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_get_unread_notifications(client, api_token, random_content):
|
||||
client.post("/messaging/in-app/write", json={"content": random_content}, headers=auth_headers(api_token))
|
||||
resp = client.get("/messaging/in-app/unread", headers=auth_headers(api_token))
|
||||
@@ -87,22 +92,26 @@ def test_get_unread_notifications(client, api_token, random_content):
|
||||
notifications = resp.json
|
||||
assert any(n["content"] == random_content for n in notifications)
|
||||
|
||||
|
||||
def test_mark_all_notifications_read(client, api_token, random_content):
|
||||
client.post("/messaging/in-app/write", json={"content": random_content}, headers=auth_headers(api_token))
|
||||
resp = client.post("/messaging/in-app/read/all", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_mark_single_notification_read(client, api_token, notification_guid):
|
||||
resp = client.post(f"/messaging/in-app/read/{notification_guid}", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_delete_single_notification(client, api_token, notification_guid):
|
||||
resp = client.delete(f"/messaging/in-app/delete/{notification_guid}", headers=auth_headers(api_token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json.get("success") is True
|
||||
|
||||
|
||||
def test_delete_all_notifications(client, api_token, random_content):
|
||||
# Add a notification first
|
||||
client.post("/messaging/in-app/write", json={"content": random_content}, headers=auth_headers(api_token))
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
@@ -40,7 +39,8 @@ def create_dummy(client, api_token, test_mac):
|
||||
"devType": "Router",
|
||||
"devVendor": "TestVendor",
|
||||
}
|
||||
resp = client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
client.post(f"/device/{test_mac}", json=payload, headers=auth_headers(api_token))
|
||||
|
||||
|
||||
def test_wakeonlan_device(client, api_token, test_mac):
|
||||
# 1. Ensure at least one device exists
|
||||
@@ -73,6 +73,7 @@ def test_wakeonlan_device(client, api_token, test_mac):
|
||||
assert data.get("success") is True
|
||||
assert "WOL packet sent" in data.get("message", "")
|
||||
|
||||
|
||||
def test_speedtest_endpoint(client, api_token):
|
||||
# 1. Call the speedtest endpoint
|
||||
resp = client.get("/nettools/speedtest", headers=auth_headers(api_token))
|
||||
@@ -92,7 +93,8 @@ def test_speedtest_endpoint(client, api_token):
|
||||
assert isinstance(data["output"], list)
|
||||
# Optionally check that output lines are strings
|
||||
assert all(isinstance(line, str) for line in data["output"])
|
||||
|
||||
|
||||
|
||||
def test_traceroute_device(client, api_token, test_mac):
|
||||
# 1. Ensure at least one device exists
|
||||
create_dummy(client, api_token, test_mac)
|
||||
@@ -127,6 +129,7 @@ def test_traceroute_device(client, api_token, test_mac):
|
||||
assert "output" in data
|
||||
assert isinstance(data["output"], str)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ip,expected_status", [
|
||||
("8.8.8.8", 200),
|
||||
("256.256.256.256", 400), # Invalid IP
|
||||
@@ -147,6 +150,7 @@ def test_nslookup_endpoint(client, api_token, ip, expected_status):
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ip,mode,expected_status", [
|
||||
("127.0.0.1", "fast", 200),
|
||||
pytest.param("127.0.0.1", "normal", 200, marks=pytest.mark.feature_complete),
|
||||
@@ -172,6 +176,7 @@ def test_nmap_endpoint(client, api_token, ip, mode, expected_status):
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
|
||||
|
||||
def test_nslookup_unauthorized(client):
|
||||
# No auth headers
|
||||
resp = client.post("/nettools/nslookup", json={"devLastIP": "8.8.8.8"})
|
||||
@@ -180,6 +185,7 @@ def test_nslookup_unauthorized(client):
|
||||
assert data.get("success") is False
|
||||
assert data.get("error") == "Forbidden"
|
||||
|
||||
|
||||
def test_nmap_unauthorized(client):
|
||||
# No auth headers
|
||||
resp = client.post("/nettools/nmap", json={"scan": "127.0.0.1", "mode": "fast"})
|
||||
@@ -201,4 +207,4 @@ def test_internet_info_endpoint(client, api_token):
|
||||
# Handle errors, e.g., curl failure
|
||||
assert data.get("success") is False
|
||||
assert "error" in data
|
||||
assert "details" in data
|
||||
assert "details" in data
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
@@ -11,31 +7,35 @@ from datetime import datetime, timedelta
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from utils.datetime_utils import timeNowTZ, timeNowDB
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from utils.datetime_utils import timeNowTZ, timeNowDB # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_create_device(client, api_token, test_mac):
|
||||
payload = {
|
||||
"createNew": True,
|
||||
"devType": "Test Device",
|
||||
"devOwner": "Unit Test",
|
||||
"devType": "Router",
|
||||
"devVendor": "TestVendor",
|
||||
@@ -129,7 +129,7 @@ def test_device_session_events(client, api_token, test_mac):
|
||||
|
||||
# 2. Fetch session events with default type ('all') and period ('7 days')
|
||||
resp = client.get(
|
||||
f"/sessions/session-events?type=all&period=7 days",
|
||||
"/sessions/session-events?type=all&period=7 days",
|
||||
headers=auth_headers(api_token)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -159,6 +159,7 @@ def test_device_session_events(client, api_token, test_mac):
|
||||
sessions = resp_sessions.json["data"]
|
||||
assert isinstance(sessions, list)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
def test_delete_session(client, api_token, test_mac):
|
||||
# First create session
|
||||
@@ -180,15 +181,12 @@ def test_delete_session(client, api_token, test_mac):
|
||||
assert not any(ses["ses_MAC"] == test_mac for ses in sessions)
|
||||
|
||||
|
||||
|
||||
def test_get_sessions_calendar(client, api_token, test_mac):
|
||||
"""
|
||||
Test the /sessions/calendar endpoint.
|
||||
Creates session and ensures the calendar output is correct.
|
||||
Cleans up test sessions after test.
|
||||
"""
|
||||
|
||||
|
||||
# --- Setup: create two sessions for the test MAC ---
|
||||
now = timeNowTZ()
|
||||
start1 = (now - timedelta(days=2)).isoformat(timespec="seconds")
|
||||
@@ -256,4 +254,4 @@ def test_get_sessions_calendar(client, api_token, test_mac):
|
||||
assert "<still connected>" in ses["tooltip"], f"End is None but session not marked as still connected: {ses}"
|
||||
|
||||
# --- Cleanup: delete all test sessions for this MAC ---
|
||||
client.delete(f"/sessions/delete?mac={test_mac}", headers=auth_headers(api_token))
|
||||
client.delete(f"/sessions/delete?mac={test_mac}", headers=auth_headers(api_token))
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_get_setting_unauthorized(client):
|
||||
resp = client.get("/settings/API_TOKEN") # no auth header
|
||||
assert resp.status_code == 403
|
||||
|
||||
@@ -6,16 +6,17 @@ Tests the fix for Issue #1210 - compound conditions with multiple AND/OR clauses
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock the logger module before importing SafeConditionBuilder
|
||||
sys.modules['logger'] = MagicMock()
|
||||
|
||||
# Add parent directory to path for imports
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from server.db.sql_safe_builder import SafeConditionBuilder
|
||||
from server.db.sql_safe_builder import SafeConditionBuilder # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -100,6 +101,7 @@ def test_multiple_or_clauses(builder):
|
||||
assert 'Device2' in param_values
|
||||
assert 'Device3' in param_values
|
||||
|
||||
|
||||
def test_mixed_and_or_clauses(builder):
|
||||
"""Test mixed AND/OR logical operators."""
|
||||
condition = "AND devName = 'Device1' OR devName = 'Device2' AND devFavorite = '1'"
|
||||
|
||||
@@ -137,7 +137,7 @@ def test_unicode_support(builder, unicode_str):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", [
|
||||
"", " ", "AND devName = ''", "AND devName = 'a'", "AND devName = '" + "x"*500 + "'"
|
||||
"", " ", "AND devName = ''", "AND devName = 'a'", "AND devName = '" + "x" * 500 + "'"
|
||||
])
|
||||
def test_edge_cases(builder, case):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# !/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive SQL Injection Prevention Tests for NetAlertX
|
||||
|
||||
@@ -15,7 +15,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'server'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'server', 'db'))
|
||||
|
||||
# Now import our module
|
||||
from sql_safe_builder import SafeConditionBuilder
|
||||
from sql_safe_builder import SafeConditionBuilder # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -28,7 +28,7 @@ def test_sql_injection_attempt_single_quote(builder):
|
||||
"""Test that single quote injection attempts are blocked."""
|
||||
malicious_input = "'; DROP TABLE users; --"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when invalid
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -38,7 +38,7 @@ def test_sql_injection_attempt_union(builder):
|
||||
"""Test that UNION injection attempts are blocked."""
|
||||
malicious_input = "1' UNION SELECT * FROM passwords --"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when invalid
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -48,7 +48,7 @@ def test_sql_injection_attempt_or_true(builder):
|
||||
"""Test that OR 1=1 injection attempts are blocked."""
|
||||
malicious_input = "' OR '1'='1"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when invalid
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -58,7 +58,7 @@ def test_valid_simple_condition(builder):
|
||||
"""Test that valid simple conditions are handled correctly."""
|
||||
valid_input = "AND devName = 'Test Device'"
|
||||
condition, params = builder.get_safe_condition_legacy(valid_input)
|
||||
|
||||
|
||||
# Should create parameterized query
|
||||
assert "AND devName = :" in condition
|
||||
assert len(params) == 1
|
||||
@@ -69,7 +69,7 @@ def test_empty_condition(builder):
|
||||
"""Test that empty conditions are handled safely."""
|
||||
empty_input = ""
|
||||
condition, params = builder.get_safe_condition_legacy(empty_input)
|
||||
|
||||
|
||||
# Should return empty condition
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -79,7 +79,7 @@ def test_whitespace_only_condition(builder):
|
||||
"""Test that whitespace-only conditions are handled safely."""
|
||||
whitespace_input = " \n\t "
|
||||
condition, params = builder.get_safe_condition_legacy(whitespace_input)
|
||||
|
||||
|
||||
# Should return empty condition
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -90,7 +90,7 @@ def test_multiple_conditions_valid(builder):
|
||||
# Test with a single condition first (our current parser handles single conditions well)
|
||||
valid_input = "AND devName = 'Device1'"
|
||||
condition, params = builder.get_safe_condition_legacy(valid_input)
|
||||
|
||||
|
||||
# Should create parameterized query
|
||||
assert "devName = :" in condition
|
||||
assert len(params) == 1
|
||||
@@ -101,7 +101,7 @@ def test_disallowed_column_name(builder):
|
||||
"""Test that non-whitelisted column names are rejected."""
|
||||
invalid_input = "AND malicious_column = 'value'"
|
||||
condition, params = builder.get_safe_condition_legacy(invalid_input)
|
||||
|
||||
|
||||
# Should return empty condition when column not in whitelist
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -111,7 +111,7 @@ def test_disallowed_operator(builder):
|
||||
"""Test that non-whitelisted operators are rejected."""
|
||||
invalid_input = "AND devName SOUNDS LIKE 'test'"
|
||||
condition, params = builder.get_safe_condition_legacy(invalid_input)
|
||||
|
||||
|
||||
# Should return empty condition when operator not allowed
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -121,7 +121,7 @@ def test_nested_select_attempt(builder):
|
||||
"""Test that nested SELECT attempts are blocked."""
|
||||
malicious_input = "AND devName IN (SELECT password FROM users)"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when nested SELECT detected
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -131,7 +131,7 @@ def test_hex_encoding_attempt(builder):
|
||||
"""Test that hex-encoded injection attempts are blocked."""
|
||||
malicious_input = "AND 0x44524f50205441424c45"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when hex encoding detected
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -141,7 +141,7 @@ def test_comment_injection_attempt(builder):
|
||||
"""Test that comment injection attempts are handled."""
|
||||
malicious_input = "AND devName = 'test' /* comment */ --"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Comments should be stripped and condition validated
|
||||
if condition:
|
||||
assert "/*" not in condition
|
||||
@@ -152,7 +152,7 @@ def test_special_placeholder_replacement(builder):
|
||||
"""Test that {s-quote} placeholder is safely replaced."""
|
||||
input_with_placeholder = "AND devName = {s-quote}Test{s-quote}"
|
||||
condition, params = builder.get_safe_condition_legacy(input_with_placeholder)
|
||||
|
||||
|
||||
# Should handle placeholder safely
|
||||
if condition:
|
||||
assert "{s-quote}" not in condition
|
||||
@@ -163,7 +163,7 @@ def test_null_byte_injection(builder):
|
||||
"""Test that null byte injection attempts are blocked."""
|
||||
malicious_input = "AND devName = 'test\x00' DROP TABLE --"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Null bytes should be sanitized
|
||||
if condition:
|
||||
assert "\x00" not in condition
|
||||
@@ -178,7 +178,7 @@ def test_build_condition_with_allowed_values(builder):
|
||||
{"column": "devName", "operator": "LIKE", "value": "%test%"}
|
||||
]
|
||||
condition, params = builder.build_condition(conditions, "AND")
|
||||
|
||||
|
||||
# Should create valid parameterized condition
|
||||
assert "eve_EventType = :" in condition
|
||||
assert "devName LIKE :" in condition
|
||||
@@ -191,7 +191,7 @@ def test_build_condition_with_invalid_column(builder):
|
||||
{"column": "invalid_column", "operator": "=", "value": "test"}
|
||||
]
|
||||
condition, params = builder.build_condition(conditions)
|
||||
|
||||
|
||||
# Should return empty when invalid column
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -204,7 +204,7 @@ def test_case_variations_injection(builder):
|
||||
"oR 1=1",
|
||||
"UnIoN SeLeCt * FrOm users"
|
||||
]
|
||||
|
||||
|
||||
for malicious_input in malicious_inputs:
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
# Should handle case variations safely
|
||||
@@ -217,7 +217,7 @@ def test_time_based_injection_attempt(builder):
|
||||
"""Test that time-based injection attempts are blocked."""
|
||||
malicious_input = "AND IF(1=1, SLEEP(5), 0)"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when SQL functions detected
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
@@ -227,7 +227,7 @@ def test_stacked_queries_attempt(builder):
|
||||
"""Test that stacked query attempts are blocked."""
|
||||
malicious_input = "'; INSERT INTO admin VALUES ('hacker', 'password'); --"
|
||||
condition, params = builder.get_safe_condition_legacy(malicious_input)
|
||||
|
||||
|
||||
# Should return empty condition when semicolon detected
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
|
||||
@@ -13,16 +13,15 @@ import unittest
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import os
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
# Add the server directory to the path for imports
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
sys.path.append('/home/dell/coding/bash/10x-agentic-setup/netalertx-sql-fix/server')
|
||||
|
||||
from db.sql_safe_builder import SafeConditionBuilder, create_safe_condition_builder
|
||||
from database import DB
|
||||
from messaging.reporting import get_notifications
|
||||
from db.sql_safe_builder import SafeConditionBuilder # noqa: E402 [flake8 lint suppression]
|
||||
from messaging.reporting import get_notifications # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
class TestSafeConditionBuilder(unittest.TestCase):
|
||||
@@ -83,7 +82,7 @@ class TestSafeConditionBuilder(unittest.TestCase):
|
||||
def test_build_simple_condition_valid(self):
|
||||
"""Test building valid simple conditions."""
|
||||
sql, params = self.builder._build_simple_condition('AND', 'devName', '=', 'TestDevice')
|
||||
|
||||
|
||||
self.assertIn('AND devName = :param_', sql)
|
||||
self.assertEqual(len(params), 1)
|
||||
self.assertIn('TestDevice', params.values())
|
||||
@@ -92,20 +91,20 @@ class TestSafeConditionBuilder(unittest.TestCase):
|
||||
"""Test that invalid column names are rejected."""
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.builder._build_simple_condition('AND', 'invalid_column', '=', 'value')
|
||||
|
||||
|
||||
self.assertIn('Invalid column name', str(context.exception))
|
||||
|
||||
def test_build_simple_condition_invalid_operator(self):
|
||||
"""Test that invalid operators are rejected."""
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.builder._build_simple_condition('AND', 'devName', 'UNION', 'value')
|
||||
|
||||
|
||||
self.assertIn('Invalid operator', str(context.exception))
|
||||
|
||||
def test_build_in_condition_valid(self):
|
||||
"""Test building valid IN conditions."""
|
||||
sql, params = self.builder._build_in_condition('AND', 'eve_EventType', 'IN', "'Connected', 'Disconnected'")
|
||||
|
||||
|
||||
self.assertIn('AND eve_EventType IN', sql)
|
||||
self.assertEqual(len(params), 2)
|
||||
self.assertIn('Connected', params.values())
|
||||
@@ -114,7 +113,7 @@ class TestSafeConditionBuilder(unittest.TestCase):
|
||||
def test_build_null_condition(self):
|
||||
"""Test building NULL check conditions."""
|
||||
sql, params = self.builder._build_null_condition('AND', 'devComments', 'IS NULL')
|
||||
|
||||
|
||||
self.assertEqual(sql, 'AND devComments IS NULL')
|
||||
self.assertEqual(len(params), 0)
|
||||
|
||||
@@ -154,7 +153,7 @@ class TestSafeConditionBuilder(unittest.TestCase):
|
||||
def test_device_name_filter(self):
|
||||
"""Test the device name filter helper method."""
|
||||
sql, params = self.builder.build_device_name_filter("TestDevice")
|
||||
|
||||
|
||||
self.assertIn('AND devName = :device_name_', sql)
|
||||
self.assertIn('TestDevice', params.values())
|
||||
|
||||
@@ -162,14 +161,13 @@ class TestSafeConditionBuilder(unittest.TestCase):
|
||||
"""Test the event type filter helper method."""
|
||||
event_types = ['Connected', 'Disconnected']
|
||||
sql, params = self.builder.build_event_type_filter(event_types)
|
||||
|
||||
|
||||
self.assertIn('AND eve_EventType IN', sql)
|
||||
self.assertEqual(len(params), 2)
|
||||
self.assertIn('Connected', params.values())
|
||||
self.assertIn('Disconnected', params.values())
|
||||
|
||||
|
||||
|
||||
class TestDatabaseParameterSupport(unittest.TestCase):
|
||||
"""Test that database layer supports parameterized queries."""
|
||||
|
||||
@@ -177,7 +175,7 @@ class TestDatabaseParameterSupport(unittest.TestCase):
|
||||
"""Set up test database."""
|
||||
self.temp_db = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
|
||||
self.temp_db.close()
|
||||
|
||||
|
||||
# Create test database
|
||||
self.conn = sqlite3.connect(self.temp_db.name)
|
||||
self.conn.execute('''CREATE TABLE test_table (
|
||||
@@ -197,23 +195,23 @@ class TestDatabaseParameterSupport(unittest.TestCase):
|
||||
def test_parameterized_query_execution(self):
|
||||
"""Test that parameterized queries work correctly."""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
|
||||
# Test named parameters
|
||||
cursor.execute("SELECT * FROM test_table WHERE name = :name", {'name': 'test1'})
|
||||
results = cursor.fetchall()
|
||||
|
||||
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0][1], 'test1')
|
||||
|
||||
def test_parameterized_query_prevents_injection(self):
|
||||
"""Test that parameterized queries prevent SQL injection."""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
|
||||
# This should not cause SQL injection
|
||||
malicious_input = "'; DROP TABLE test_table; --"
|
||||
cursor.execute("SELECT * FROM test_table WHERE name = :name", {'name': malicious_input})
|
||||
results = cursor.fetchall()
|
||||
|
||||
# results = cursor.fetchall()
|
||||
|
||||
# The table should still exist and be queryable
|
||||
cursor.execute("SELECT COUNT(*) FROM test_table")
|
||||
count = cursor.fetchone()[0]
|
||||
@@ -228,7 +226,7 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
self.mock_db = Mock()
|
||||
self.mock_db.sql = Mock()
|
||||
self.mock_db.get_table_as_json = Mock()
|
||||
|
||||
|
||||
# Mock successful JSON response
|
||||
mock_json_obj = Mock()
|
||||
mock_json_obj.columnNames = ['MAC', 'Datetime', 'IP', 'Event Type', 'Device name', 'Comments']
|
||||
@@ -245,7 +243,7 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
}.get(key, '')
|
||||
|
||||
# Call the function
|
||||
result = get_notifications(self.mock_db)
|
||||
get_notifications(self.mock_db)
|
||||
|
||||
# Verify that get_table_as_json was called with parameters
|
||||
self.mock_db.get_table_as_json.assert_called()
|
||||
@@ -265,7 +263,6 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
# Ensure the parameter dict has the correct value (using actual param name)
|
||||
self.assertEqual(list(params.values())[0], "TestDevice")
|
||||
|
||||
|
||||
@patch('messaging.reporting.get_setting_value')
|
||||
def test_events_section_security(self, mock_get_setting):
|
||||
"""Test that events section uses safe SQL building."""
|
||||
@@ -276,7 +273,7 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
}.get(key, '')
|
||||
|
||||
# Call the function
|
||||
result = get_notifications(self.mock_db)
|
||||
get_notifications(self.mock_db)
|
||||
|
||||
# Verify that get_table_as_json was called with parameters
|
||||
self.mock_db.get_table_as_json.assert_called()
|
||||
@@ -291,7 +288,7 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
}.get(key, '')
|
||||
|
||||
# Call the function - should not raise an exception
|
||||
result = get_notifications(self.mock_db)
|
||||
get_notifications(self.mock_db)
|
||||
|
||||
# Should still call get_table_as_json (with safe fallback query)
|
||||
self.mock_db.get_table_as_json.assert_called()
|
||||
@@ -306,7 +303,7 @@ class TestReportingSecurityIntegration(unittest.TestCase):
|
||||
}.get(key, '')
|
||||
|
||||
# Call the function
|
||||
result = get_notifications(self.mock_db)
|
||||
get_notifications(self.mock_db)
|
||||
|
||||
# Should call get_table_as_json
|
||||
self.mock_db.get_table_as_json.assert_called()
|
||||
@@ -322,12 +319,12 @@ class TestSecurityBenchmarks(unittest.TestCase):
|
||||
def test_performance_simple_condition(self):
|
||||
"""Test performance of simple condition building."""
|
||||
import time
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
for _ in range(1000):
|
||||
sql, params = self.builder.build_safe_condition("AND devName = 'TestDevice'")
|
||||
end_time = time.time()
|
||||
|
||||
|
||||
execution_time = end_time - start_time
|
||||
self.assertLess(execution_time, 1.0, "Simple condition building should be fast")
|
||||
|
||||
@@ -339,7 +336,7 @@ class TestSecurityBenchmarks(unittest.TestCase):
|
||||
self.skipTest("psutil not available")
|
||||
return
|
||||
import os
|
||||
|
||||
|
||||
process = psutil.Process(os.getpid())
|
||||
initial_memory = process.memory_info().rss
|
||||
|
||||
@@ -350,7 +347,7 @@ class TestSecurityBenchmarks(unittest.TestCase):
|
||||
|
||||
final_memory = process.memory_info().rss
|
||||
memory_increase = final_memory - initial_memory
|
||||
|
||||
|
||||
# Memory increase should be reasonable (less than 10MB)
|
||||
self.assertLess(memory_increase, 10 * 1024 * 1024, "Memory usage should be reasonable")
|
||||
|
||||
@@ -376,4 +373,4 @@ class TestSecurityBenchmarks(unittest.TestCase):
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run the test suite
|
||||
unittest.main(verbosity=2)
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# !/usr/bin/env python3
|
||||
"""
|
||||
Pytest-based Mount Diagnostic Tests for NetAlertX
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def dummy_container(tmp_path):
|
||||
f.write(" network_mode: host\n")
|
||||
f.write(" userns_mode: host\n")
|
||||
f.write(" command: sh -c \"while true; do nc -l -p 20211 < /dev/null > /dev/null; done & while true; do nc -l -p 20212 < /dev/null > /dev/null; done & sleep 30\"\n")
|
||||
|
||||
|
||||
# Start the dummy container
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
@@ -47,12 +47,12 @@ def dummy_container(tmp_path):
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Failed to start dummy container: {result.stderr}")
|
||||
|
||||
|
||||
# Wait a bit for the container to start listening
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
yield "dummy"
|
||||
|
||||
|
||||
# Cleanup
|
||||
subprocess.run(["docker-compose", "-f", str(compose_file), "down"], capture_output=True)
|
||||
|
||||
@@ -139,10 +139,10 @@ def _run_container(
|
||||
# Copy the script content and run it
|
||||
script_path = pathlib.Path("install/production-filesystem/entrypoint.d/99-ports-available.sh")
|
||||
with script_path.open('r', encoding='utf-8') as f:
|
||||
script_content = f.read()
|
||||
script_cont = f.read()
|
||||
|
||||
# Use printf to avoid shell interpretation issues
|
||||
script = f"printf '%s\\n' '{script_content.replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}' > /tmp/ports-check.sh && chmod +x /tmp/ports-check.sh && sh /tmp/ports-check.sh"
|
||||
script = f"printf '%s\\n' '{script_cont.replace(chr(39), chr(39) + chr(92) + chr(39) + chr(39))}' > /tmp/ports-check.sh && chmod +x /tmp/ports-check.sh && sh /tmp/ports-check.sh" # noqa: E501 - inline script
|
||||
cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script])
|
||||
|
||||
print(f"\n--- DOCKER CMD ---\n{' '.join(cmd)}\n--- END CMD ---\n")
|
||||
@@ -157,8 +157,7 @@ def _run_container(
|
||||
|
||||
# Combine and clean stdout and stderr
|
||||
stdouterr = (
|
||||
re.sub(r'\x1b\[[0-9;]*m', '', result.stdout or '') +
|
||||
re.sub(r'\x1b\[[0-9;]*m', '', result.stderr or '')
|
||||
re.sub(r'\x1b\[[0-9;]*m', '', result.stdout or '') + re.sub(r'\x1b\[[0-9;]*m', '', result.stderr or '')
|
||||
)
|
||||
result.output = stdouterr
|
||||
print(f"\n--- CONTAINER stdout ---\n{result.stdout}")
|
||||
@@ -255,4 +254,4 @@ def test_ports_in_use_warning(dummy_container, tmp_path: pathlib.Path) -> None:
|
||||
|
||||
_assert_contains(result, "Port Warning: Application port 20211 is already in use")
|
||||
_assert_contains(result, "Port Warning: GraphQL API port 20212 is already in use")
|
||||
assert result.returncode == 0
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -9,12 +9,14 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server', 'db'))
|
||||
|
||||
from db.sql_safe_builder import SafeConditionBuilder, create_safe_condition_builder
|
||||
from messaging.reporting import get_notifications
|
||||
from db.sql_safe_builder import create_safe_condition_builder # noqa: E402 [flake8 lint suppression]
|
||||
from messaging.reporting import get_notifications # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
# -----------------------------
|
||||
# Fixtures
|
||||
# -----------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_db_path():
|
||||
path = tempfile.mktemp(suffix=".db")
|
||||
@@ -22,10 +24,12 @@ def test_db_path():
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def builder():
|
||||
return create_safe_condition_builder()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_db(test_db_path):
|
||||
conn = sqlite3.connect(test_db_path)
|
||||
@@ -96,6 +100,7 @@ def test_db(test_db_path):
|
||||
# Tests
|
||||
# -----------------------------
|
||||
|
||||
|
||||
def test_fresh_install_compatibility(builder):
|
||||
condition, params = builder.get_safe_condition_legacy("")
|
||||
assert condition == ""
|
||||
@@ -105,6 +110,7 @@ def test_fresh_install_compatibility(builder):
|
||||
assert "devName = :" in condition
|
||||
assert 'TestDevice' in params.values()
|
||||
|
||||
|
||||
def test_existing_db_compatibility():
|
||||
mock_db = Mock()
|
||||
mock_result = Mock()
|
||||
@@ -129,6 +135,7 @@ def test_existing_db_compatibility():
|
||||
assert 'events_meta' in result
|
||||
assert mock_db.get_table_as_json.called
|
||||
|
||||
|
||||
def test_notification_system_integration(builder):
|
||||
email_condition = "AND devName = 'EmailTestDevice'"
|
||||
condition, params = builder.get_safe_condition_legacy(email_condition)
|
||||
@@ -150,6 +157,7 @@ def test_notification_system_integration(builder):
|
||||
assert "eve_MAC = :" in condition
|
||||
assert 'aa:bb:cc:dd:ee:ff' in params.values()
|
||||
|
||||
|
||||
def test_settings_persistence(builder):
|
||||
test_settings = [
|
||||
"AND devName = 'Persistent Device'",
|
||||
@@ -163,6 +171,7 @@ def test_settings_persistence(builder):
|
||||
assert isinstance(condition, str)
|
||||
assert isinstance(params, dict)
|
||||
|
||||
|
||||
def test_device_operations(builder):
|
||||
device_conditions = [
|
||||
"AND devName = 'Updated Device'",
|
||||
@@ -175,6 +184,7 @@ def test_device_operations(builder):
|
||||
assert len(params) > 0 or safe_condition == ""
|
||||
assert "'" not in safe_condition
|
||||
|
||||
|
||||
def test_plugin_functionality(builder):
|
||||
plugin_conditions = [
|
||||
"AND Plugin = 'TestPlugin'",
|
||||
@@ -187,6 +197,7 @@ def test_plugin_functionality(builder):
|
||||
assert ":" in safe_condition
|
||||
assert len(params) > 0
|
||||
|
||||
|
||||
def test_sql_injection_prevention(builder):
|
||||
malicious_inputs = [
|
||||
"'; DROP TABLE Events_Devices; --",
|
||||
@@ -200,6 +211,7 @@ def test_sql_injection_prevention(builder):
|
||||
assert condition == ""
|
||||
assert params == {}
|
||||
|
||||
|
||||
def test_error_handling(builder):
|
||||
invalid_condition = "INVALID SQL SYNTAX HERE"
|
||||
condition, params = builder.get_safe_condition_legacy(invalid_condition)
|
||||
@@ -213,6 +225,7 @@ def test_error_handling(builder):
|
||||
assert isinstance(condition, str)
|
||||
assert isinstance(params, dict)
|
||||
|
||||
|
||||
def test_backward_compatibility(builder):
|
||||
legacy_conditions = [
|
||||
"AND devName = {s-quote}Legacy Device{s-quote}",
|
||||
@@ -226,6 +239,7 @@ def test_backward_compatibility(builder):
|
||||
assert ":" in condition
|
||||
assert len(params) > 0
|
||||
|
||||
|
||||
def test_performance_impact(builder):
|
||||
import time
|
||||
test_condition = "AND devName = 'Performance Test Device'"
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import sys
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import os
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value
|
||||
from api_server.api_server_start import app
|
||||
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
|
||||
from api_server.api_server_start import app # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
@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():
|
||||
# Generate a unique MAC for each test run
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0,255):02X}" for _ in range(3))
|
||||
return "AA:BB:CC:" + ":".join(f"{random.randint(0, 255):02X}" for _ in range(3))
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
@@ -38,6 +37,7 @@ def test_graphql_debug_get(client):
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.decode() == "NetAlertX GraphQL server running."
|
||||
|
||||
|
||||
def test_graphql_post_unauthorized(client):
|
||||
"""POST /graphql without token should return 401"""
|
||||
query = {"query": "{ devices { devName devMac } }"}
|
||||
@@ -47,13 +47,14 @@ def test_graphql_post_unauthorized(client):
|
||||
error_text = resp.json.get("error", "") or resp.json.get("message", "")
|
||||
assert "Unauthorized" in error_text or "Forbidden" in error_text
|
||||
|
||||
|
||||
def test_graphql_post_devices(client, api_token):
|
||||
"""POST /graphql with a valid token should return device data"""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
devices {
|
||||
devices {
|
||||
devices {
|
||||
devGUID
|
||||
devGroup
|
||||
devIsRandomMac
|
||||
@@ -77,6 +78,7 @@ def test_graphql_post_devices(client, api_token):
|
||||
assert isinstance(data["devices"]["devices"], list)
|
||||
assert isinstance(data["devices"]["count"], int)
|
||||
|
||||
|
||||
def test_graphql_post_settings(client, api_token):
|
||||
"""POST /graphql should return settings data"""
|
||||
query = {
|
||||
|
||||
Reference in New Issue
Block a user