diff --git a/redis/cluster.py b/redis/cluster.py index 171e586efb..6862554898 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -2838,7 +2838,8 @@ def disconnect(self): if self.connection: self.connection.disconnect() for pubsub in self.node_pubsub_mapping.values(): - pubsub.connection.disconnect() + if pubsub.connection: + pubsub.connection.disconnect() class ClusterPipeline(RedisCluster): diff --git a/tests/test_cluster.py b/tests/test_cluster.py index b8537aad8a..b74089d3f1 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -3289,6 +3289,48 @@ def test_get_redis_connection(self, r): p = r.pubsub(node=node) assert p.get_redis_connection() == node.redis_connection + def test_disconnect_with_none_connections(self, r): + """ + Test that disconnect() does not raise when pubsub.connection is None, + both on the ClusterPubSub itself and on node pubsub instances in + node_pubsub_mapping. + """ + node = r.get_default_node() + p = r.pubsub(node=node) + # Ensure self.connection is None + p.connection = None + + # Add a mock pubsub with connection=None into node_pubsub_mapping + mock_pubsub = Mock() + mock_pubsub.connection = None + p.node_pubsub_mapping["fake_node"] = mock_pubsub + + # Should not raise AttributeError + p.disconnect() + + def test_disconnect_calls_disconnect_on_existing_connections(self, r): + """ + Test that disconnect() properly disconnects non-None connections + on both self and node_pubsub_mapping entries. + """ + node = r.get_default_node() + p = r.pubsub(node=node) + + # Mock self.connection + mock_conn = Mock() + p.connection = mock_conn + + # Add a mock pubsub with a real connection into node_pubsub_mapping + mock_node_conn = Mock() + mock_node_pubsub = Mock() + mock_node_pubsub.connection = mock_node_conn + p.node_pubsub_mapping["fake_node"] = mock_node_pubsub + + p.disconnect() + + mock_conn.disconnect.assert_called_once() + mock_node_conn.disconnect.assert_called_once() + @pytest.mark.onlycluster class TestClusterPipeline: