mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-09-16 16:18:50 -04:00
262 lines
9.8 KiB
Python
262 lines
9.8 KiB
Python
import sys
|
|
import os
|
|
import pytest
|
|
import random
|
|
from datetime import timedelta
|
|
|
|
INSTALL_PATH = os.getenv('NETALERTX_APP', '/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 utils.datetime_utils import timeNowUTC # 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)).lower()
|
|
|
|
|
|
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}
|
|
|
|
# Calculate the event_time if days_old is given
|
|
if days_old is not None:
|
|
event_time = timeNowUTC(as_string=False) - timedelta(days=days_old)
|
|
# ISO 8601 string
|
|
payload["event_time"] = event_time.isoformat()
|
|
|
|
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)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data.get("success") is True
|
|
|
|
# confirm event exists
|
|
resp = list_events(client, api_token, test_mac)
|
|
assert resp.status_code == 200
|
|
events = resp.get_json().get("events", [])
|
|
assert any(ev.get("eveMac") == test_mac for ev in events)
|
|
|
|
|
|
def test_delete_events_for_mac(client, api_token, test_mac):
|
|
# create event
|
|
resp = create_event(client, api_token, test_mac)
|
|
assert resp.status_code == 200
|
|
|
|
# confirm exists
|
|
resp = list_events(client, api_token, test_mac)
|
|
assert resp.status_code == 200
|
|
events = resp.json.get("events", [])
|
|
assert any(ev["eveMac"] == test_mac for ev in events)
|
|
|
|
# delete
|
|
resp = client.delete(f"/events/{test_mac}", headers=auth_headers(api_token))
|
|
assert resp.status_code == 200
|
|
assert resp.json.get("success") is True
|
|
|
|
# confirm deleted
|
|
resp = list_events(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(
|
|
"/sessions/totals",
|
|
headers=auth_headers(api_token)
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json
|
|
assert isinstance(data, list)
|
|
# Expecting 6 counts: all_events, sessions, missing, voided, new, down
|
|
assert len(data) == 6
|
|
for count in data:
|
|
assert isinstance(count, int) # each should be a number
|
|
|
|
# 2. Request totals with custom period
|
|
resp_month = client.get(
|
|
"/sessions/totals?period=1 month",
|
|
headers=auth_headers(api_token)
|
|
)
|
|
assert resp_month.status_code == 200
|
|
data_month = resp_month.json
|
|
assert isinstance(data_month, list)
|
|
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)
|
|
create_event(client, api_token, "ff:ff:ff:ff:ff:ff")
|
|
|
|
resp = list_events(client, api_token)
|
|
# At least the two we created should be present
|
|
assert len(resp.json.get("events", [])) >= 2
|
|
|
|
# delete all
|
|
resp = client.delete("/events", headers=auth_headers(api_token))
|
|
assert resp.status_code == 200
|
|
assert resp.json.get("success") is True
|
|
|
|
# confirm no events
|
|
resp = list_events(client, api_token)
|
|
assert len(resp.json.get("events", [])) == 0
|
|
|
|
|
|
def test_get_events_pagination(client, api_token, test_mac):
|
|
"""limit/offset on GET /events must page through the same set the
|
|
unpaginated response gives for one MAC, ordered by eveDateTime
|
|
descending, with no gaps or duplicates, and must reject invalid values."""
|
|
# Distinct event_type per call - idx_events_unique is on
|
|
# (eveMac, eveIp, eveEventType, eveDateTime), and eveDateTime only has
|
|
# second precision, so 5 calls in the same second with the same
|
|
# event_type would collide and INSERT OR IGNORE would drop 4 of them.
|
|
for i in range(5):
|
|
create_event(client, api_token, test_mac, event=f"UnitTest Event {i}")
|
|
|
|
full_resp = list_events(client, api_token, test_mac)
|
|
assert full_resp.status_code == 200
|
|
full_events = full_resp.json.get("events", [])
|
|
assert len(full_events) >= 5
|
|
|
|
total = len(full_events)
|
|
half = (total + 1) // 2
|
|
page1 = client.get(
|
|
f"/events?mac={test_mac}&limit={half}&offset=0",
|
|
headers=auth_headers(api_token),
|
|
).json.get("events", [])
|
|
page2 = client.get(
|
|
f"/events?mac={test_mac}&limit={total - half}&offset={half}",
|
|
headers=auth_headers(api_token),
|
|
).json.get("events", [])
|
|
assert page1 + page2 == full_events
|
|
|
|
# offset alone (no limit) must still take effect.
|
|
offset_only = client.get(
|
|
f"/events?mac={test_mac}&offset={half}",
|
|
headers=auth_headers(api_token),
|
|
).json.get("events", [])
|
|
assert offset_only == full_events[half:]
|
|
|
|
# Invalid limit/offset are rejected, not silently clamped.
|
|
resp_bad_limit = client.get(
|
|
f"/events?mac={test_mac}&limit=0", headers=auth_headers(api_token)
|
|
)
|
|
assert resp_bad_limit.status_code == 422
|
|
|
|
resp_bad_offset = client.get(
|
|
f"/events?mac={test_mac}&offset=-1", headers=auth_headers(api_token)
|
|
)
|
|
assert resp_bad_offset.status_code == 422
|
|
|
|
|
|
def test_get_events_pagination_stable_order_for_ties(client, api_token, test_mac):
|
|
"""Events sharing the exact same eveDateTime (a real occurrence - it only
|
|
has second precision) must still page deterministically: ORDER BY
|
|
eveDateTime DESC alone leaves tied rows in an unspecified order, so
|
|
concatenated pages could omit or duplicate rows. rowid DESC as a secondary
|
|
key must make the order stable across the unpaginated and paged calls."""
|
|
# create_event() only sets event_time when days_old is given, so post
|
|
# directly with an explicit, identical event_time for all 5 to force a tie.
|
|
shared_time = timeNowUTC(as_string=False).isoformat()
|
|
for i in range(5):
|
|
payload = {"ip": "0.0.0.0", "event_type": f"TieEventExplicit {i}", "event_time": shared_time}
|
|
resp = client.post(f"/events/create/{test_mac}", json=payload, headers=auth_headers(api_token))
|
|
assert resp.status_code == 200
|
|
|
|
full_resp = list_events(client, api_token, test_mac)
|
|
full_events = full_resp.json.get("events", [])
|
|
tied = [e for e in full_events if e.get("eveEventType", "").startswith("TieEventExplicit")]
|
|
assert len(tied) == 5
|
|
assert all(e["eveDateTime"] == tied[0]["eveDateTime"] for e in tied)
|
|
|
|
total = len(full_events)
|
|
half = (total + 1) // 2
|
|
page1 = client.get(
|
|
f"/events?mac={test_mac}&limit={half}&offset=0",
|
|
headers=auth_headers(api_token),
|
|
).json.get("events", [])
|
|
page2 = client.get(
|
|
f"/events?mac={test_mac}&limit={total - half}&offset={half}",
|
|
headers=auth_headers(api_token),
|
|
).json.get("events", [])
|
|
assert page1 + page2 == full_events
|
|
|
|
|
|
def test_delete_events_dynamic_days(client, api_token, test_mac):
|
|
# Determine initial count so test doesn't rely on preexisting events
|
|
before = list_events(client, api_token, test_mac)
|
|
initial_events = before.json.get("events", [])
|
|
initial_count = len(initial_events)
|
|
|
|
# Count pre-existing events younger than 30 days for test_mac
|
|
# These will remain after delete operation
|
|
from datetime import datetime
|
|
thirty_days_ago = timeNowUTC(as_string=False) - timedelta(days=30)
|
|
initial_younger_count = 0
|
|
for ev in initial_events:
|
|
if ev.get("eveMac") == test_mac and ev.get("eveDateTime"):
|
|
try:
|
|
# Parse event datetime (handle ISO format)
|
|
ev_time_str = ev["eveDateTime"]
|
|
# Try parsing with timezone info
|
|
try:
|
|
ev_time = datetime.fromisoformat(ev_time_str.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
# Fallback for formats without timezone
|
|
ev_time = datetime.fromisoformat(ev_time_str)
|
|
if ev_time.tzinfo is None:
|
|
ev_time = ev_time.replace(tzinfo=thirty_days_ago.tzinfo)
|
|
if ev_time > thirty_days_ago:
|
|
initial_younger_count += 1
|
|
except (ValueError, TypeError):
|
|
pass # Skip events with unparseable dates
|
|
|
|
# create old + new events
|
|
create_event(client, api_token, test_mac, days_old=40) # should be deleted
|
|
create_event(client, api_token, test_mac, days_old=5) # should remain
|
|
|
|
resp = list_events(client, api_token, test_mac)
|
|
assert len(resp.json.get("events", [])) == initial_count + 2
|
|
|
|
# delete events older than 30 days
|
|
resp = client.delete("/events/30", headers=auth_headers(api_token))
|
|
assert resp.status_code == 200
|
|
assert resp.json.get("success") is True
|
|
assert "Deleted events older than 30 days" in resp.json.get("message", "")
|
|
|
|
# confirm only recent events remain (pre-existing younger + newly created 5-day-old)
|
|
resp = list_events(client, api_token, test_mac)
|
|
events = resp.get_json().get("events", [])
|
|
mac_events = [ev for ev in events if ev.get("eveMac") == test_mac]
|
|
expected_remaining = initial_younger_count + 1 # 1 for the 5-day-old event we created
|
|
assert len(mac_events) == expected_remaining
|