Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "bitssh"
version = "3.4.0"
version = "3.5.0"
description = "A new and modern SSH connector written in Python."
readme = "README.md"
authors = [
Expand Down Expand Up @@ -45,7 +45,7 @@ Issues = "https://github.com/Mr-Sunglasses/bitssh/issues"
bitssh = "bitssh.__main__:main"

[tool.bumpver]
current_version = "3.4.0"
current_version = "3.5.0"
version_pattern = "MAJOR.MINOR.PATCH"
commit_message = "Bump version {old_version} -> {new_version}"
commit = true
Expand Down
2 changes: 1 addition & 1 deletion src/bitssh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
pass


__version__ = "3.4.0"
__version__ = "3.5.0"
4 changes: 4 additions & 0 deletions src/bitssh/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def get_config_content():
for i, match in enumerate(host_matches):
host = match.group(1)

# Skip wildcard entries (*, *.example.com, etc.) as they're not connection targets
if "*" in host:
continue

# Find the end of this host section (start of next host or end of content)
if i + 1 < len(host_matches):
host_section_end = host_matches[i + 1].start()
Expand Down
218 changes: 186 additions & 32 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def test_config_incomplete_entries(self, mock_exists) -> None:
with patch("builtins.open", mock_open(read_data=self.mock_config_incomplete)):
result = get_config_content()

expected: Dict[str, Dict[str, str]] = {
expected = {
"complete_host": {
"Hostname": "complete.example.com",
"User": "complete_user",
Expand Down Expand Up @@ -484,37 +484,6 @@ def test_config_with_other_directives(self, mock_exists) -> None:
}
self.assertEqual(result, expected)

@patch("os.path.exists", return_value=True)
def test_config_with_wildcards(self, mock_exists) -> None:
"""Test parsing config with wildcard hosts"""
wildcard_config = """
Host *.example.com
User wildcard_user
Port 3030

Host specific.example.com
HostName specific.example.com
User specific_user
Port 4040

Host *
User default_user
"""
with patch("builtins.open", mock_open(read_data=wildcard_config)):
result = get_config_content()

# Check for the actual key names that should be parsed
self.assertIn("*.example.com", result)
self.assertIn("specific.example.com", result) # Fixed: use full hostname
self.assertIn("*", result)

# Verify the parsed values
self.assertEqual(result["*.example.com"]["User"], "wildcard_user")
self.assertEqual(result["*.example.com"]["Port"], "3030")
self.assertEqual(result["specific.example.com"]["User"], "specific_user")
self.assertEqual(result["specific.example.com"]["Port"], "4040")
self.assertEqual(result["*"]["User"], "default_user")

@patch("os.path.exists", return_value=True)
def test_large_config_performance(self, mock_exists) -> None:
"""Test parsing a large config file"""
Expand Down Expand Up @@ -660,3 +629,188 @@ def test_debug_incomplete_actual_behavior(self, mock_exists) -> None:
else:
# There might be cross-contamination between sections
print("Potential cross-contamination detected")

@patch("os.path.exists", return_value=True)
def test_basic_host_parsing(self, mock_exists):
"""Test parsing a basic SSH config with hyphenated host names"""
config = """
Host server-one
HostName server-one.example.com
User username
Port 22

Host web-server
HostName web.example.com
User webuser
Port 2222
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

self.assertIn("server-one", result)
self.assertIn("web-server", result)

# Check server-one details
self.assertEqual(result["server-one"]["Hostname"], "server-one.example.com")
self.assertEqual(result["server-one"]["User"], "username")
self.assertEqual(result["server-one"]["Port"], "22")

# Check web-server details
self.assertEqual(result["web-server"]["Hostname"], "web.example.com")
self.assertEqual(result["web-server"]["User"], "webuser")
self.assertEqual(result["web-server"]["Port"], "2222")

@patch("os.path.exists", return_value=True)
def test_wildcard_filtering(self, mock_exists):
"""Test that wildcard hosts are filtered out"""
config = """
Host *
Compression yes
User default_user

Host server-one
HostName server-one.example.com
User username
Port 22

Host *.example.com
User wildcard_user
Port 3030
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

# Should only contain non-wildcard hosts
self.assertIn("server-one", result)
self.assertNotIn("*", result)
self.assertNotIn("*.example.com", result)

# Should have only one entry
self.assertEqual(len(result), 1)

# Check that server-one is parsed correctly
self.assertEqual(result["server-one"]["Hostname"], "server-one.example.com")
self.assertEqual(result["server-one"]["User"], "username")
self.assertEqual(result["server-one"]["Port"], "22")

@patch("os.path.exists", return_value=True)
def test_mixed_wildcard_and_specific_hosts(self, mock_exists):
"""Test parsing config with mix of wildcard and specific hosts"""
config = """
Host *
Compression yes

Host *.dev
User dev_user

Host server-one
HostName server-one.example.com
User username
Port 22

Host db-server
HostName db.example.com
User dbuser

Host *.test.com
User test_user
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

# Should only contain specific hosts, no wildcards
expected_hosts = {"server-one", "db-server"}
actual_hosts = set(result.keys())
self.assertEqual(actual_hosts, expected_hosts)

# Verify no wildcard entries
for host in result.keys():
self.assertNotIn("*", host)

@patch("os.path.exists", return_value=True)
def test_complex_hostnames(self, mock_exists):
"""Test parsing hosts with various character patterns"""
config = """
Host my-server.prod
HostName my-server.prod.example.com
User prod_user
Port 443

Host test_server_01
HostName 192.168.1.100
User testuser
Port 2222

Host staging-web-01
HostName staging-web-01.internal
User deploy
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

expected_hosts = {"my-server.prod", "test_server_01", "staging-web-01"}
actual_hosts = set(result.keys())
self.assertEqual(actual_hosts, expected_hosts)

# Check specific details
self.assertEqual(result["my-server.prod"]["Port"], "443")
self.assertEqual(result["test_server_01"]["Hostname"], "192.168.1.100")
self.assertEqual(result["staging-web-01"]["User"], "deploy")

@patch("os.path.exists", return_value=True)
def test_commented_lines_ignored(self, mock_exists):
"""Test that commented lines are properly ignored"""
config = """
# Global settings
Host *
Compression yes

# Production server
Host server-one
HostName server-one.example.com
User username
# Port 2222 # Commented out port
Port 22

# Host commented-out-server
# HostName should-not-appear.com
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

# Should only have server-one
self.assertEqual(list(result.keys()), ["server-one"])
self.assertEqual(
result["server-one"]["Port"], "22"
) # Should use uncommented port

@patch("os.path.exists", return_value=True)
def test_default_values(self, mock_exists):
"""Test default values are applied correctly"""
config = """
Host minimal-server
HostName minimal.example.com
# No user or port specified

Host partial-server
User partial_user
# No hostname or port specified
"""
with patch("builtins.open", mock_open(read_data=config)):
result = get_config_content()

# Check minimal-server defaults
self.assertEqual(
result["minimal-server"]["Hostname"], "minimal.example.com"
)
self.assertEqual(
result["minimal-server"]["User"], None
) # No user specified
self.assertEqual(result["minimal-server"]["Port"], "22") # Default port

# Check partial-server defaults
self.assertEqual(
result["partial-server"]["Hostname"], "partial-server"
) # Defaults to host name
self.assertEqual(result["partial-server"]["User"], "partial_user")
self.assertEqual(result["partial-server"]["Port"], "22") # Default port