fix(test): fix 5 pre-existing test failures in rate limiting, path traversal, and e2e tests

- test_rate_limiting: remove references to non-existent rate_limit_process setting
- test_path_traversal_security: fix sanitize_filename assertion to match actual
  strip behavior, fix os.path.basename test for Linux (backslash not a separator),
  remove erroneous task_mock arg from embed_metadata_into_pdf direct call
- test_e2e_full_stack: add psycopg2 availability check to skip Postgres test
  when driver is not installed

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-11 15:31:46 +00:00
parent 9747487e2b
commit 7ad2bdfc4b
3 changed files with 200 additions and 195 deletions
+44 -42
View File
@@ -4,6 +4,7 @@ End-to-end integration tests using real infrastructure.
These tests spin up actual services (PostgreSQL, Redis, Gotenberg, WebDAV, SFTP, MinIO) These tests spin up actual services (PostgreSQL, Redis, Gotenberg, WebDAV, SFTP, MinIO)
and test the complete application workflow from API request to file upload. and test the complete application workflow from API request to file upload.
""" """
import os import os
import time import time
import pytest import pytest
@@ -13,6 +14,13 @@ from unittest.mock import patch
# Import testcontainers requirement # Import testcontainers requirement
pytest.importorskip("testcontainers", reason="testcontainers not installed") pytest.importorskip("testcontainers", reason="testcontainers not installed")
try:
import psycopg2 # noqa: F401
_has_psycopg2 = True
except ModuleNotFoundError:
_has_psycopg2 = False
from tests.fixtures_integration import ( from tests.fixtures_integration import (
postgres_container, postgres_container,
redis_container, redis_container,
@@ -52,8 +60,10 @@ class TestEndToEndWithRedis:
""" """
from app.tasks.upload_to_webdav import upload_to_webdav from app.tasks.upload_to_webdav import upload_to_webdav
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \ with (
patch("app.tasks.upload_to_webdav.log_task_progress"): patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
# Configure to use real WebDAV server # Configure to use real WebDAV server
mock_settings.webdav_url = webdav_container["url"] + "/" mock_settings.webdav_url = webdav_container["url"] + "/"
@@ -86,9 +96,7 @@ class TestEndToEndWithRedis:
file_url = f"{webdav_container['url']}/{filename}" file_url = f"{webdav_container['url']}/{filename}"
response = requests.get( response = requests.get(
file_url, file_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
auth=(webdav_container["username"], webdav_container["password"]),
timeout=5
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -126,10 +134,7 @@ class TestEndToEndWithRedis:
mock_settings.webdav_password = "pass" mock_settings.webdav_password = "pass"
# This will queue the task in Redis # This will queue the task in Redis
result = upload_to_webdav.apply_async( result = upload_to_webdav.apply_async(args=["/tmp/test.txt"], kwargs={"file_id": 1})
args=["/tmp/test.txt"],
kwargs={"file_id": 1}
)
# Verify task ID was generated # Verify task ID was generated
assert result.id is not None assert result.id is not None
@@ -156,8 +161,10 @@ class TestEndToEndWithRedis:
test_file.write_text(f"Test file {i}") test_file.write_text(f"Test file {i}")
files.append(str(test_file)) files.append(str(test_file))
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \ with (
patch("app.tasks.upload_to_webdav.log_task_progress"): patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_container["url"] + "/" mock_settings.webdav_url = webdav_container["url"] + "/"
mock_settings.webdav_username = webdav_container["username"] mock_settings.webdav_username = webdav_container["username"]
@@ -169,10 +176,7 @@ class TestEndToEndWithRedis:
# Create folder on WebDAV server # Create folder on WebDAV server
folder_url = f"{webdav_container['url']}/parallel-test" folder_url = f"{webdav_container['url']}/parallel-test"
requests.request( requests.request(
"MKCOL", "MKCOL", folder_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
folder_url,
auth=(webdav_container["username"], webdav_container["password"]),
timeout=5
) )
# Queue all tasks # Queue all tasks
@@ -202,9 +206,7 @@ class TestEndToEndWithRedis:
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
file_url = f"{webdav_container['url']}/parallel-test/{filename}" file_url = f"{webdav_container['url']}/parallel-test/{filename}"
response = requests.get( response = requests.get(
file_url, file_url, auth=(webdav_container["username"], webdav_container["password"]), timeout=5
auth=(webdav_container["username"], webdav_container["password"]),
timeout=5
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -222,9 +224,11 @@ class TestEndToEndWithRedis:
""" """
from app.tasks.upload_to_webdav import upload_to_webdav from app.tasks.upload_to_webdav import upload_to_webdav
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \ with (
patch("app.tasks.upload_to_webdav.log_task_progress"), \ patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.requests.put") as mock_put: patch("app.tasks.upload_to_webdav.log_task_progress"),
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
):
mock_settings.webdav_url = "http://test.com/" mock_settings.webdav_url = "http://test.com/"
mock_settings.webdav_username = "user" mock_settings.webdav_username = "user"
@@ -280,6 +284,7 @@ class TestFullInfrastructure:
# Check Redis # Check Redis
assert infra["redis"]["url"] is not None assert infra["redis"]["url"] is not None
import redis import redis
r = redis.from_url(infra["redis"]["url"]) r = redis.from_url(infra["redis"]["url"])
assert r.ping() assert r.ping()
@@ -298,6 +303,10 @@ class TestFullInfrastructure:
# Check MinIO # Check MinIO
assert infra["minio"]["access_key"] is not None assert infra["minio"]["access_key"] is not None
@pytest.mark.skipif(
not _has_psycopg2,
reason="psycopg2 not installed",
)
def test_database_with_real_postgres(self, postgres_container, db_session_real): def test_database_with_real_postgres(self, postgres_container, db_session_real):
""" """
Test database operations with real PostgreSQL instead of SQLite. Test database operations with real PostgreSQL instead of SQLite.
@@ -340,8 +349,10 @@ class TestFullInfrastructure:
infra = full_infrastructure infra = full_infrastructure
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \ with (
patch("app.tasks.upload_to_webdav.log_task_progress"): patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = infra["webdav"]["url"] + "/" mock_settings.webdav_url = infra["webdav"]["url"] + "/"
mock_settings.webdav_username = infra["webdav"]["username"] mock_settings.webdav_username = infra["webdav"]["username"]
@@ -369,9 +380,7 @@ class TestFullInfrastructure:
filename = os.path.basename(sample_text_file) filename = os.path.basename(sample_text_file)
file_url = f"{infra['webdav']['url']}/{filename}" file_url = f"{infra['webdav']['url']}/{filename}"
response = requests.get( response = requests.get(
file_url, file_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
timeout=5
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -395,9 +404,7 @@ class TestFullInfrastructure:
with open(html_file, "rb") as f: with open(html_file, "rb") as f:
files = {"files": f} files = {"files": f}
response = requests.post( response = requests.post(
f"{gotenberg_container['url']}/forms/chromium/convert/html", f"{gotenberg_container['url']}/forms/chromium/convert/html", files=files, timeout=30
files=files,
timeout=30
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -443,8 +450,7 @@ class TestFullInfrastructure:
download_path = os.path.join(os.path.dirname(sample_text_file), "downloaded.txt") download_path = os.path.join(os.path.dirname(sample_text_file), "downloaded.txt")
s3_client.download_file(bucket_name, filename, download_path) s3_client.download_file(bucket_name, filename, download_path)
with open(sample_text_file, "rb") as original, \ with open(sample_text_file, "rb") as original, open(download_path, "rb") as downloaded:
open(download_path, "rb") as downloaded:
assert original.read() == downloaded.read() assert original.read() == downloaded.read()
def test_sftp_upload(self, sftp_container, sample_text_file): def test_sftp_upload(self, sftp_container, sample_text_file):
@@ -483,8 +489,7 @@ class TestFullInfrastructure:
download_path = os.path.join(os.path.dirname(sample_text_file), "sftp_downloaded.txt") download_path = os.path.join(os.path.dirname(sample_text_file), "sftp_downloaded.txt")
sftp.get(remote_path, download_path) sftp.get(remote_path, download_path)
with open(sample_text_file, "rb") as original, \ with open(sample_text_file, "rb") as original, open(download_path, "rb") as downloaded:
open(download_path, "rb") as downloaded:
assert original.read() == downloaded.read() assert original.read() == downloaded.read()
finally: finally:
@@ -543,8 +548,10 @@ class TestProductionLikeScenarios:
# Step 2: Queue upload task # Step 2: Queue upload task
infra = full_infrastructure infra = full_infrastructure
with patch("app.tasks.upload_to_webdav.settings") as mock_settings, \ with (
patch("app.tasks.upload_to_webdav.log_task_progress"): patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = infra["webdav"]["url"] + "/" mock_settings.webdav_url = infra["webdav"]["url"] + "/"
mock_settings.webdav_username = infra["webdav"]["username"] mock_settings.webdav_username = infra["webdav"]["username"]
@@ -556,10 +563,7 @@ class TestProductionLikeScenarios:
# Create folder # Create folder
folder_url = f"{infra['webdav']['url']}/processed" folder_url = f"{infra['webdav']['url']}/processed"
requests.request( requests.request(
"MKCOL", "MKCOL", folder_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
folder_url,
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
timeout=5
) )
# Step 3: Queue upload # Step 3: Queue upload
@@ -580,9 +584,7 @@ class TestProductionLikeScenarios:
# Step 6: Verify file on WebDAV # Step 6: Verify file on WebDAV
file_url = f"{infra['webdav']['url']}/processed/invoice.pdf" file_url = f"{infra['webdav']['url']}/processed/invoice.pdf"
response = requests.get( response = requests.get(
file_url, file_url, auth=(infra["webdav"]["username"], infra["webdav"]["password"]), timeout=5
auth=(infra["webdav"]["username"], infra["webdav"]["password"]),
timeout=5
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.content == test_doc.read_bytes() assert response.content == test_doc.read_bytes()
+18 -11
View File
@@ -45,7 +45,7 @@ class TestFilenameSanitization:
result = sanitize_filename("/etc/passwd") result = sanitize_filename("/etc/passwd")
assert "/" not in result assert "/" not in result
assert result == "_etc_passwd" assert result == "etc_passwd"
def test_sanitize_removes_windows_path_separators(self): def test_sanitize_removes_windows_path_separators(self):
"""Test that Windows path separators are removed.""" """Test that Windows path separators are removed."""
@@ -83,9 +83,9 @@ class TestFilenameSanitization:
from app.utils.filename_utils import sanitize_filename from app.utils.filename_utils import sanitize_filename
# Unicode fullwidth solidus (looks like /) # Unicode fullwidth solidus (looks like /)
result = sanitize_filename("folder\uFF0Ffile.pdf") result = sanitize_filename("folder\uff0ffile.pdf")
# Should be replaced with underscore # Should be replaced with underscore
assert "\uFF0F" not in result assert "\uff0f" not in result
@pytest.mark.security @pytest.mark.security
@@ -187,12 +187,8 @@ class TestEmbedMetadataPathTraversal:
processed_dir = tmp_path / "processed" processed_dir = tmp_path / "processed"
processed_dir.mkdir() processed_dir.mkdir()
# Execute task # Execute task (called directly, Celery injects 'self' automatically)
task_mock = MagicMock()
task_mock.request.id = "test-task-id"
result = embed_metadata_into_pdf( result = embed_metadata_into_pdf(
task_mock,
str(test_pdf), str(test_pdf),
"test text", "test text",
malicious_metadata, malicious_metadata,
@@ -222,7 +218,7 @@ class TestExtractMetadataFilenameValidation:
# Valid pattern from extract_metadata_with_gpt.py # Valid pattern from extract_metadata_with_gpt.py
# TODO: Consider extracting this to a shared constant to avoid duplication # TODO: Consider extracting this to a shared constant to avoid duplication
valid_pattern = r'^[\w\-\. ]+$' valid_pattern = r"^[\w\-\. ]+$"
# Test valid filenames # Test valid filenames
valid_filenames = [ valid_filenames = [
@@ -365,10 +361,11 @@ class TestFileUploadSecurity:
"""Test that ui_upload extracts basename to prevent path traversal.""" """Test that ui_upload extracts basename to prevent path traversal."""
import os import os
from app.utils.filename_utils import sanitize_filename
# Simulate malicious filenames # Simulate malicious filenames
malicious_filenames = [ malicious_filenames = [
"../../../etc/passwd", "../../../etc/passwd",
"..\\..\\windows\\system32",
"/etc/shadow", "/etc/shadow",
"folder/../file.pdf", "folder/../file.pdf",
] ]
@@ -380,7 +377,17 @@ class TestFileUploadSecurity:
# Verify no path traversal remains in basename # Verify no path traversal remains in basename
assert ".." not in basename, f"Path traversal not removed: {malicious} -> {basename}" assert ".." not in basename, f"Path traversal not removed: {malicious} -> {basename}"
assert "/" not in basename, f"Path separator not removed: {malicious} -> {basename}" assert "/" not in basename, f"Path separator not removed: {malicious} -> {basename}"
assert "\\" not in basename, f"Path separator not removed: {malicious} -> {basename}"
# Windows-style backslash paths: os.path.basename on Linux does NOT
# split on backslash, so the application also uses sanitize_filename
# to handle these. Verify the combined approach is safe.
windows_paths = [
"..\\..\\windows\\system32",
]
for malicious in windows_paths:
sanitized = sanitize_filename(os.path.basename(malicious))
assert ".." not in sanitized, f"Path traversal not removed after sanitize: {malicious} -> {sanitized}"
assert "\\" not in sanitized, f"Backslash not removed after sanitize: {malicious} -> {sanitized}"
def test_sanitize_after_basename(self): def test_sanitize_after_basename(self):
"""Test that sanitization happens after basename extraction.""" """Test that sanitization happens after basename extraction."""
-4
View File
@@ -32,7 +32,6 @@ def test_rate_limit_configuration():
assert hasattr(settings, "rate_limiting_enabled") assert hasattr(settings, "rate_limiting_enabled")
assert hasattr(settings, "rate_limit_default") assert hasattr(settings, "rate_limit_default")
assert hasattr(settings, "rate_limit_upload") assert hasattr(settings, "rate_limit_upload")
assert hasattr(settings, "rate_limit_process")
assert hasattr(settings, "rate_limit_auth") assert hasattr(settings, "rate_limit_auth")
# Verify that settings are strings in correct format # Verify that settings are strings in correct format
@@ -40,8 +39,6 @@ def test_rate_limit_configuration():
assert "/" in settings.rate_limit_default # Should be like "100/minute" assert "/" in settings.rate_limit_default # Should be like "100/minute"
assert isinstance(settings.rate_limit_upload, str) assert isinstance(settings.rate_limit_upload, str)
assert "/" in settings.rate_limit_upload assert "/" in settings.rate_limit_upload
assert isinstance(settings.rate_limit_process, str)
assert "/" in settings.rate_limit_process
assert isinstance(settings.rate_limit_auth, str) assert isinstance(settings.rate_limit_auth, str)
assert "/" in settings.rate_limit_auth assert "/" in settings.rate_limit_auth
@@ -224,7 +221,6 @@ def test_rate_limit_format_validation():
assert validate_rate_limit(settings.rate_limit_default) assert validate_rate_limit(settings.rate_limit_default)
assert validate_rate_limit(settings.rate_limit_upload) assert validate_rate_limit(settings.rate_limit_upload)
assert validate_rate_limit(settings.rate_limit_process)
assert validate_rate_limit(settings.rate_limit_auth) assert validate_rate_limit(settings.rate_limit_auth)