352 lines
12 KiB
Python
352 lines
12 KiB
Python
"""
|
|
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 = """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
|
|
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)
|
|
|
|
assert content == "\n"
|
|
|
|
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()
|
|
assert content == "127.0.0.1\tlocalhost\n"
|
|
|
|
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()
|
|
assert new_content == "127.0.0.1\tlocalhost\n"
|
|
|
|
# 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)
|