7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
633 lines
21 KiB
Python
633 lines
21 KiB
Python
"""
|
|
Fuzz tests for security-critical functions using Hypothesis.
|
|
|
|
These tests verify that security functions don't crash on arbitrary input
|
|
and properly reject malicious inputs.
|
|
"""
|
|
|
|
import html
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from urllib.parse import quote, unquote
|
|
|
|
import pytest
|
|
from hypothesis import assume, given, settings, strategies as st
|
|
|
|
from local_deep_research.security.path_validator import PathValidator
|
|
from local_deep_research.utilities.url_utils import normalize_url
|
|
|
|
|
|
# =============================================================================
|
|
# Custom Strategies for Security Testing
|
|
# =============================================================================
|
|
|
|
|
|
def path_traversal_payloads():
|
|
"""Generate various path traversal attack payloads."""
|
|
basic = [
|
|
"../",
|
|
"..\\",
|
|
"../" * 10,
|
|
"..\\..\\",
|
|
"....//",
|
|
"..../",
|
|
".../",
|
|
"..%2f",
|
|
"..%5c",
|
|
"%2e%2e/",
|
|
"%2e%2e%2f",
|
|
"..%252f", # Double encoding
|
|
"..%c0%af", # Overlong UTF-8
|
|
"..%c1%9c",
|
|
"..;/", # Parameter injection
|
|
"..%00/", # Null byte
|
|
"..//",
|
|
".\\.\\",
|
|
"..\\../",
|
|
]
|
|
return st.sampled_from(basic)
|
|
|
|
|
|
def null_byte_payloads():
|
|
"""Generate null byte injection payloads."""
|
|
return st.sampled_from(
|
|
[
|
|
"\x00",
|
|
"%00",
|
|
"\x00\x00",
|
|
"file\x00.txt",
|
|
"file%00.txt",
|
|
"\x00/../etc/passwd",
|
|
]
|
|
)
|
|
|
|
|
|
def xss_payloads():
|
|
"""Generate XSS attack payloads for HTML contexts."""
|
|
return st.sampled_from(
|
|
[
|
|
"<script>alert(1)</script>",
|
|
'"><script>alert(1)</script>',
|
|
"javascript:alert(1)",
|
|
"<img src=x onerror=alert(1)>",
|
|
"<svg onload=alert(1)>",
|
|
"'-alert(1)-'",
|
|
'"><img src=x onerror=alert(1)>',
|
|
"<body onload=alert(1)>",
|
|
"{{constructor.constructor('alert(1)')()}}",
|
|
"${alert(1)}",
|
|
"#{alert(1)}",
|
|
"<iframe src='javascript:alert(1)'>",
|
|
"<math><maction xlink:href='javascript:alert(1)'>",
|
|
"data:text/html,<script>alert(1)</script>",
|
|
]
|
|
)
|
|
|
|
|
|
def dangerous_url_schemes():
|
|
"""Generate dangerous URL schemes."""
|
|
return st.sampled_from(
|
|
[
|
|
"javascript:",
|
|
"data:",
|
|
"vbscript:",
|
|
"file:",
|
|
"ftp://",
|
|
"gopher://",
|
|
"jar:",
|
|
"netdoc:",
|
|
]
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# PATH VALIDATOR FUZZ TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestPathValidatorFuzzing:
|
|
"""Comprehensive fuzz tests for PathValidator security functions."""
|
|
|
|
@given(user_input=st.text(max_size=1000))
|
|
@settings(max_examples=200)
|
|
def test_validate_safe_path_no_crash(self, user_input):
|
|
"""Test that validate_safe_path never crashes on arbitrary input."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
try:
|
|
PathValidator.validate_safe_path(user_input, temp_dir)
|
|
except ValueError:
|
|
# Expected for invalid paths - this is correct behavior
|
|
pass
|
|
except Exception as e:
|
|
# Any other exception type is a bug
|
|
pytest.fail(
|
|
f"Unexpected exception type: {type(e).__name__}: {e}"
|
|
)
|
|
|
|
@given(
|
|
user_input=st.text(
|
|
alphabet=st.characters(
|
|
blacklist_categories=("Cs",), # Exclude surrogates
|
|
),
|
|
max_size=500,
|
|
)
|
|
)
|
|
@settings(max_examples=200)
|
|
def test_validate_config_path_no_crash(self, user_input):
|
|
"""Test that validate_config_path never crashes on arbitrary input."""
|
|
try:
|
|
PathValidator.validate_config_path(user_input, "/tmp")
|
|
except ValueError:
|
|
# Expected for invalid paths
|
|
pass
|
|
except Exception as e:
|
|
# Any other exception type is a bug
|
|
pytest.fail(f"Unexpected exception type: {type(e).__name__}: {e}")
|
|
|
|
@given(payload=path_traversal_payloads(), suffix=st.text(max_size=50))
|
|
@settings(max_examples=100)
|
|
def test_path_traversal_variants_blocked(self, payload, suffix):
|
|
"""Test that various path traversal encodings are blocked."""
|
|
# Filter out problematic characters from suffix
|
|
safe_suffix = "".join(c for c in suffix if c.isalnum() or c in "._-/")
|
|
path = payload + safe_suffix
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
try:
|
|
result = PathValidator.validate_safe_path(path, temp_dir)
|
|
# If it didn't raise, verify path is still within base_dir
|
|
if result:
|
|
resolved = result.resolve()
|
|
base_resolved = Path(temp_dir).resolve()
|
|
assert str(resolved).startswith(str(base_resolved)), (
|
|
f"Path escaped base_dir: {resolved}"
|
|
)
|
|
except ValueError:
|
|
# Expected - traversal was blocked
|
|
pass
|
|
|
|
@given(
|
|
null_count=st.integers(min_value=1, max_value=10),
|
|
position=st.sampled_from(["start", "middle", "end"]),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_null_byte_injection_positions(self, null_count, position):
|
|
"""Test null byte injection at various positions."""
|
|
null_bytes = "\x00" * null_count
|
|
if position == "start":
|
|
path = null_bytes + "file.json"
|
|
elif position == "middle":
|
|
path = "test" + null_bytes + "file.json"
|
|
else:
|
|
path = "testfile.json" + null_bytes
|
|
|
|
try:
|
|
PathValidator.validate_config_path(path, "/tmp")
|
|
except ValueError:
|
|
# Expected - null bytes should be rejected
|
|
pass
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"Unexpected exception for null byte input: {type(e).__name__}"
|
|
)
|
|
|
|
@given(
|
|
prefix=st.sampled_from(
|
|
["etc", "proc", "sys", "dev", "var/log", "root", "home"]
|
|
),
|
|
suffix=st.text(
|
|
alphabet=st.characters(whitelist_categories=("L", "N")), max_size=20
|
|
),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_restricted_system_paths_blocked(self, prefix, suffix):
|
|
"""Test that restricted system directories are blocked."""
|
|
for path_variant in [
|
|
f"/{prefix}/{suffix}",
|
|
f"{prefix}/{suffix}",
|
|
f"/{prefix}",
|
|
f"../{prefix}/{suffix}",
|
|
]:
|
|
try:
|
|
PathValidator.validate_config_path(path_variant, "/tmp")
|
|
except ValueError:
|
|
# Expected - restricted paths should be blocked
|
|
pass
|
|
|
|
@given(
|
|
depth=st.integers(min_value=1, max_value=50),
|
|
segment=st.text(
|
|
alphabet=st.characters(whitelist_categories=("L",)),
|
|
min_size=1,
|
|
max_size=10,
|
|
),
|
|
)
|
|
@settings(max_examples=50)
|
|
def test_deep_path_nesting(self, depth, segment):
|
|
"""Test handling of deeply nested paths."""
|
|
assume(len(segment) > 0)
|
|
path = "/".join([segment] * depth)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
try:
|
|
PathValidator.validate_safe_path(path, temp_dir)
|
|
except ValueError:
|
|
pass
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"Unexpected exception for deep path: {type(e).__name__}"
|
|
)
|
|
|
|
@given(
|
|
encoded=st.sampled_from(
|
|
[
|
|
"%2e%2e%2f", # ../
|
|
"%2e%2e/", # ../
|
|
"..%2f", # ../
|
|
"%2e%2e%5c", # ..\
|
|
"..%5c", # ..\
|
|
"%252e%252e%252f", # Double encoded ../
|
|
"..%c0%af", # Overlong UTF-8 /
|
|
"..%e0%80%af", # Overlong UTF-8 /
|
|
"..%c0%2f", # Mixed encoding
|
|
]
|
|
)
|
|
)
|
|
@settings(max_examples=50)
|
|
def test_url_encoded_traversal_blocked(self, encoded):
|
|
"""Test URL-encoded path traversal attempts."""
|
|
# Try both encoded and decoded versions
|
|
for path in [encoded, unquote(encoded)]:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
try:
|
|
result = PathValidator.validate_safe_path(path, temp_dir)
|
|
if result:
|
|
# Verify we didn't escape
|
|
resolved = result.resolve()
|
|
base_resolved = Path(temp_dir).resolve()
|
|
assert str(resolved).startswith(str(base_resolved))
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# URL NORMALIZATION FUZZ TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestURLNormalizationFuzzing:
|
|
"""Fuzz tests for URL normalization functions."""
|
|
|
|
@given(
|
|
url=st.text(
|
|
alphabet=st.characters(blacklist_categories=("Cs",)),
|
|
min_size=1,
|
|
max_size=500,
|
|
)
|
|
)
|
|
@settings(max_examples=200)
|
|
def test_normalize_url_no_crash(self, url):
|
|
"""Test that normalize_url never crashes on arbitrary input."""
|
|
try:
|
|
result = normalize_url(url)
|
|
# Result should always be a string
|
|
assert isinstance(result, str)
|
|
except ValueError:
|
|
# Empty URL raises ValueError - this is expected
|
|
pass
|
|
except Exception as e:
|
|
pytest.fail(f"Unexpected exception: {type(e).__name__}: {e}")
|
|
|
|
@given(scheme=dangerous_url_schemes(), rest=st.text(max_size=100))
|
|
@settings(max_examples=100)
|
|
def test_dangerous_schemes_handling(self, scheme, rest):
|
|
"""Test handling of dangerous URL schemes."""
|
|
url = scheme + rest
|
|
try:
|
|
result = normalize_url(url)
|
|
# Function should either reject or sanitize dangerous schemes
|
|
# At minimum, it shouldn't crash
|
|
assert isinstance(result, str)
|
|
except ValueError:
|
|
pass
|
|
|
|
@given(
|
|
hostname=st.sampled_from(
|
|
[
|
|
"localhost",
|
|
"127.0.0.1",
|
|
"0.0.0.0",
|
|
"[::1]",
|
|
"127.0.0.2",
|
|
"127.1",
|
|
"2130706433", # 127.0.0.1 as decimal
|
|
"0x7f000001", # 127.0.0.1 as hex
|
|
"0177.0.0.1", # Octal
|
|
"localhost.localdomain",
|
|
]
|
|
),
|
|
port=st.integers(min_value=1, max_value=65535),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_localhost_variants(self, hostname, port):
|
|
"""Test various localhost representations."""
|
|
url = f"{hostname}:{port}"
|
|
try:
|
|
result = normalize_url(url)
|
|
assert isinstance(result, str)
|
|
# Should have a scheme
|
|
assert "://" in result
|
|
except ValueError:
|
|
pass
|
|
|
|
@given(
|
|
host=st.text(
|
|
alphabet=st.characters(whitelist_categories=("L", "N")),
|
|
min_size=1,
|
|
max_size=50,
|
|
),
|
|
port=st.integers(min_value=1, max_value=65535),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_host_port_combinations(self, host, port):
|
|
"""Test various host:port combinations."""
|
|
url = f"{host}:{port}"
|
|
try:
|
|
result = normalize_url(url)
|
|
assert isinstance(result, str)
|
|
except ValueError:
|
|
pass
|
|
|
|
@given(
|
|
ipv6=st.from_regex(r"\[[0-9a-fA-F:]+\]", fullmatch=True),
|
|
port=st.integers(min_value=1, max_value=65535),
|
|
)
|
|
@settings(max_examples=50)
|
|
def test_ipv6_addresses(self, ipv6, port):
|
|
"""Test IPv6 address handling."""
|
|
url = f"{ipv6}:{port}"
|
|
try:
|
|
result = normalize_url(url)
|
|
assert isinstance(result, str)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# HTML/PDF XSS FUZZ TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestHTMLSanitizationFuzzing:
|
|
"""Fuzz tests for HTML generation and sanitization."""
|
|
|
|
@given(payload=xss_payloads())
|
|
@settings(max_examples=50)
|
|
def test_xss_payloads_escaped_in_html(self, payload):
|
|
"""Test that XSS payloads with HTML tags are properly escaped."""
|
|
# Test html.escape function behavior
|
|
escaped = html.escape(payload)
|
|
|
|
# HTML angle brackets should always be escaped
|
|
# This prevents script injection via HTML tags
|
|
if "<" in payload:
|
|
assert "<" in escaped, "< should be escaped to <"
|
|
if ">" in payload:
|
|
assert ">" in escaped, "> should be escaped to >"
|
|
|
|
# The escaped string should never contain raw angle brackets
|
|
# (the original ones should all be escaped)
|
|
original_lt_count = payload.count("<")
|
|
original_gt_count = payload.count(">")
|
|
escaped_lt_count = escaped.count("<")
|
|
escaped_gt_count = escaped.count(">")
|
|
|
|
assert original_lt_count == escaped_lt_count, "All < should be escaped"
|
|
assert original_gt_count == escaped_gt_count, "All > should be escaped"
|
|
|
|
@given(
|
|
title=st.text(max_size=200),
|
|
key=st.text(
|
|
alphabet=st.characters(whitelist_categories=("L", "N")), max_size=50
|
|
),
|
|
value=st.text(max_size=200),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_metadata_escaping(self, title, key, value):
|
|
"""Test that metadata values are properly escaped for HTML."""
|
|
# Simulate what PDFService._markdown_to_html does
|
|
# These should be escaped before insertion
|
|
escaped_title = html.escape(title)
|
|
escaped_key = html.escape(key)
|
|
escaped_value = html.escape(value)
|
|
|
|
# Verify no unescaped angle brackets (except the tags themselves)
|
|
assert title.count("<") == escaped_title.count("<")
|
|
assert title.count(">") == escaped_title.count(">")
|
|
|
|
# Verify the escaped values don't contain raw HTML special chars
|
|
assert "<" not in escaped_title or "<" in escaped_title
|
|
assert '"' not in escaped_key or """ in escaped_key
|
|
assert '"' not in escaped_value or """ in escaped_value
|
|
|
|
@given(
|
|
content=st.text(
|
|
alphabet=st.characters(blacklist_categories=("Cs",)), max_size=1000
|
|
)
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_markdown_content_handling(self, content):
|
|
"""Test markdown content doesn't cause crashes."""
|
|
# Import here to avoid import errors if markdown not installed
|
|
try:
|
|
import markdown
|
|
|
|
md = markdown.Markdown(
|
|
extensions=["tables", "fenced_code", "footnotes"]
|
|
)
|
|
# Should not crash on arbitrary input
|
|
result = md.convert(content)
|
|
assert isinstance(result, str)
|
|
except ImportError:
|
|
pytest.skip("markdown not installed")
|
|
except Exception as e:
|
|
# Some parsing errors are acceptable, but not crashes
|
|
if "crash" in str(type(e).__name__).lower():
|
|
pytest.fail(f"Markdown parser crashed: {e}")
|
|
|
|
|
|
# =============================================================================
|
|
# JSON PARSING ROBUSTNESS FUZZ TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestJSONParsingFuzzing:
|
|
"""Fuzz tests for JSON parsing robustness."""
|
|
|
|
@given(
|
|
data=st.recursive(
|
|
st.none()
|
|
| st.booleans()
|
|
| st.integers()
|
|
| st.floats(allow_nan=False, allow_infinity=False)
|
|
| st.text(max_size=100),
|
|
lambda children: (
|
|
st.lists(children, max_size=10)
|
|
| st.dictionaries(st.text(max_size=20), children, max_size=10)
|
|
),
|
|
max_leaves=50,
|
|
)
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_json_roundtrip(self, data):
|
|
"""Test JSON serialization/deserialization doesn't crash."""
|
|
try:
|
|
serialized = json.dumps(data)
|
|
deserialized = json.loads(serialized)
|
|
# Should roundtrip correctly
|
|
assert json.dumps(deserialized) == serialized
|
|
except (TypeError, ValueError):
|
|
# Some Python objects can't be JSON serialized
|
|
pass
|
|
|
|
@given(malformed=st.text(max_size=500))
|
|
@settings(max_examples=100)
|
|
def test_malformed_json_handling(self, malformed):
|
|
"""Test that malformed JSON raises JSONDecodeError, not crashes."""
|
|
try:
|
|
json.loads(malformed)
|
|
except json.JSONDecodeError:
|
|
# Expected for malformed JSON
|
|
pass
|
|
except Exception as e:
|
|
# Any other exception is unexpected
|
|
if not isinstance(e, (ValueError, TypeError)):
|
|
pytest.fail(
|
|
f"Unexpected exception parsing JSON: {type(e).__name__}"
|
|
)
|
|
|
|
@given(depth=st.integers(min_value=1, max_value=100))
|
|
@settings(max_examples=50)
|
|
def test_deeply_nested_json(self, depth):
|
|
"""Test handling of deeply nested JSON structures."""
|
|
# Build nested structure
|
|
nested = {"value": None}
|
|
for _ in range(depth):
|
|
nested = {"nested": nested}
|
|
|
|
try:
|
|
serialized = json.dumps(nested)
|
|
json.loads(serialized)
|
|
except RecursionError:
|
|
# Very deep nesting might hit recursion limit - that's acceptable
|
|
pass
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"Unexpected exception for nested JSON: {type(e).__name__}"
|
|
)
|
|
|
|
@given(
|
|
size=st.integers(min_value=1, max_value=10000),
|
|
char=st.characters(whitelist_categories=("L", "N")),
|
|
)
|
|
@settings(max_examples=50)
|
|
def test_large_json_strings(self, size, char):
|
|
"""Test handling of large JSON string values."""
|
|
large_string = char * size
|
|
data = {"large": large_string}
|
|
|
|
try:
|
|
serialized = json.dumps(data)
|
|
result = json.loads(serialized)
|
|
assert result["large"] == large_string
|
|
except MemoryError:
|
|
# Very large strings might cause memory issues - acceptable
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# SEARCH QUERY FUZZ TESTS
|
|
# =============================================================================
|
|
|
|
|
|
class TestSearchQueryFuzzing:
|
|
"""Fuzz tests for search query handling."""
|
|
|
|
@given(
|
|
query=st.text(
|
|
alphabet=st.characters(blacklist_categories=("Cs",)), max_size=500
|
|
)
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_search_query_special_chars(self, query):
|
|
"""Test that search queries with special characters don't crash."""
|
|
# Test common query operations
|
|
try:
|
|
# Normalize whitespace
|
|
normalized = " ".join(query.split())
|
|
|
|
# URL encode for API calls
|
|
encoded = quote(query, safe="")
|
|
|
|
# Should produce valid strings
|
|
assert isinstance(normalized, str)
|
|
assert isinstance(encoded, str)
|
|
except Exception as e:
|
|
pytest.fail(f"Query processing failed: {type(e).__name__}: {e}")
|
|
|
|
@given(
|
|
operator=st.sampled_from(
|
|
[
|
|
"AND",
|
|
"OR",
|
|
"NOT",
|
|
"NEAR",
|
|
"ADJ",
|
|
"&&",
|
|
"||",
|
|
"!",
|
|
"+",
|
|
"-",
|
|
"~",
|
|
"*",
|
|
"?",
|
|
'"',
|
|
"(",
|
|
")",
|
|
"[",
|
|
"]",
|
|
"{",
|
|
"}",
|
|
":",
|
|
"^",
|
|
]
|
|
),
|
|
term=st.text(
|
|
alphabet=st.characters(whitelist_categories=("L",)),
|
|
min_size=1,
|
|
max_size=20,
|
|
),
|
|
)
|
|
@settings(max_examples=100)
|
|
def test_search_operators_handling(self, operator, term):
|
|
"""Test handling of search operators in queries."""
|
|
queries = [
|
|
f"{operator}{term}",
|
|
f"{term}{operator}",
|
|
f"{term} {operator} {term}",
|
|
f'"{operator}"',
|
|
f"({term} {operator} {term})",
|
|
]
|
|
|
|
for query in queries:
|
|
try:
|
|
# Basic sanitization - should not crash
|
|
sanitized = query.replace("\x00", "")
|
|
normalized = " ".join(sanitized.split())
|
|
assert isinstance(normalized, str)
|
|
except Exception as e:
|
|
pytest.fail(
|
|
f"Search operator handling failed: {type(e).__name__}"
|
|
)
|