"""Additional tests for upload_to_ftp task.""" import ftplib from unittest.mock import Mock, patch import pytest from app.tasks.upload_to_ftp import upload_to_ftp @pytest.mark.unit class TestUploadToFtp: """Tests for upload_to_ftp task.""" def test_module_imports(self): """Test that the module can be imported.""" from app.tasks.upload_to_ftp import upload_to_ftp assert callable(upload_to_ftp) @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_uploads_file_with_ftps(self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename): """Test uploads file using FTPS (FTP with TLS).""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = "/uploads" mock_settings.ftp_use_tls = True mock_settings.ftp_allow_plaintext = True mock_ftp = Mock() mock_ftp_tls.return_value = mock_ftp mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_ftp(mock_self, "/tmp/test.pdf") assert result["status"] == "Completed" assert result["used_tls"] is True mock_ftp.login.assert_called_once() mock_ftp.prot_p.assert_called_once() @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_falls_back_to_plaintext_ftp( self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_ftp, mock_basename ): """Test falls back to plaintext FTP when FTPS fails.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = None mock_settings.ftp_use_tls = True mock_settings.ftp_allow_plaintext = True # FTPS fails mock_ftp_tls_instance = Mock() mock_ftp_tls_instance.connect.side_effect = Exception("TLS not supported") mock_ftp_tls.return_value = mock_ftp_tls_instance # Plaintext FTP succeeds mock_ftp_instance = Mock() mock_ftp.return_value = mock_ftp_instance mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_ftp(mock_self, "/tmp/test.pdf") assert result["status"] == "Completed" assert result["used_tls"] is False @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") def test_raises_error_when_ftps_fails_and_plaintext_forbidden( self, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename ): """Test raises error when FTPS fails and plaintext is forbidden.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = None mock_settings.ftp_use_tls = True mock_settings.ftp_allow_plaintext = False mock_ftp_tls_instance = Mock() mock_ftp_tls_instance.connect.side_effect = Exception("TLS not supported") mock_ftp_tls.return_value = mock_ftp_tls_instance mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(Exception, match="FTPS connection failed and plaintext FTP is forbidden"): upload_to_ftp(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") def test_raises_error_when_file_not_found(self, mock_exists, mock_log, mock_basename): """Test raises error when file not found.""" mock_exists.return_value = False mock_basename.return_value = "file.pdf" mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(FileNotFoundError): upload_to_ftp(mock_self, "/nonexistent/file.pdf") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") def test_raises_error_when_ftp_host_not_configured(self, mock_settings, mock_exists, mock_log, mock_basename): """Test raises error when FTP host not configured.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = None mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(ValueError, match="FTP host is not configured"): upload_to_ftp(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_creates_directory_structure( self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename ): """Test creates directory structure if it doesn't exist.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = "/uploads/documents" mock_settings.ftp_use_tls = True mock_ftp = Mock() # First cwd fails (trying full path), then for each subfolder: fail then succeed after mkd mock_ftp.cwd.side_effect = [ ftplib.error_perm("No such directory"), # noqa: S321 # Initial try for full path ftplib.error_perm("No such directory"), # noqa: S321 # /uploads doesn't exist None, # /uploads now exists after mkd ftplib.error_perm("No such directory"), # noqa: S321 # /uploads/documents doesn't exist None, # /uploads/documents now exists after mkd ] mock_ftp_tls.return_value = mock_ftp mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_ftp(mock_self, "/tmp/test.pdf") assert result["status"] == "Completed" mock_ftp.mkd.assert_called() @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_uses_plaintext_ftp_when_tls_disabled( self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp, mock_basename ): """Test uses plaintext FTP when TLS is explicitly disabled.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = None mock_settings.ftp_use_tls = False mock_settings.ftp_allow_plaintext = True mock_ftp_instance = Mock() mock_ftp.return_value = mock_ftp_instance mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_ftp(mock_self, "/tmp/test.pdf") assert result["status"] == "Completed" assert result["used_tls"] is False @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") def test_raises_error_when_plaintext_forbidden_and_tls_disabled( self, mock_settings, mock_exists, mock_log, mock_basename ): """Test raises error when plaintext is forbidden and TLS is disabled.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_use_tls = False mock_settings.ftp_allow_plaintext = False mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(Exception, match="Plaintext FTP is forbidden"): upload_to_ftp(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_removes_leading_slash_from_folder( self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename ): """Test removes leading slash from folder path.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = "/uploads" mock_settings.ftp_use_tls = True mock_ftp = Mock() mock_ftp_tls.return_value = mock_ftp mock_self = Mock() mock_self.request.id = "test-task-id" upload_to_ftp(mock_self, "/tmp/test.pdf") # Verify cwd was called with folder without leading slash mock_ftp.cwd.assert_called_with("uploads") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") def test_handles_directory_creation_error(self, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename): """Test handles directory creation error.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = "/uploads" mock_settings.ftp_use_tls = True mock_ftp = Mock() mock_ftp.cwd.side_effect = ftplib.error_perm("Permission denied") # noqa: S321 mock_ftp.mkd.side_effect = ftplib.error_perm("Cannot create directory") # noqa: S321 mock_ftp_tls.return_value = mock_ftp mock_self = Mock() mock_self.request.id = "test-task-id" with pytest.raises(Exception, match="Failed to change/create directory"): upload_to_ftp(mock_self, "/tmp/test.pdf") @patch("app.tasks.upload_to_ftp.os.path.basename") @patch("app.tasks.upload_to_ftp.ftplib.FTP_TLS") @patch("app.tasks.upload_to_ftp.log_task_progress") @patch("app.tasks.upload_to_ftp.os.path.exists") @patch("app.tasks.upload_to_ftp.settings") @patch("builtins.open", create=True) def test_returns_ftp_path_in_result( self, mock_open, mock_settings, mock_exists, mock_log, mock_ftp_tls, mock_basename ): """Test returns FTP path in result.""" mock_exists.return_value = True mock_basename.return_value = "test.pdf" mock_settings.ftp_host = "ftp.example.com" mock_settings.ftp_port = 21 mock_settings.ftp_username = "user" mock_settings.ftp_password = "password" mock_settings.ftp_folder = "/uploads" mock_settings.ftp_use_tls = True mock_ftp = Mock() mock_ftp_tls.return_value = mock_ftp mock_self = Mock() mock_self.request.id = "test-task-id" result = upload_to_ftp(mock_self, "/tmp/test.pdf") assert "ftp_path" in result assert result["ftp_path"] == "/uploads/test.pdf"