130 lines
4.0 KiB
Python
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"
|