hosts/tests/test_parser.py
phg 1fddff91c8 Refactor tests for PermissionManager, HostsManager, HostEntry, HostsFile, and HostsParser
- Updated test cases in test_manager.py to improve readability and consistency.
- Simplified assertions and mock setups in tests for PermissionManager.
- Enhanced test coverage for HostsManager, including edit mode and entry manipulation tests.
- Improved test structure in test_models.py for HostEntry and HostsFile, ensuring clarity in test cases.
- Refined test cases in test_parser.py for better organization and readability.
- Adjusted test_save_confirmation_modal.py to maintain consistency in mocking and assertions.
2025-08-14 17:32:02 +02:00

372 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 = """# #
# 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)