Cybersecurity-Projects/PROJECTS/advanced/ai-threat-detection/backend/tests/test_parsers.py

130 lines
4.0 KiB
Python

"""
©AngelaMos | 2026
test_parsers.py
Tests nginx combined-format log line parsing via the
parse_combined function
Validates full field extraction (IP, timestamp, method,
path, query string, status code, response size, referer,
user agent, raw line), IPv4 and IPv6 address handling,
dash-referer normalization to empty string, multi-parameter
query strings with special characters, malformed and empty
line None returns, dash response size normalization to
zero, and full-length IPv6 address parsing
Connects to:
core/ingestion/parsers - parse_combined, ParsedLogEntry
"""
from datetime import datetime, UTC
from app.core.ingestion.parsers import ParsedLogEntry, parse_combined
def test_parse_standard_combined_line() -> None:
"""
Parse a standard nginx combined log line into all fields.
"""
line = ("93.184.216.34 - - [11/Feb/2026:14:30:00 +0000] "
'"GET /api/users?page=1 HTTP/1.1" 200 1234 '
'"https://example.com" '
'"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"')
result = parse_combined(line)
assert result is not None
assert isinstance(result, ParsedLogEntry)
assert result.ip == "93.184.216.34"
assert result.timestamp == datetime(2026, 2, 11, 14, 30, 0, tzinfo=UTC)
assert result.method == "GET"
assert result.path == "/api/users"
assert result.query_string == "page=1"
assert result.status_code == 200
assert result.response_size == 1234
assert result.referer == "https://example.com"
assert result.user_agent == "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
assert result.raw_line == line
def test_parse_ipv6_address() -> None:
"""
Parse a line with an IPv6 source address.
"""
line = '::1 - - [11/Feb/2026:14:30:00 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/8.0"'
result = parse_combined(line)
assert result is not None
assert result.ip == "::1"
assert result.method == "GET"
assert result.path == "/"
assert result.query_string == ""
def test_parse_missing_referer() -> None:
"""
A dash referer is normalized to an empty string.
"""
line = ("10.0.0.1 - - [11/Feb/2026:08:15:42 +0000] "
'"POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"')
result = parse_combined(line)
assert result is not None
assert result.referer == ""
assert result.method == "POST"
assert result.status_code == 302
def test_parse_complex_query_string() -> None:
"""
Query strings with multiple parameters and special characters.
"""
line = (
"93.184.216.34 - - [11/Feb/2026:14:30:00 +0000] "
'"GET /search?q=hello+world&lang=en&page=2&sort=relevance HTTP/1.1" '
'200 5678 "https://example.com/search" "Mozilla/5.0"')
result = parse_combined(line)
assert result is not None
assert result.path == "/search"
assert result.query_string == "q=hello+world&lang=en&page=2&sort=relevance"
def test_parse_malformed_line_returns_none() -> None:
"""
Malformed lines return None instead of raising.
"""
assert parse_combined("this is not a valid log line") is None
def test_parse_empty_line_returns_none() -> None:
"""
Empty input returns None.
"""
assert parse_combined("") is None
def test_parse_dash_response_size() -> None:
"""
A dash response size (e.g. HEAD 304) is normalized to zero.
"""
line = '1.2.3.4 - - [11/Feb/2026:10:00:00 +0000] "HEAD / HTTP/1.1" 304 - "-" "Mozilla/5.0"'
result = parse_combined(line)
assert result is not None
assert result.response_size == 0
assert result.status_code == 304
def test_parse_full_ipv6_address() -> None:
"""
Parse a line with a full-length IPv6 address.
"""
line = ("2001:0db8:85a3:0000:0000:8a2e:0370:7334 - - "
'[11/Feb/2026:14:30:00 +0000] "GET /api/v1/health HTTP/2.0" '
'200 256 "-" "python-httpx/0.28"')
result = parse_combined(line)
assert result is not None
assert result.ip == "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
assert result.path == "/api/v1/health"