"""Comprehensive tests for SSH config management.""" from pathlib import Path import pytest from dropkit.ssh_config import ( add_ssh_host, get_ssh_host_ip, remove_known_hosts_entry, remove_ssh_host, ) @pytest.fixture def temp_ssh_dir(tmp_path): """Create a temporary SSH directory for testing.""" ssh_dir = tmp_path / ".ssh" ssh_dir.mkdir(mode=0o700) yield ssh_dir # Cleanup is automatic with tmp_path @pytest.fixture def temp_config(temp_ssh_dir): """Create a temporary SSH config file path.""" config_path = temp_ssh_dir / "config" return str(config_path) class TestAddSSHHost: """Tests for add_ssh_host function.""" def test_empty_file(self, temp_config): """Test adding host to empty SSH config file.""" # Create empty file Path(temp_config).touch(mode=0o600) add_ssh_host(temp_config, "myhost", "192.168.1.1", "ubuntu") content = Path(temp_config).read_text() assert "Host myhost" in content assert "HostName 192.168.1.1" in content assert "User ubuntu" in content def test_non_existent_file(self, temp_config): """Test creating new file and directory if they don't exist.""" # Ensure file doesn't exist assert not Path(temp_config).exists() add_ssh_host(temp_config, "myhost", "192.168.1.1", "ubuntu", "~/.ssh/id_ed25519") assert Path(temp_config).exists() content = Path(temp_config).read_text() assert "Host myhost" in content assert "IdentityFile ~/.ssh/id_ed25519" in content def test_simple_addition(self, temp_config): """Test adding host to config with existing unrelated hosts.""" # Create config with existing host existing = """Host other-server HostName 10.0.0.1 User admin Host another-host HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "myhost", "192.168.1.1", "ubuntu") content = Path(temp_config).read_text() assert "Host other-server" in content assert "Host another-host" in content assert "Host myhost" in content # Count "Host " with space to avoid matching "HostName" assert content.count("Host ") == 3 def test_update_existing_host(self, temp_config): """Test replacing existing host entry without duplication.""" # Create config with existing host existing = """Host myhost HostName 10.0.0.1 User old_user Host other-host HostName 10.0.0.2 User admin """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "myhost", "192.168.1.1", "new_user") content = Path(temp_config).read_text() assert content.count("Host myhost") == 1 # Should not duplicate assert "HostName 192.168.1.1" in content assert "User new_user" in content assert "old_user" not in content assert "10.0.0.1" not in content assert "Host other-host" in content # Other hosts preserved def test_host_with_similar_names(self, temp_config): """Test that similar host names don't interfere.""" existing = """Host myhost HostName 10.0.0.1 User user1 Host myhost-dev HostName 10.0.0.2 User user2 Host myhost-prod HostName 10.0.0.3 User user3 """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "myhost", "192.168.1.1", "newuser") content = Path(temp_config).read_text() assert "Host myhost-dev" in content assert "10.0.0.2" in content assert "Host myhost-prod" in content assert "10.0.0.3" in content # myhost should be updated lines = content.split("\n") myhost_line = [i for i, line in enumerate(lines) if line.strip() == "Host myhost"][-1] # Check that the next few lines after myhost contain the new values section = "\n".join(lines[myhost_line : myhost_line + 5]) assert "192.168.1.1" in section assert "newuser" in section def test_host_as_substring(self, temp_config): """Test that substring hosts are handled correctly.""" existing = """Host prod-server HostName 10.0.0.1 User admin Host prod HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "prod", "192.168.1.1", "ubuntu") content = Path(temp_config).read_text() assert "Host prod-server" in content assert "10.0.0.1" in content # prod should be updated assert content.count("Host prod\n") >= 1 def test_multiple_blank_lines(self, temp_config): """Test handling config with various blank lines.""" existing = """Host server1 HostName 10.0.0.1 User admin Host server2 HostName 10.0.0.2 User root Host server3 HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "newhost", "192.168.1.1", "user") content = Path(temp_config).read_text() assert "Host server1" in content assert "Host server2" in content assert "Host server3" in content assert "Host newhost" in content def test_comments_in_config(self, temp_config): """Test preserving comments in config.""" existing = """# This is a comment Host server1 HostName 10.0.0.1 User admin # Another comment # Global settings Host server2 HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "newhost", "192.168.1.1", "user") content = Path(temp_config).read_text() assert "# This is a comment" in content assert "# Another comment" in content assert "# Global settings" in content def test_mixed_indentation(self, temp_config): """Test handling tabs and spaces.""" existing = """Host server1 HostName 10.0.0.1 User admin Host server2 HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "newhost", "192.168.1.1", "user") content = Path(temp_config).read_text() # Original hosts should be preserved assert "Host server1" in content assert "Host server2" in content # New host should be added assert "Host newhost" in content def test_host_at_beginning(self, temp_config): """Test updating a host that's the first entry.""" existing = """Host first-host HostName 10.0.0.1 User admin Host second-host HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "first-host", "192.168.1.1", "newuser") content = Path(temp_config).read_text() assert content.count("Host first-host") == 1 assert "192.168.1.1" in content assert "10.0.0.1" not in content assert "Host second-host" in content def test_host_at_end(self, temp_config): """Test updating a host that's the last entry.""" existing = """Host first-host HostName 10.0.0.1 User admin Host last-host HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "last-host", "192.168.1.1", "newuser") content = Path(temp_config).read_text() assert "Host first-host" in content assert content.count("Host last-host") == 1 assert "192.168.1.1" in content assert "10.0.0.2" not in content def test_host_in_middle(self, temp_config): """Test updating a host between others.""" existing = """Host first-host HostName 10.0.0.1 User admin Host middle-host HostName 10.0.0.2 User root Host last-host HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "middle-host", "192.168.1.1", "newuser") content = Path(temp_config).read_text() assert "Host first-host" in content assert "Host last-host" in content assert content.count("Host middle-host") == 1 assert "192.168.1.1" in content assert "10.0.0.2" not in content def test_complex_host_entries(self, temp_config): """Test host with many configuration options.""" existing = """Host complex-host HostName 10.0.0.1 User admin Port 2222 ForwardAgent yes ProxyJump bastion IdentityFile ~/.ssh/custom_key StrictHostKeyChecking no """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "complex-host", "192.168.1.1", "newuser", "~/.ssh/new_key") content = Path(temp_config).read_text() assert content.count("Host complex-host") == 1 assert "192.168.1.1" in content assert "newuser" in content # Old complex options should be replaced assert "Port 2222" not in content def test_host_with_extra_spaces(self, temp_config): """Test host declaration with multiple spaces.""" existing = """Host server-with-spaces HostName 10.0.0.1 User admin """ Path(temp_config).write_text(existing) # This won't match because our check is exact add_ssh_host(temp_config, "server-with-spaces", "192.168.1.1", "newuser") content = Path(temp_config).read_text() # Should add a new entry since "Host server-with-spaces" != "Host server-with-spaces" assert "Host server-with-spaces" in content def test_host_patterns(self, temp_config): """Test existing wildcard hosts.""" existing = """Host *.example.com User admin IdentityFile ~/.ssh/example_key Host myserver HostName 10.0.0.1 User root """ Path(temp_config).write_text(existing) add_ssh_host(temp_config, "newhost", "192.168.1.1", "ubuntu") content = Path(temp_config).read_text() assert "Host *.example.com" in content assert "Host myserver" in content assert "Host newhost" in content def test_no_trailing_newline(self, temp_config): """Test config file without final newline.""" existing = """Host myhost HostName 10.0.0.1 User admin""" # No trailing newline Path(temp_config).write_text(existing) add_ssh_host(temp_config, "newhost", "192.168.1.1", "ubuntu") content = Path(temp_config).read_text() assert "Host myhost" in content assert "Host newhost" in content # Verify Host newhost is on its own line (not concatenated to previous line) lines = content.split("\n") host_lines = [line for line in lines if "Host newhost" in line] assert len(host_lines) == 1 assert host_lines[0].strip() == "Host newhost" def test_permissions_preserved(self, temp_config): """Test that file permissions are set correctly.""" Path(temp_config).touch(mode=0o644) add_ssh_host(temp_config, "myhost", "192.168.1.1", "ubuntu") # File should now have 0600 permissions mode = Path(temp_config).stat().st_mode & 0o777 assert mode == 0o600 def test_backup_created(self, temp_config): """Test that backup file is created.""" existing = """Host oldhost HostName 10.0.0.1 User admin """ Path(temp_config).write_text(existing) Path(temp_config).chmod(0o600) add_ssh_host(temp_config, "newhost", "192.168.1.1", "ubuntu") backup_path = Path(temp_config).parent / "config.bak" assert backup_path.exists() backup_content = backup_path.read_text() assert "Host oldhost" in backup_content assert "Host newhost" not in backup_content # Check backup has same permissions backup_mode = backup_path.stat().st_mode & 0o777 assert backup_mode == 0o600 class TestRemoveSSHHost: """Tests for remove_ssh_host function.""" def test_non_existent_file(self, temp_config): """Test removing from non-existent config.""" assert not Path(temp_config).exists() result = remove_ssh_host(temp_config, "myhost") assert result is False def test_host_doesnt_exist(self, temp_config): """Test removing host that's not in config.""" existing = """Host server1 HostName 10.0.0.1 User admin """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "nonexistent") assert result is False content = Path(temp_config).read_text() assert "Host server1" in content # Should be unchanged def test_remove_first_host(self, temp_config): """Test removing the first host entry.""" existing = """Host first-host HostName 10.0.0.1 User admin Host second-host HostName 10.0.0.2 User root Host third-host HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "first-host") assert result is True content = Path(temp_config).read_text() assert "Host first-host" not in content assert "Host second-host" in content assert "Host third-host" in content def test_remove_last_host(self, temp_config): """Test removing the last host entry.""" existing = """Host first-host HostName 10.0.0.1 User admin Host second-host HostName 10.0.0.2 User root Host last-host HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "last-host") assert result is True content = Path(temp_config).read_text() assert "Host first-host" in content assert "Host second-host" in content assert "Host last-host" not in content def test_remove_middle_host(self, temp_config): """Test removing a host between others.""" existing = """Host first-host HostName 10.0.0.1 User admin Host middle-host HostName 10.0.0.2 User root Host last-host HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "middle-host") assert result is True content = Path(temp_config).read_text() assert "Host first-host" in content assert "Host middle-host" not in content assert "Host last-host" in content def test_remove_only_host(self, temp_config): """Test removing the only host in config.""" existing = """Host only-host HostName 10.0.0.1 User admin """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "only-host") assert result is True content = Path(temp_config).read_text() assert "Host only-host" not in content # File should be essentially empty or just whitespace assert content.strip() == "" or "Host" not in content def test_remove_similar_host_names(self, temp_config): """Test removing specific host when similar names exist.""" existing = """Host myhost HostName 10.0.0.1 User user1 Host myhost-dev HostName 10.0.0.2 User user2 Host myhost-prod HostName 10.0.0.3 User user3 """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "myhost") assert result is True content = Path(temp_config).read_text() assert "Host myhost\n" not in content assert "10.0.0.1" not in content assert "Host myhost-dev" in content assert "10.0.0.2" in content assert "Host myhost-prod" in content assert "10.0.0.3" in content def test_remove_host_with_many_options(self, temp_config): """Test removing host with many configuration lines.""" existing = """Host simple-host HostName 10.0.0.1 User admin Host complex-host HostName 10.0.0.2 User root Port 2222 ForwardAgent yes ProxyJump bastion IdentityFile ~/.ssh/custom_key StrictHostKeyChecking no LocalForward 8080 localhost:80 Host another-host HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "complex-host") assert result is True content = Path(temp_config).read_text() assert "Host simple-host" in content assert "Host complex-host" not in content assert "Port 2222" not in content assert "ProxyJump bastion" not in content assert "Host another-host" in content def test_backup_created_on_remove(self, temp_config): """Test that backup is created when removing host.""" existing = """Host myhost HostName 10.0.0.1 User admin Host otherhost HostName 10.0.0.2 User root """ Path(temp_config).write_text(existing) Path(temp_config).chmod(0o600) remove_ssh_host(temp_config, "myhost") backup_path = Path(temp_config).parent / "config.bak" assert backup_path.exists() backup_content = backup_path.read_text() assert "Host myhost" in backup_content assert "Host otherhost" in backup_content # Check backup has same permissions backup_mode = backup_path.stat().st_mode & 0o777 assert backup_mode == 0o600 def test_remove_preserves_comments(self, temp_config): """Test that comments are preserved when removing host.""" existing = """# Global comment Host keephost HostName 10.0.0.1 User admin # This host will be removed Host removehost HostName 10.0.0.2 User root # Another comment Host anotherhost HostName 10.0.0.3 User ubuntu """ Path(temp_config).write_text(existing) result = remove_ssh_host(temp_config, "removehost") assert result is True content = Path(temp_config).read_text() assert "# Global comment" in content assert "# Another comment" in content assert "Host keephost" in content assert "Host removehost" not in content assert "Host anotherhost" in content class TestGetSSHHostIP: """Tests for get_ssh_host_ip function.""" def test_valid_host(self, temp_config): """Test getting IP for existing host.""" existing = """Host myhost HostName 192.168.1.100 User ubuntu """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "myhost") assert result == "192.168.1.100" def test_host_not_found(self, temp_config): """Test getting IP for non-existent host.""" existing = """Host otherhost HostName 192.168.1.100 User ubuntu """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "myhost") assert result is None def test_missing_hostname_field(self, temp_config): """Test host entry without HostName field.""" existing = """Host myhost User ubuntu IdentityFile ~/.ssh/id_rsa """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "myhost") assert result is None def test_non_existent_file(self, temp_config): """Test with non-existent config file.""" result = get_ssh_host_ip(temp_config, "myhost") assert result is None def test_tailscale_ip(self, temp_config): """Test getting Tailscale IP address.""" existing = """Host dropkit.myhost HostName 100.80.123.45 User ubuntu ForwardAgent yes """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "dropkit.myhost") assert result == "100.80.123.45" def test_multiple_hosts(self, temp_config): """Test getting IP from config with multiple hosts.""" existing = """Host firsthost HostName 10.0.0.1 User admin Host targethost HostName 192.168.1.50 User ubuntu Host thirdhost HostName 10.0.0.3 User root """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "targethost") assert result == "192.168.1.50" def test_host_with_extra_whitespace(self, temp_config): """Test HostName with extra whitespace.""" existing = """Host myhost HostName 192.168.1.100 User ubuntu """ Path(temp_config).write_text(existing) result = get_ssh_host_ip(temp_config, "myhost") assert result == "192.168.1.100" def test_multiple_hosts_on_same_line(self, temp_config): """Test host with multiple aliases on same line.""" existing = """Host myhost myalias anotherhost HostName 192.168.1.100 User ubuntu """ Path(temp_config).write_text(existing) # Should work for any of the aliases assert get_ssh_host_ip(temp_config, "myhost") == "192.168.1.100" assert get_ssh_host_ip(temp_config, "myalias") == "192.168.1.100" assert get_ssh_host_ip(temp_config, "anotherhost") == "192.168.1.100" def test_hostname_vs_host(self, temp_config): """Test that we distinguish between Host directive and HostName option.""" existing = """Host realhost HostName 192.168.1.100 User ubuntu Host anotherreal HostName 10.0.0.1 User admin """ Path(temp_config).write_text(existing) # Should not find HostName as a host alias assert get_ssh_host_ip(temp_config, "192.168.1.100") is None # Should find the actual hosts assert get_ssh_host_ip(temp_config, "realhost") == "192.168.1.100" class TestRemoveKnownHostsEntry: """Tests for remove_known_hosts_entry function.""" @pytest.fixture def temp_known_hosts(self, temp_ssh_dir): """Create a temporary known_hosts file path.""" known_hosts_path = temp_ssh_dir / "known_hosts" return str(known_hosts_path) def test_non_existent_file(self, temp_known_hosts): """Test removing from non-existent known_hosts.""" assert not Path(temp_known_hosts).exists() result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 0 def test_no_matching_entries(self, temp_known_hosts): """Test removing hostname that's not in known_hosts.""" existing = """otherhost ssh-ed25519 AAAA... 192.168.1.1 ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 0 content = Path(temp_known_hosts).read_text() assert "otherhost" in content assert "192.168.1.1" in content def test_remove_single_hostname(self, temp_known_hosts): """Test removing a single hostname entry.""" existing = """myhost ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "myhost" not in content assert "otherhost" in content def test_remove_ip_address(self, temp_known_hosts): """Test removing an IP address entry.""" existing = """192.168.1.100 ssh-ed25519 AAAA... 10.0.0.1 ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["192.168.1.100"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "192.168.1.100" not in content assert "10.0.0.1" in content def test_remove_multiple_entries(self, temp_known_hosts): """Test removing hostname and IP address together.""" existing = """dropkit.myhost ssh-ed25519 AAAA... 100.80.123.45 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["dropkit.myhost", "100.80.123.45"]) assert result == 2 content = Path(temp_known_hosts).read_text() assert "dropkit.myhost" not in content assert "100.80.123.45" not in content assert "otherhost" in content def test_comma_separated_hostnames(self, temp_known_hosts): """Test removing entry with comma-separated hostnames.""" existing = """myhost,192.168.1.100 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) # Should match by hostname result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "myhost" not in content assert "192.168.1.100" not in content # Whole line removed assert "otherhost" in content def test_comma_separated_match_by_ip(self, temp_known_hosts): """Test removing entry by IP when comma-separated.""" existing = """myhost,192.168.1.100 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) # Should match by IP result = remove_known_hosts_entry(temp_known_hosts, ["192.168.1.100"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "myhost" not in content assert "192.168.1.100" not in content assert "otherhost" in content def test_bracketed_entry(self, temp_known_hosts): """Test removing bracketed entry like [hostname]:port. Note: ssh-keygen -R requires the exact hostname format, so bracketed entries must be passed as "[hostname]:port", not just "hostname". """ existing = """[myhost]:2222 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) # Must use exact format for bracketed entries result = remove_known_hosts_entry(temp_known_hosts, ["[myhost]:2222"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "[myhost]:2222" not in content assert "otherhost" in content def test_bracketed_ip_entry(self, temp_known_hosts): """Test removing bracketed IP entry like [192.168.1.1]:2222. Note: ssh-keygen -R requires the exact hostname format, so bracketed entries must be passed as "[ip]:port", not just "ip". """ existing = """[192.168.1.100]:2222 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) # Must use exact format for bracketed entries result = remove_known_hosts_entry(temp_known_hosts, ["[192.168.1.100]:2222"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "192.168.1.100" not in content assert "otherhost" in content def test_hashed_entries_preserved(self, temp_known_hosts): """Test that hashed entries (|1|...) are preserved when removing other hosts.""" import subprocess # Create real hashed entries by hashing actual hostnames existing = """other1.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest1 myhost ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQTest other2.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest2 """ Path(temp_known_hosts).write_text(existing) # Hash the file to create real |1|... entries subprocess.run( ["ssh-keygen", "-H", "-f", str(temp_known_hosts)], capture_output=True, check=True, ) # Remove the .old backup file ssh-keygen creates old_file = Path(temp_known_hosts + ".old") if old_file.exists(): old_file.unlink() # Verify we have hashed entries (all should be hashed now) content = Path(temp_known_hosts).read_text() lines = [line for line in content.strip().split("\n") if line] assert len(lines) == 3 assert all(line.startswith("|1|") for line in lines) # Now remove "myhost" - the other two hashed entries should remain result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() lines = [line for line in content.strip().split("\n") if line] # Should have exactly 2 entries remaining (other1 and other2) assert len(lines) == 2 assert all(line.startswith("|1|") for line in lines) def test_remove_hashed_entry(self, temp_known_hosts): """Test removing a hashed known_hosts entry by hostname. This tests the actual bug: macOS/Linux often use HashKnownHosts yes, which stores entries like |1|| instead of plaintext hostnames. The function should still be able to remove these entries. """ import subprocess hostname = "test.example.com" # Create an unhashed entry first unhashed = f"{hostname} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest\n" Path(temp_known_hosts).write_text(unhashed) # Hash it using ssh-keygen -H subprocess.run( ["ssh-keygen", "-H", "-f", str(temp_known_hosts)], capture_output=True, check=True, ) # Remove the .old backup file ssh-keygen creates old_file = Path(temp_known_hosts + ".old") if old_file.exists(): old_file.unlink() # Verify entry is now hashed content = Path(temp_known_hosts).read_text() assert content.startswith("|1|"), f"Entry should be hashed, got: {content}" assert hostname not in content # Hostname should not appear in plaintext # Now try to remove by hostname - this is the bug test result = remove_known_hosts_entry(temp_known_hosts, [hostname]) assert result == 1, "Should have removed the hashed entry" content = Path(temp_known_hosts).read_text() assert content.strip() == "", f"File should be empty after removal, got: {content}" def test_comments_preserved(self, temp_known_hosts): """Test that comments are preserved.""" existing = """# This is a comment myhost ssh-ed25519 AAAA... # Another comment otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "# This is a comment" in content assert "# Another comment" in content assert "myhost" not in content assert "otherhost" in content def test_empty_lines_preserved(self, temp_known_hosts): """Test that empty lines are preserved.""" existing = """myhost ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "myhost" not in content assert "otherhost" in content # Should preserve structure assert "\n\n" in content or content.count("\n") >= 2 def test_case_insensitive_matching(self, temp_known_hosts): """Test that hostname matching is case-insensitive.""" existing = """MyHost ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost"]) assert result == 1 content = Path(temp_known_hosts).read_text() assert "MyHost" not in content assert "otherhost" in content def test_backup_created(self, temp_known_hosts): """Test that backup file is created.""" existing = """myhost ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) Path(temp_known_hosts).chmod(0o600) remove_known_hosts_entry(temp_known_hosts, ["myhost"]) # ssh-keygen -R creates .old backup (not .bak) backup_path = Path(temp_known_hosts + ".old") assert backup_path.exists() backup_content = backup_path.read_text() assert "myhost" in backup_content assert "otherhost" in backup_content def test_remove_all_entries(self, temp_known_hosts): """Test removing all entries from known_hosts.""" existing = """myhost ssh-ed25519 AAAA... 192.168.1.100 ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["myhost", "192.168.1.100"]) assert result == 2 content = Path(temp_known_hosts).read_text() assert content.strip() == "" def test_dropkit_hostname_format(self, temp_known_hosts): """Test removing dropkit-style hostname.""" existing = """dropkit.my-droplet ssh-ed25519 AAAA... 100.80.123.45 ssh-ed25519 AAAA... otherhost ssh-rsa AAAA... """ Path(temp_known_hosts).write_text(existing) result = remove_known_hosts_entry(temp_known_hosts, ["dropkit.my-droplet", "100.80.123.45"]) assert result == 2 content = Path(temp_known_hosts).read_text() assert "dropkit.my-droplet" not in content assert "100.80.123.45" not in content assert "otherhost" in content