Skip to content

Commit 25b8e76

Browse files
Merge pull request #37 from Mr-Sunglasses/fix/wildcard-case
2 parents 9a279c0 + 36ad6cb commit 25b8e76

4 files changed

Lines changed: 193 additions & 35 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

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

4747
[tool.bumpver]
48-
current_version = "3.4.0"
48+
current_version = "3.5.0"
4949
version_pattern = "MAJOR.MINOR.PATCH"
5050
commit_message = "Bump version {old_version} -> {new_version}"
5151
commit = true

src/bitssh/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
pass
66

77

8-
__version__ = "3.4.0"
8+
__version__ = "3.5.0"

src/bitssh/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def get_config_content():
4444
for i, match in enumerate(host_matches):
4545
host = match.group(1)
4646

47+
# Skip wildcard entries (*, *.example.com, etc.) as they're not connection targets
48+
if "*" in host:
49+
continue
50+
4751
# Find the end of this host section (start of next host or end of content)
4852
if i + 1 < len(host_matches):
4953
host_section_end = host_matches[i + 1].start()

tests/test_utils.py

Lines changed: 186 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def test_config_incomplete_entries(self, mock_exists) -> None:
224224
with patch("builtins.open", mock_open(read_data=self.mock_config_incomplete)):
225225
result = get_config_content()
226226

