This is an automated email from the ASF dual-hosted git repository.

Miretpl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ce8bc76baa9 Fix FTP test connection passing without contacting the 
server (#76355)
ce8bc76baa9 is described below

commit ce8bc76baa98ff435fa10ef0f3c584ab6a697f25
Author: Pavan Nandi <[email protected]>
AuthorDate: Tue Nov 10 03:36:21 2026 +0530

    Fix FTP test connection passing without contacting the server (#76355)
    
    FTPHook.test_connection accessed conn.pwd without calling it, so the test
    never sent a command to the server. Because get_conn skips connect() when
    the connection has no host and skips login() when it has no login, the
    UI reported "Connection successfully tested" for FTP connections that
    could not work: one without a host, and one missing the login a server
    requires. PWD needs a logged-in session on most servers, unlike NOOP,
    which is what makes it a meaningful check. A connection without a host
    now gets a readable message instead of ftplib's AttributeError.
---
 .../ftp/src/airflow/providers/ftp/hooks/ftp.py     |  8 +++---
 providers/ftp/tests/unit/ftp/hooks/test_ftp.py     | 30 +++++++++++++++++++++-
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py 
b/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
index 2812681475b..28a04e17d46 100644
--- a/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
+++ b/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
@@ -297,10 +297,12 @@ class FTPHook(BaseHook):
         return int(size) if size is not None else None
 
     def test_connection(self) -> tuple[bool, str]:
-        """Test the FTP connection by calling path with directory."""
+        """Test the FTP connection by asking the server for the current 
directory."""
         try:
-            conn = self.get_conn()
-            conn.pwd
+            if not self.get_connection(self.ftp_conn_id).host:
+                return False, "Connection has no host to connect to"
+            # PWD needs a logged-in session on most servers; NOOP would not 
catch a missing login.
+            self.get_conn().pwd()
             return True, "Connection successfully tested"
         except Exception as e:
             return False, str(e)
diff --git a/providers/ftp/tests/unit/ftp/hooks/test_ftp.py 
b/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
index 079c46f6c3a..d58796b91ff 100644
--- a/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
+++ b/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
@@ -17,7 +17,7 @@
 # under the License.
 from __future__ import annotations
 
-from ftplib import FTP_TLS
+from ftplib import FTP_TLS, error_perm
 from io import StringIO
 from unittest import mock
 
@@ -41,6 +41,12 @@ class TestFTPHook:
     def teardown_method(self):
         fh.FTPHook.get_conn = self.get_conn_orig
 
+    @pytest.fixture
+    def ftp_default(self, create_connection_without_db):
+        from airflow.models import Connection
+
+        create_connection_without_db(Connection(conn_id="ftp_default", 
conn_type="ftp", host="localhost"))
+
     def test_close_conn(self):
         ftp_hook = fh.FTPHook()
         ftp_hook.get_conn()
@@ -146,12 +152,34 @@ class TestFTPHook:
                     ftp_hook.store_file(self.path, "/local/path")
         handle.close.assert_called_once_with()
 
+    @pytest.mark.usefixtures("ftp_default")
     def test_connection_success(self):
         with fh.FTPHook() as ftp_hook:
             status, msg = ftp_hook.test_connection()
             assert status is True
             assert msg == "Connection successfully tested"
+        self.conn_mock.pwd.assert_called_once_with()
+
+    @pytest.mark.usefixtures("ftp_default")
+    def test_connection_failure_when_server_rejects_command(self):
+        self.conn_mock.pwd.side_effect = error_perm("530 Log in with USER and 
PASS first.")
+        with fh.FTPHook() as ftp_hook:
+            status, msg = ftp_hook.test_connection()
+            assert status is False
+            assert msg == "530 Log in with USER and PASS first."
+
+    def test_connection_failure_without_host(self, 
create_connection_without_db):
+        from airflow.models import Connection
+
+        create_connection_without_db(Connection(conn_id="ftp_no_host", 
conn_type="ftp"))
+
+        status, msg = fh.FTPHook(ftp_conn_id="ftp_no_host").test_connection()
+
+        assert status is False
+        assert msg == "Connection has no host to connect to"
+        self.conn_mock.pwd.assert_not_called()
 
+    @pytest.mark.usefixtures("ftp_default")
     def test_connection_failure(self):
         self.conn_mock = mock.MagicMock(name="conn_mock", 
side_effect=Exception("Test"))
         fh.FTPHook.get_conn = self.conn_mock

Reply via email to