Files
gh-christianlouis-docuelevate/tests/test_upload_tasks.py

533 lines
21 KiB
Python

"""
Tests for upload tasks including OneDrive, S3, FTP, SFTP, WebDAV, Google Drive, and Email.
"""
import os
from unittest.mock import Mock, patch
import pytest
from app.tasks.upload_to_email import upload_to_email
from app.tasks.upload_to_ftp import upload_to_ftp
from app.tasks.upload_to_google_drive import upload_to_google_drive
from app.tasks.upload_to_onedrive import upload_to_onedrive
from app.tasks.upload_to_s3 import upload_to_s3
from app.tasks.upload_to_sftp import upload_to_sftp
from app.tasks.upload_to_webdav import upload_to_webdav
_TEST_CREDENTIAL = "test_pass" # noqa: S105
@pytest.fixture
def mock_settings():
"""Mock settings for upload tests."""
with (
patch("app.tasks.upload_to_onedrive.settings") as onedrive_settings,
patch("app.tasks.upload_to_s3.settings") as s3_settings,
):
# OneDrive settings
onedrive_settings.onedrive_client_id = "test_client_id"
onedrive_settings.onedrive_client_secret = "test_secret"
onedrive_settings.onedrive_refresh_token = "test_refresh_token"
onedrive_settings.onedrive_folder_path = "test_folder"
onedrive_settings.onedrive_tenant_id = "common"
# S3 settings
s3_settings.s3_bucket_name = "test-bucket"
s3_settings.aws_access_key_id = "test_access_key"
s3_settings.aws_secret_access_key = "test_secret_key"
s3_settings.aws_region = "us-east-1"
s3_settings.s3_folder_prefix = "documents"
s3_settings.s3_storage_class = "STANDARD"
s3_settings.s3_acl = None
yield onedrive_settings, s3_settings
@pytest.mark.unit
def test_upload_to_onedrive_accepts_file_id(sample_text_file, mock_settings):
"""Test that upload_to_onedrive accepts file_id parameter."""
with (
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress"),
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
mock_upload.return_value = {"webUrl": "https://onedrive.test/file"}
# Call with file_id parameter using apply() to simulate task execution
# This bypasses Celery and calls the function directly
result = upload_to_onedrive.apply(args=[sample_text_file], kwargs={"file_id": 42}).get()
assert result["status"] == "Completed"
assert result["file_path"] == sample_text_file
assert "onedrive_path" in result
@pytest.mark.unit
def test_upload_to_onedrive_without_file_id(sample_text_file, mock_settings):
"""Test that upload_to_onedrive works without file_id parameter."""
with (
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress"),
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
mock_upload.return_value = {"webUrl": "https://onedrive.test/file"}
# Call without file_id parameter - should use default None
result = upload_to_onedrive.apply(args=[sample_text_file]).get()
assert result["status"] == "Completed"
assert result["file_path"] == sample_text_file
@pytest.mark.unit
def test_upload_to_s3_accepts_file_id(sample_text_file, mock_settings):
"""Test that upload_to_s3 accepts file_id parameter."""
with (
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress"),
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
mock_s3.upload_file.return_value = None
# Call with file_id parameter using apply()
result = upload_to_s3.apply(args=[sample_text_file], kwargs={"file_id": 99}).get()
assert result["status"] == "Completed"
assert result["file"] == sample_text_file
assert result["s3_bucket"] == "test-bucket"
assert "s3_key" in result
@pytest.mark.unit
def test_upload_to_s3_without_file_id(sample_text_file, mock_settings):
"""Test that upload_to_s3 works without file_id parameter."""
with (
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress"),
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
mock_s3.upload_file.return_value = None
# Call without file_id parameter using apply()
result = upload_to_s3.apply(args=[sample_text_file]).get()
assert result["status"] == "Completed"
assert result["file"] == sample_text_file
@pytest.mark.unit
def test_upload_to_onedrive_file_not_found(mock_settings):
"""Test that upload_to_onedrive raises error for missing file."""
with patch("app.tasks.upload_to_onedrive.log_task_progress"):
with pytest.raises(FileNotFoundError):
upload_to_onedrive.apply(args=["/nonexistent/file.pdf"], kwargs={"file_id": 1}).get()
@pytest.mark.unit
def test_upload_to_s3_file_not_found(mock_settings):
"""Test that upload_to_s3 raises error for missing file."""
with patch("app.tasks.upload_to_s3.log_task_progress"):
with pytest.raises(FileNotFoundError):
upload_to_s3.apply(args=["/nonexistent/file.pdf"], kwargs={"file_id": 1}).get()
@pytest.mark.unit
def test_upload_to_onedrive_logs_with_file_id(sample_text_file, mock_settings):
"""Test that upload_to_onedrive properly logs with file_id."""
with (
patch("app.tasks.upload_to_onedrive.get_onedrive_token") as mock_token,
patch("app.tasks.upload_to_onedrive.create_upload_session") as mock_session,
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress") as mock_log,
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
mock_upload.return_value = {"webUrl": "https://onedrive.test/file"}
# Call with file_id
upload_to_onedrive.apply(args=[sample_text_file], kwargs={"file_id": 123}).get()
# Verify log_task_progress was called with file_id
assert mock_log.called
# Check that at least one call included the file_id parameter
calls_with_file_id = [
call for call in mock_log.call_args_list if "file_id" in call[1] and call[1]["file_id"] == 123
]
assert len(calls_with_file_id) > 0
@pytest.mark.unit
def test_upload_to_s3_logs_with_file_id(sample_text_file, mock_settings):
"""Test that upload_to_s3 properly logs with file_id."""
with (
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress") as mock_log,
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
mock_s3.upload_file.return_value = None
# Call with file_id
upload_to_s3.apply(args=[sample_text_file], kwargs={"file_id": 456}).get()
# Verify log_task_progress was called with file_id
assert mock_log.called
# Check that at least one call included the file_id parameter
calls_with_file_id = [
call for call in mock_log.call_args_list if "file_id" in call[1] and call[1]["file_id"] == 456
]
assert len(calls_with_file_id) > 0
# Tests for newly standardized upload tasks
@pytest.mark.unit
def test_upload_to_ftp_accepts_file_id(sample_text_file):
"""Test that upload_to_ftp accepts file_id parameter."""
with (
patch("app.tasks.upload_to_ftp.settings") as mock_settings,
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
patch("app.tasks.upload_to_ftp.log_task_progress"),
):
# Setup settings
mock_settings.ftp_host = "ftp.example.com"
mock_settings.ftp_port = 21
mock_settings.ftp_username = "test_user"
mock_settings.ftp_password = _TEST_CREDENTIAL
mock_settings.ftp_folder = "uploads"
mock_settings.ftp_use_tls = False
mock_settings.ftp_allow_plaintext = True
# Setup mock FTP
mock_ftp_instance = Mock()
mock_ftp.return_value = mock_ftp_instance
# Call with file_id parameter
result = upload_to_ftp.apply(args=[sample_text_file], kwargs={"file_id": 100}).get()
assert result["status"] == "Completed"
assert result["file"] == sample_text_file
assert result["ftp_host"] == "ftp.example.com"
@pytest.mark.unit
def test_upload_to_ftp_without_file_id(sample_text_file):
"""Test that upload_to_ftp works without file_id parameter."""
with (
patch("app.tasks.upload_to_ftp.settings") as mock_settings,
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
patch("app.tasks.upload_to_ftp.log_task_progress"),
):
# Setup settings
mock_settings.ftp_host = "ftp.example.com"
mock_settings.ftp_username = "test_user"
mock_settings.ftp_password = _TEST_CREDENTIAL
mock_settings.ftp_folder = None
mock_settings.ftp_use_tls = False
mock_settings.ftp_allow_plaintext = True
# Setup mock FTP
mock_ftp_instance = Mock()
mock_ftp.return_value = mock_ftp_instance
# Call without file_id parameter
result = upload_to_ftp.apply(args=[sample_text_file]).get()
assert result["status"] == "Completed"
@pytest.mark.unit
def test_upload_to_sftp_accepts_file_id(sample_text_file):
"""Test that upload_to_sftp accepts file_id parameter."""
with (
patch("app.tasks.upload_to_sftp.settings") as mock_settings,
patch("app.tasks.upload_to_sftp.paramiko.SSHClient") as mock_ssh,
patch("app.tasks.upload_to_sftp.log_task_progress"),
patch("app.tasks.upload_to_sftp.extract_remote_path") as mock_extract,
patch("app.tasks.upload_to_sftp.get_unique_filename") as mock_unique,
):
# Setup settings
mock_settings.sftp_host = "sftp.example.com"
mock_settings.sftp_port = 22
mock_settings.sftp_username = "test_user"
mock_settings.sftp_password = _TEST_CREDENTIAL
mock_settings.sftp_folder = "/uploads"
mock_settings.workdir = "/tmp"
# Setup mocks
mock_ssh_instance = Mock()
mock_sftp = Mock()
mock_ssh.return_value = mock_ssh_instance
mock_ssh_instance.open_sftp.return_value = mock_sftp
mock_extract.return_value = "/uploads/test.txt"
mock_unique.return_value = "/uploads/test.txt"
# Call with file_id parameter
result = upload_to_sftp.apply(args=[sample_text_file], kwargs={"file_id": 200}).get()
assert result["status"] == "Completed"
assert result["file_path"] == sample_text_file
assert "sftp_path" in result
@pytest.mark.unit
def test_upload_to_webdav_accepts_file_id(sample_text_file):
"""Test that upload_to_webdav accepts file_id parameter."""
with (
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"),
):
# Setup settings
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
mock_settings.webdav_folder = "uploads"
mock_settings.webdav_verify_ssl = True
# Setup mock response
mock_response = Mock()
mock_response.status_code = 201
mock_put.return_value = mock_response
# Call with file_id parameter
result = upload_to_webdav.apply(args=[sample_text_file], kwargs={"file_id": 300}).get()
assert result["status"] == "Completed"
assert result["file"] == sample_text_file
assert "url" in result
@pytest.mark.unit
def test_upload_to_google_drive_accepts_file_id(sample_text_file):
"""Test that upload_to_google_drive accepts file_id parameter."""
with (
patch("app.tasks.upload_to_google_drive.get_google_drive_service") as mock_service,
patch("app.tasks.upload_to_google_drive.MediaFileUpload"),
patch("app.tasks.upload_to_google_drive.extract_metadata_from_file") as mock_metadata,
patch("app.tasks.upload_to_google_drive.settings") as mock_settings,
patch("app.tasks.upload_to_google_drive.log_task_progress"),
):
# Setup settings
mock_settings.google_drive_folder_id = "test_folder_id"
# Setup mocks
mock_drive_service = Mock()
mock_service.return_value = mock_drive_service
mock_metadata.return_value = {}
mock_files = Mock()
mock_drive_service.files.return_value = mock_files
mock_create = Mock()
mock_files.create.return_value = mock_create
mock_create.execute.return_value = {
"id": "file123",
"name": "test.txt",
"webViewLink": "https://drive.google.com/file/d/file123",
}
# Call with file_id parameter
result = upload_to_google_drive.apply(args=[sample_text_file], kwargs={"file_id": 400}).get()
assert result["status"] == "Completed"
assert result["file_path"] == sample_text_file
assert "google_drive_file_id" in result
@pytest.mark.unit
def test_upload_to_email_accepts_file_id(sample_text_file):
"""Test that upload_to_email accepts file_id parameter."""
with (
patch("app.tasks.upload_to_email.settings") as mock_settings,
patch("app.tasks.upload_to_email.smtplib.SMTP"),
patch("app.tasks.upload_to_email.get_email_template") as mock_template,
patch("app.tasks.upload_to_email.extract_metadata_from_file") as mock_metadata,
patch("app.tasks.upload_to_email.log_task_progress"),
patch("app.tasks.upload_to_email._prepare_recipients") as mock_recipients,
patch("app.tasks.upload_to_email._send_email_with_smtp") as mock_send,
patch("app.tasks.upload_to_email.attach_logo") as mock_logo,
):
# Setup settings
mock_settings.dest_email_host = "smtp.example.com"
mock_settings.dest_email_port = 587
mock_settings.dest_email_username = "test@example.com"
mock_settings.dest_email_password = _TEST_CREDENTIAL
mock_settings.dest_email_use_tls = True
mock_settings.dest_email_sender = "sender@example.com"
mock_settings.external_hostname = "docuelevate.example.com"
# Setup mocks
mock_recipients.return_value = (["recipient@example.com"], None)
mock_send.return_value = None
mock_metadata.return_value = {}
mock_logo.return_value = False
mock_template_obj = Mock()
mock_template_obj.render.return_value = "<html>Test email</html>"
mock_template.return_value = mock_template_obj
# Call with file_id parameter
result = upload_to_email.apply(args=[sample_text_file], kwargs={"file_id": 500}).get()
assert result["status"] == "Completed"
assert result["file"] == sample_text_file
assert "recipients" in result
@pytest.mark.unit
def test_upload_to_ftp_file_not_found():
"""Test that upload_to_ftp raises error for missing file."""
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, patch("app.tasks.upload_to_ftp.log_task_progress"):
mock_settings.ftp_host = "ftp.example.com"
with pytest.raises(FileNotFoundError):
upload_to_ftp.apply(args=["/nonexistent/file.pdf"], kwargs={"file_id": 1}).get()
@pytest.mark.unit
def test_upload_to_sftp_file_not_found():
"""Test that upload_to_sftp raises error for missing file."""
with (
patch("app.tasks.upload_to_sftp.settings") as mock_settings,
patch("app.tasks.upload_to_sftp.log_task_progress"),
):
mock_settings.sftp_host = "sftp.example.com"
mock_settings.sftp_port = 22
mock_settings.sftp_username = "test_user"
with pytest.raises(FileNotFoundError):
upload_to_sftp.apply(args=["/nonexistent/file.pdf"], kwargs={"file_id": 1}).get()
@pytest.mark.unit
def test_upload_to_webdav_file_not_found():
"""Test that upload_to_webdav raises error for missing file."""
with (
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
with pytest.raises(FileNotFoundError):
upload_to_webdav.apply(args=["/nonexistent/file.pdf"], kwargs={"file_id": 1}).get()
@pytest.mark.unit
def test_all_upload_tasks_have_consistent_signature(sample_text_file):
"""Test that all upload tasks accept file_id as a keyword parameter.
This test verifies that all upload tasks can be called with the same signature
as used in send_to_all.py: task.delay(file_path, file_id=file_id)
Note: We use task.run to inspect the actual function signature because
Celery tasks wrap the original function, and .run provides access to
the unwrapped callable's signature.
"""
upload_tasks = [
(upload_to_s3, "app.tasks.upload_to_s3"),
(upload_to_ftp, "app.tasks.upload_to_ftp"),
(upload_to_sftp, "app.tasks.upload_to_sftp"),
(upload_to_webdav, "app.tasks.upload_to_webdav"),
(upload_to_google_drive, "app.tasks.upload_to_google_drive"),
(upload_to_email, "app.tasks.upload_to_email"),
]
import inspect
for task, module_path in upload_tasks:
# Use task.run to inspect the actual wrapped function's signature
# This is necessary because Celery's task decorator wraps the original function
sig = inspect.signature(task.run)
params = list(sig.parameters.keys())
# Should have at least file_path and file_id parameters
assert "file_path" in params, f"{task.name} missing file_path parameter"
assert "file_id" in params, f"{task.name} missing file_id parameter"
# file_id should have a default value (None)
assert sig.parameters["file_id"].default is None, f"{task.name} file_id should default to None"
@pytest.mark.unit
def test_send_to_all_calls_upload_tasks_with_keyword_argument():
"""Test that send_to_all_destinations calls upload tasks with file_id as keyword argument.
Regression test for issue: upload_to_s3() takes 1 positional argument but 2 were given.
This ensures that file_id is always passed as a keyword argument, not positional.
"""
from app.tasks.send_to_all import send_to_all_destinations
test_file = "/tmp/test_file.pdf"
# Create the test file
with open(test_file, "w") as f:
f.write("test content")
try:
# Mock all the upload functions and settings
with (
patch("app.tasks.send_to_all.upload_to_s3") as mock_s3,
patch("app.tasks.send_to_all.settings") as mock_settings,
patch("app.tasks.send_to_all.log_task_progress"),
patch("app.tasks.send_to_all.SessionLocal"),
patch("app.tasks.send_to_all.get_configured_services_from_validator") as mock_validator,
):
# Configure validator to return S3 as configured
mock_validator.return_value = {"s3": True}
# Configure settings to enable only S3
mock_settings.s3_bucket_name = "test-bucket"
mock_settings.aws_access_key_id = "test-key"
mock_settings.aws_secret_access_key = "test-secret"
mock_settings.dropbox_app_key = None
mock_settings.nextcloud_upload_url = None
mock_settings.paperless_ngx_api_token = None
mock_settings.google_drive_credentials_json = None
mock_settings.webdav_url = None
mock_settings.ftp_host = None
mock_settings.sftp_host = None
mock_settings.dest_email_host = None
mock_settings.onedrive_client_id = None
mock_settings.workdir = "/tmp"
# Mock the delay method to track how it's called
mock_s3_task = Mock()
mock_s3_task.id = "test-task-id"
mock_s3.delay.return_value = mock_s3_task
# Call send_to_all_destinations with file_id
result = send_to_all_destinations.apply(args=[test_file], kwargs={"file_id": 123}).get()
# Verify that upload_to_s3.delay was called with file_id as keyword argument
mock_s3.delay.assert_called_once()
call_args, call_kwargs = mock_s3.delay.call_args
# The call should be: delay(file_path, file_id=file_id)
# So we expect 1 positional arg (file_path) and file_id in kwargs
assert len(call_args) == 1, "Should have exactly 1 positional argument (file_path)"
assert call_args[0] == test_file, "First positional arg should be file_path"
assert "file_id" in call_kwargs, "file_id should be passed as keyword argument"
assert call_kwargs["file_id"] == 123, "file_id value should be correct"
finally:
# Clean up test file
if os.path.exists(test_file):
os.remove(test_file)