227-
expected: Dict[str, Dict[str, str]] = {
227+
expected = {
228228
"complete_host": {
229229
"Hostname": "complete.example.com",
230230
"User": "complete_user",
@@ -484,37 +484,6 @@ def test_config_with_other_directives(self, mock_exists) -> None:
484484
}
485485
self.assertEqual(result, expected)
486486

487-
@patch("os.path.exists", return_value=True)
488-
def test_config_with_wildcards(self, mock_exists) -> None:
489-
"""Test parsing config with wildcard hosts"""
490-
wildcard_config = """
491-
Host *.example.com
492-
User wildcard_user
493-
Port 3030
494-
495-
Host specific.example.com
496-
HostName specific.example.com
497-
User specific_user
498-
Port 4040
499-
500-
Host *
501-
User default_user
502-
"""
503-
with patch("builtins.open", mock_open(read_data=wildcard_config)):
504-
result = get_config_content()
505-
506-
# Check for the actual key names that should be parsed
507-
self.assertIn("*.example.com", result)
508-
self.assertIn("specific.example.com", result) # Fixed: use full hostname
509-
self.assertIn("*", result)
510-
511-
# Verify the parsed values
512-
self.assertEqual(result["*.example.com"]["User"], "wildcard_user")
513-
self.assertEqual(result["*.example.com"]["Port"], "3030")
514-
self.assertEqual(result["specific.example.com"]["User"], "specific_user")
515-
self.assertEqual(result["specific.example.com"]["Port"], "4040")
516-
self.assertEqual(result["*"]["User"], "default_user")
517-
518487
@patch("os.path.exists", return_value=True)
519488
def test_large_config_performance(self, mock_exists) -> None:
520489
"""Test parsing a large config file"""
@@ -660,3 +629,188 @@ def test_debug_incomplete_actual_behavior(self, mock_exists) -> None:
660629
else:
661630
# There might be cross-contamination between sections
662631
print("Potential cross-contamination detected")
632+
633+
@patch("os.path.exists", return_value=True)
634+
def test_basic_host_parsing(self, mock_exists):
635+
"""Test parsing a basic SSH config with hyphenated host names"""
636+
config = """
637+
Host server-one
638+
HostName server-one.example.com
639+
User username
640+
Port 22
641+
642+
Host web-server
643+
HostName web.example.com
644+
User webuser
645+
Port 2222
646+
"""
647+
with patch("builtins.open", mock_open(read_data=config)):
648+
result = get_config_content()
649+
650+
self.assertIn("server-one", result)
651+
self.assertIn("web-server", result)
652+
653+
# Check server-one details
654+
self.assertEqual(result["server-one"]["Hostname"], "server-one.example.com")
655+
self.assertEqual(result["server-one"]["User"], "username")
656+
self.assertEqual(result["server-one"]["Port"], "22")
657+
658+
# Check web-server details
659+
self.assertEqual(result["web-server"]["Hostname"], "web.example.com")
660+
self.assertEqual(result["web-server"]["User"], "webuser")
661+
self.assertEqual(result["web-server"]["Port"], "2222")
662+
663+
@patch("os.path.exists", return_value=True)
664+
def test_wildcard_filtering(self, mock_exists):
665+
"""Test that wildcard hosts are filtered out"""
666+
config = """
667+
Host *
668+
Compression yes
669+
User default_user
670+
671+
Host server-one
672+
HostName server-one.example.com
673+
User username
674+
Port 22
675+
676+
Host *.example.com
677+
User wildcard_user
678+
Port 3030
679+
"""
680+
with patch("builtins.open", mock_open(read_data=config)):
681+
result = get_config_content()
682+
683+
# Should only contain non-wildcard hosts
684+
self.assertIn("server-one", result)
685+
self.assertNotIn("*", result)
686+
self.assertNotIn("*.example.com", result)
687+
688+
# Should have only one entry
689+
self.assertEqual(len(result), 1)
690+
691+
# Check that server-one is parsed correctly
692+
self.assertEqual(result["server-one"]["Hostname"], "server-one.example.com")
693+
self.assertEqual(result["server-one"]["User"], "username")
694+
self.assertEqual(result["server-one"]["Port"], "22")
695+
696+
@patch("os.path.exists", return_value=True)
697+
def test_mixed_wildcard_and_specific_hosts(self, mock_exists):
698+
"""Test parsing config with mix of wildcard and specific hosts"""
699+
config = """
700+
Host *
701+
Compression yes
702+
703+
Host *.dev
704+
User dev_user
705+
706+
Host server-one
707+
HostName server-one.example.com
708+
User username
709+
Port 22
710+
711+
Host db-server
712+
HostName db.example.com
713+
User dbuser
714+
715+
Host *.test.com
716+
User test_user
717+
"""
718+
with patch("builtins.open", mock_open(read_data=config)):
719+
result = get_config_content()
720+
721+
# Should only contain specific hosts, no wildcards
722+
expected_hosts = {"server-one", "db-server"}
723+
actual_hosts = set(result.keys())
724+
self.assertEqual(actual_hosts, expected_hosts)
725+
726+
# Verify no wildcard entries
727+
for host in result.keys():
728+
self.assertNotIn("*", host)
729+
730+
@patch("os.path.exists", return_value=True)
731+
def test_complex_hostnames(self, mock_exists):
732+
"""Test parsing hosts with various character patterns"""
733+
config = """
734+
Host my-server.prod
735+
HostName my-server.prod.example.com
736+
User prod_user
737+
Port 443
738+
739+
Host test_server_01
740+
HostName 192.168.1.100
741+
User testuser
742+
Port 2222
743+
744+
Host staging-web-01
745+
HostName staging-web-01.internal
746+
User deploy
747+
"""
748+
with patch("builtins.open", mock_open(read_data=config)):
749+
result = get_config_content()
750+
751+
expected_hosts = {"my-server.prod", "test_server_01", "staging-web-01"}
752+
actual_hosts = set(result.keys())
753+
self.assertEqual(actual_hosts, expected_hosts)
754+
755+
# Check specific details
756+
self.assertEqual(result["my-server.prod"]["Port"], "443")
757+
self.assertEqual(result["test_server_01"]["Hostname"], "192.168.1.100")
758+
self.assertEqual(result["staging-web-01"]["User"], "deploy")
759+
760+
@patch("os.path.exists", return_value=True)
761+
def test_commented_lines_ignored(self, mock_exists):
762+
"""Test that commented lines are properly ignored"""
763+
config = """
764+
# Global settings
765+
Host *
766+
Compression yes
767+
768+
# Production server
769+
Host server-one
770+
HostName server-one.example.com
771+
User username
772+
# Port 2222 # Commented out port
773+
Port 22
774+
775+
# Host commented-out-server
776+
# HostName should-not-appear.com
777+
"""
778+
with patch("builtins.open", mock_open(read_data=config)):
779+
result = get_config_content()
780+
781+
# Should only have server-one
782+
self.assertEqual(list(result.keys()), ["server-one"])
783+
self.assertEqual(
784+
result["server-one"]["Port"], "22"
785+
) # Should use uncommented port
786+
787+
@patch("os.path.exists", return_value=True)
788+
def test_default_values(self, mock_exists):
789+
"""Test default values are applied correctly"""
790+
config = """
791+
Host minimal-server
792+
HostName minimal.example.com
793+
# No user or port specified
794+
795+
Host partial-server
796+
User partial_user
797+
# No hostname or port specified
798+
"""
799+
with patch("builtins.open", mock_open(read_data=config)):
800+
result = get_config_content()
801+
802+
# Check minimal-server defaults
803+
self.assertEqual(
804+
result["minimal-server"]["Hostname"], "minimal.example.com"
805+
)
806+
self.assertEqual(
807+
result["minimal-server"]["User"], None
808+
) # No user specified
809+
self.assertEqual(result["minimal-server"]["Port"], "22") # Default port
810+
811+
# Check partial-server defaults
812+
self.assertEqual(
813+
result["partial-server"]["Hostname"], "partial-server"
814+
) # Defaults to host name
815+
self.assertEqual(result["partial-server"]["User"], "partial_user")
816+
self.assertEqual(result["partial-server"]["Port"], "22") # Default port

0 commit comments

Comments
 (0)