""" Tests for the hosts file parser. This module contains unit tests for the HostsParser class, validating file parsing and serialization functionality. """ import pytest import tempfile import os from pathlib import Path from hosts.core.parser import HostsParser from hosts.core.models import HostEntry, HostsFile class TestHostsParser: """Test cases for the HostsParser class.""" def test_parser_initialization(self): """Test parser initialization with default and custom paths.""" # Default path parser = HostsParser() assert str(parser.file_path) == "/etc/hosts" # Custom path custom_path = "/tmp/test_hosts" parser = HostsParser(custom_path) assert str(parser.file_path) == custom_path def test_parse_simple_hosts_file(self): """Test parsing a simple hosts file.""" content = """127.0.0.1 localhost 192.168.1.1 router """ with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(content) f.flush() parser = HostsParser(f.name) hosts_file = parser.parse() assert len(hosts_file.entries) == 2 # Check first entry entry1 = hosts_file.entries[0] assert entry1.ip_address == "127.0.0.1" assert entry1.hostnames == ["localhost"] assert entry1.is_active is True assert entry1.comment is None # Check second entry entry2 = hosts_file.entries[1] assert entry2.ip_address == "192.168.1.1" assert entry2.hostnames == ["router"] assert entry2.is_active is True assert entry2.comment is None os.unlink(f.name) def test_parse_hosts_file_with_comments(self): """Test parsing hosts file with comments and inactive entries.""" content = """# This is a header comment # Another header comment 127.0.0.1 localhost loopback # Loopback address 192.168.1.1 router gateway # Local router # 10.0.0.1 test.local # Disabled test entry # Footer comment """ with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(content) f.flush() parser = HostsParser(f.name) hosts_file = parser.parse() # Check header comments assert len(hosts_file.header_comments) == 2 assert hosts_file.header_comments[0] == "This is a header comment" assert hosts_file.header_comments[1] == "Another header comment" # Check entries assert len(hosts_file.entries) == 3 # Active entry with comment entry1 = hosts_file.entries[0] assert entry1.ip_address == "127.0.0.1" assert entry1.hostnames == ["localhost", "loopback"] assert entry1.comment == "Loopback address" assert entry1.is_active is True # Another active entry entry2 = hosts_file.entries[1] assert entry2.ip_address == "192.168.1.1" assert entry2.hostnames == ["router", "gateway"] assert entry2.comment == "Local router" assert entry2.is_active is True # Inactive entry entry3 = hosts_file.entries[2] assert entry3.ip_address == "10.0.0.1" assert entry3.hostnames == ["test.local"] assert entry3.comment == "Disabled test entry" assert entry3.is_active is False # Check footer comments assert len(hosts_file.footer_comments) == 1 assert hosts_file.footer_comments[0] == "Footer comment" os.unlink(f.name) def test_parse_empty_file(self): """Test parsing an empty hosts file.""" with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write("") f.flush() parser = HostsParser(f.name) hosts_file = parser.parse() assert len(hosts_file.entries) == 0 assert len(hosts_file.header_comments) == 0 assert len(hosts_file.footer_comments) == 0 os.unlink(f.name) def test_parse_comments_only_file(self): """Test parsing a file with only comments.""" content = """# This is a comment # Another comment # Yet another comment """ with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(content) f.flush() parser = HostsParser(f.name) hosts_file = parser.parse() assert len(hosts_file.entries) == 0 assert len(hosts_file.header_comments) == 3 assert hosts_file.header_comments[0] == "This is a comment" assert hosts_file.header_comments[1] == "Another comment" assert hosts_file.header_comments[2] == "Yet another comment" os.unlink(f.name) def test_parse_nonexistent_file(self): """Test parsing a nonexistent file raises FileNotFoundError.""" parser = HostsParser("/nonexistent/path/hosts") with pytest.raises(FileNotFoundError): parser.parse() def test_serialize_simple_hosts_file(self): """Test serializing a simple hosts file.""" hosts_file = HostsFile() entry1 = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) entry2 = HostEntry(ip_address="192.168.1.1", hostnames=["router"]) hosts_file.add_entry(entry1) hosts_file.add_entry(entry2) parser = HostsParser() content = parser.serialize(hosts_file) expected = """# # # Host Database # # Managed by hosts - https://git.s1q.dev/phg/hosts # # 127.0.0.1\tlocalhost 192.168.1.1\trouter """ assert content == expected def test_serialize_hosts_file_with_comments(self): """Test serializing hosts file with comments.""" hosts_file = HostsFile() hosts_file.header_comments = ["Header comment 1", "Header comment 2"] hosts_file.footer_comments = ["Footer comment"] entry1 = HostEntry( ip_address="127.0.0.1", hostnames=["localhost"], comment="Loopback" ) entry2 = HostEntry( ip_address="10.0.0.1", hostnames=["test"], is_active=False ) hosts_file.add_entry(entry1) hosts_file.add_entry(entry2) parser = HostsParser() content = parser.serialize(hosts_file) expected = """# Header comment 1 # Header comment 2 # Managed by hosts - https://git.s1q.dev/phg/hosts 127.0.0.1\tlocalhost\t# Loopback # 10.0.0.1\ttest # Footer comment """ assert content == expected def test_serialize_empty_hosts_file(self): """Test serializing an empty hosts file.""" hosts_file = HostsFile() parser = HostsParser() content = parser.serialize(hosts_file) expected = """# # # Host Database # # Managed by hosts - https://git.s1q.dev/phg/hosts # # """ assert content == expected def test_write_hosts_file(self): """Test writing hosts file to disk.""" hosts_file = HostsFile() entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) hosts_file.add_entry(entry) with tempfile.NamedTemporaryFile(delete=False) as f: parser = HostsParser(f.name) parser.write(hosts_file, backup=False) # Read back and verify with open(f.name, 'r') as read_file: content = read_file.read() expected = """# # # Host Database # # Managed by hosts - https://git.s1q.dev/phg/hosts # # 127.0.0.1\tlocalhost """ assert content == expected os.unlink(f.name) def test_write_hosts_file_with_backup(self): """Test writing hosts file with backup creation.""" # Create initial file initial_content = "192.168.1.1 router\n" with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(initial_content) f.flush() # Create new hosts file to write hosts_file = HostsFile() entry = HostEntry(ip_address="127.0.0.1", hostnames=["localhost"]) hosts_file.add_entry(entry) parser = HostsParser(f.name) parser.write(hosts_file, backup=True) # Check that backup was created backup_path = Path(f.name).with_suffix('.bak') assert backup_path.exists() # Check backup content with open(backup_path, 'r') as backup_file: backup_content = backup_file.read() assert backup_content == initial_content # Check new content with open(f.name, 'r') as new_file: new_content = new_file.read() expected = """# # # Host Database # # Managed by hosts - https://git.s1q.dev/phg/hosts # # 127.0.0.1\tlocalhost """ assert new_content == expected # Cleanup os.unlink(backup_path) os.unlink(f.name) def test_validate_write_permissions(self): """Test write permission validation.""" # Test with a temporary file (should be writable) with tempfile.NamedTemporaryFile() as f: parser = HostsParser(f.name) assert parser.validate_write_permissions() is True # Test with a nonexistent file in /tmp (should be writable) parser = HostsParser("/tmp/test_hosts_nonexistent") assert parser.validate_write_permissions() is True # Test with a path that likely doesn't have write permissions parser = HostsParser("/root/test_hosts") # This might be True if running as root, so we can't assert False result = parser.validate_write_permissions() assert isinstance(result, bool) def test_get_file_info(self): """Test getting file information.""" content = "127.0.0.1 localhost\n" with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(content) f.flush() parser = HostsParser(f.name) info = parser.get_file_info() assert info['path'] == f.name assert info['exists'] is True assert info['readable'] is True assert info['size'] == len(content) assert info['modified'] is not None assert isinstance(info['modified'], float) os.unlink(f.name) def test_get_file_info_nonexistent(self): """Test getting file information for nonexistent file.""" parser = HostsParser("/nonexistent/path") info = parser.get_file_info() assert info['path'] == "/nonexistent/path" assert info['exists'] is False assert info['readable'] is False assert info['writable'] is False assert info['size'] == 0 assert info['modified'] is None def test_round_trip_parsing(self): """Test that parsing and serializing preserves content.""" original_content = """# System hosts file # Do not edit manually 127.0.0.1 localhost loopback # Local loopback ::1 localhost # IPv6 loopback 192.168.1.1 router gateway # Local router # 10.0.0.1 test.local # Test entry (disabled) # End of file """ with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(original_content) f.flush() # Parse and serialize parser = HostsParser(f.name) hosts_file = parser.parse() # Write back and read parser.write(hosts_file, backup=False) with open(f.name, 'r') as read_file: final_content = read_file.read() # The content should be functionally equivalent # (though formatting might differ slightly with tabs) assert "127.0.0.1\tlocalhost\tloopback\t# Local loopback" in final_content assert "::1\t\tlocalhost\t# IPv6 loopback" in final_content assert "192.168.1.1\trouter\t\tgateway\t# Local router" in final_content assert "# 10.0.0.1\ttest.local\t# Test entry (disabled)" in final_content os.unlink(f.name)