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