""" Tests for the filtering system. This module contains comprehensive tests for the EntryFilter class and filtering functionality. """ import pytest from datetime import datetime, timedelta from src.hosts.core.filters import EntryFilter, FilterOptions from src.hosts.core.models import HostEntry class TestFilterOptions: """Test FilterOptions dataclass.""" def test_default_values(self): """Test default FilterOptions values.""" options = FilterOptions() assert options.show_active is True assert options.show_inactive is True assert options.active_only is False assert options.inactive_only is False assert options.show_dns_entries is True assert options.show_ip_entries is True assert options.dns_only is False assert options.ip_only is False assert options.show_resolved is True assert options.show_unresolved is True assert options.show_resolving is True assert options.show_failed is True assert options.show_mismatched is True assert options.mismatch_only is False assert options.resolved_only is False assert options.search_term is None assert options.preset_name is None def test_custom_values(self): """Test FilterOptions with custom values.""" options = FilterOptions( active_only=True, dns_only=True, search_term="test", preset_name="Active DNS Only" ) assert options.active_only is True assert options.dns_only is True assert options.search_term == "test" assert options.preset_name == "Active DNS Only" def test_to_dict(self): """Test converting FilterOptions to dictionary.""" options = FilterOptions( active_only=True, search_term="test", preset_name="Test Preset" ) result = options.to_dict() expected = { 'show_active': True, 'show_inactive': True, 'active_only': True, 'inactive_only': False, 'show_dns_entries': True, 'show_ip_entries': True, 'dns_only': False, 'ip_only': False, 'show_resolved': True, 'show_unresolved': True, 'show_resolving': True, 'show_failed': True, 'show_mismatched': True, 'mismatch_only': False, 'resolved_only': False, 'search_term': 'test', 'search_in_hostnames': True, 'search_in_comments': True, 'search_in_ips': True, 'case_sensitive': False, 'preset_name': 'Test Preset' } assert result == expected def test_from_dict(self): """Test creating FilterOptions from dictionary.""" data = { 'active_only': True, 'dns_only': True, 'search_term': 'test', 'preset_name': 'Test Preset' } options = FilterOptions.from_dict(data) assert options.active_only is True assert options.dns_only is True assert options.search_term == 'test' assert options.preset_name == 'Test Preset' # Verify missing keys use defaults assert options.inactive_only is False def test_from_dict_partial(self): """Test creating FilterOptions from partial dictionary.""" data = {'active_only': True} options = FilterOptions.from_dict(data) assert options.active_only is True assert options.inactive_only is False # Default value assert options.search_term is None # Default value def test_is_empty(self): """Test checking if filter options are empty.""" # Default options should be empty options = FilterOptions() assert options.is_empty() is True # Options with search term should not be empty options = FilterOptions(search_term="test") assert options.is_empty() is False # Options with any filter enabled should not be empty options = FilterOptions(active_only=True) assert options.is_empty() is False class TestEntryFilter: """Test EntryFilter class.""" @pytest.fixture def sample_entries(self): """Create sample entries for testing.""" entries = [] # Active IP entry entry1 = HostEntry("192.168.1.1", ["example.com"], "Test entry", True) entries.append(entry1) # Inactive IP entry entry2 = HostEntry("192.168.1.2", ["inactive.com"], "Inactive entry", False) entries.append(entry2) # Active DNS entry - create with temporary IP then convert to DNS entry entry3 = HostEntry("1.1.1.1", ["dns-only.com"], "DNS only entry", True) entry3.ip_address = "" # Remove IP after creation entry3.dns_name = "dns-only.com" # Set DNS name entries.append(entry3) # Inactive DNS entry - create with temporary IP then convert to DNS entry entry4 = HostEntry("1.1.1.1", ["inactive-dns.com"], "Inactive DNS entry", False) entry4.ip_address = "" # Remove IP after creation entry4.dns_name = "inactive-dns.com" # Set DNS name entries.append(entry4) # Entry with DNS resolution data entry5 = HostEntry("10.0.0.1", ["resolved.com"], "Resolved entry", True) entry5.resolved_ip = "10.0.0.1" entry5.last_resolved = datetime.now() entry5.dns_resolution_status = "IP_MATCH" entries.append(entry5) # Entry with mismatched DNS entry6 = HostEntry("10.0.0.2", ["mismatch.com"], "Mismatch entry", True) entry6.resolved_ip = "10.0.0.3" # Different from IP address entry6.last_resolved = datetime.now() entry6.dns_resolution_status = "IP_MISMATCH" entries.append(entry6) # Entry without DNS resolution entry7 = HostEntry("10.0.0.4", ["unresolved.com"], "Unresolved entry", True) entries.append(entry7) return entries @pytest.fixture def entry_filter(self): """Create EntryFilter instance.""" return EntryFilter() def test_apply_filters_no_filters(self, entry_filter, sample_entries): """Test applying empty filters returns all entries.""" options = FilterOptions() result = entry_filter.apply_filters(sample_entries, options) assert len(result) == len(sample_entries) assert result == sample_entries def test_filter_by_status_active_only(self, entry_filter, sample_entries): """Test filtering by active status only.""" options = FilterOptions(active_only=True) result = entry_filter.filter_by_status(sample_entries, options) active_entries = [e for e in result if e.is_active] assert len(active_entries) == len(result) assert all(entry.is_active for entry in result) def test_filter_by_status_inactive_only(self, entry_filter, sample_entries): """Test filtering by inactive status only.""" options = FilterOptions(inactive_only=True) result = entry_filter.filter_by_status(sample_entries, options) assert all(not entry.is_active for entry in result) assert len(result) == 2 # entry2 and entry4 def test_filter_by_dns_type_dns_only(self, entry_filter, sample_entries): """Test filtering by DNS entries only.""" options = FilterOptions(dns_only=True) result = entry_filter.filter_by_dns_type(sample_entries, options) assert all(entry.dns_name is not None for entry in result) assert len(result) == 2 # entry3 and entry4 def test_filter_by_dns_type_ip_only(self, entry_filter, sample_entries): """Test filtering by IP entries only.""" options = FilterOptions(ip_only=True) result = entry_filter.filter_by_dns_type(sample_entries, options) assert all(not entry.has_dns_name() for entry in result) # Should exclude DNS-only entries (entry3, entry4) expected_count = len(sample_entries) - 2 assert len(result) == expected_count def test_filter_by_resolution_status_resolved(self, entry_filter, sample_entries): """Test filtering by resolved entries only.""" options = FilterOptions(resolved_only=True) result = entry_filter.filter_by_resolution_status(sample_entries, options) assert all(entry.dns_resolution_status in ["IP_MATCH", "RESOLVED"] for entry in result) assert len(result) == 1 # Only entry5 has resolved status def test_filter_by_resolution_status_unresolved(self, entry_filter, sample_entries): """Test filtering by unresolved entries only.""" options = FilterOptions( show_resolved=False, show_resolving=False, show_failed=False, show_mismatched=False ) result = entry_filter.filter_by_resolution_status(sample_entries, options) assert all(entry.dns_resolution_status in [None, "NOT_RESOLVED"] for entry in result) assert len(result) == 5 # All except entry5 and entry6 def test_filter_by_resolution_status_mismatch(self, entry_filter, sample_entries): """Test filtering by DNS mismatch entries only.""" options = FilterOptions(mismatch_only=True) result = entry_filter.filter_by_resolution_status(sample_entries, options) # Should only return entry6 (mismatch between IP and resolved_ip) assert len(result) == 1 assert result[0].hostnames[0] == "mismatch.com" def test_filter_by_search_hostname(self, entry_filter, sample_entries): """Test filtering by search term in hostname.""" options = FilterOptions(search_term="example") result = entry_filter.filter_by_search(sample_entries, options) assert len(result) == 1 assert result[0].hostnames[0] == "example.com" def test_filter_by_search_ip(self, entry_filter, sample_entries): """Test filtering by search term in IP address.""" options = FilterOptions(search_term="192.168") result = entry_filter.filter_by_search(sample_entries, options) assert len(result) == 2 # entry1 and entry2 def test_filter_by_search_comment(self, entry_filter, sample_entries): """Test filtering by search term in comment.""" options = FilterOptions(search_term="DNS only") result = entry_filter.filter_by_search(sample_entries, options) assert len(result) == 1 assert result[0].comment == "DNS only entry" def test_filter_by_search_case_insensitive(self, entry_filter, sample_entries): """Test search is case insensitive.""" options = FilterOptions(search_term="EXAMPLE") result = entry_filter.filter_by_search(sample_entries, options) assert len(result) == 1 assert result[0].hostnames[0] == "example.com" def test_combined_filters(self, entry_filter, sample_entries): """Test applying multiple filters together.""" # Filter for active DNS entries containing "dns" options = FilterOptions( active_only=True, dns_only=True, search_term="dns" ) result = entry_filter.apply_filters(sample_entries, options) # Should only return entry3 (active DNS entry with "dns" in hostname) assert len(result) == 1 assert result[0].hostnames[0] == "dns-only.com" assert result[0].is_active assert result[0].dns_name is not None def test_count_filtered_entries(self, entry_filter, sample_entries): """Test counting filtered entries.""" options = FilterOptions(active_only=True) counts = entry_filter.count_filtered_entries(sample_entries, options) assert counts['total'] == len(sample_entries) assert counts['filtered'] == 5 # 5 active entries def test_get_default_presets(self, entry_filter): """Test getting default filter presets.""" presets = entry_filter.get_default_presets() # Check that default presets exist assert "All Entries" in presets assert "Active Only" in presets assert "Inactive Only" in presets assert "DNS Entries Only" in presets assert "IP Entries Only" in presets assert "DNS Mismatches" in presets assert "Resolved Entries" in presets assert "Unresolved Entries" in presets # Check that presets have correct structure for preset_name, options in presets.items(): assert isinstance(options, FilterOptions) def test_save_and_load_preset(self, entry_filter): """Test saving and loading custom presets.""" # Create custom filter options custom_options = FilterOptions( active_only=True, search_term="test", preset_name="My Custom Filter" ) # Save preset entry_filter.save_preset("My Custom Filter", custom_options) # Check it was saved presets = entry_filter.get_saved_presets() assert "My Custom Filter" in presets # Load and verify loaded_options = presets["My Custom Filter"] assert loaded_options.active_only is True # Note: search_term is not saved in presets assert loaded_options.search_term is None def test_delete_preset(self, entry_filter): """Test deleting custom presets.""" # Save a preset first custom_options = FilterOptions(active_only=True) entry_filter.save_preset("To Delete", custom_options) # Verify it exists presets = entry_filter.get_saved_presets() assert "To Delete" in presets # Delete it result = entry_filter.delete_preset("To Delete") assert result is True # Verify it's gone presets = entry_filter.get_saved_presets() assert "To Delete" not in presets # Try to delete non-existent preset result = entry_filter.delete_preset("Non Existent") assert result is False def test_filter_edge_cases(self, entry_filter): """Test filtering with edge cases.""" # Empty entry list empty_options = FilterOptions() result = entry_filter.apply_filters([], empty_options) assert result == [] # None entries in list - filtering should handle None values gracefully entries_with_none = [None, HostEntry("192.168.1.1", ["test.com"], "", True)] # Filter out None values before applying filters valid_entries = [e for e in entries_with_none if e is not None] result = entry_filter.apply_filters(valid_entries, empty_options) assert len(result) == 1 # Only the valid entry assert result[0].ip_address == "192.168.1.1" def test_search_multiple_hostnames(self, entry_filter): """Test search across multiple hostnames in single entry.""" # Create entry with multiple hostnames entry = HostEntry("192.168.1.1", ["primary.com", "secondary.com", "alias.org"], "Multi-hostname entry", True) entries = [entry] # Search for each hostname for hostname in ["primary", "secondary", "alias"]: options = FilterOptions(search_term=hostname) result = entry_filter.filter_by_search(entries, options) assert len(result) == 1 assert result[0] == entry def test_dns_resolution_age_filtering(self, entry_filter, sample_entries): """Test filtering based on DNS resolution age.""" # Modify sample entries to have different resolution times old_time = datetime.now() - timedelta(days=1) recent_time = datetime.now() - timedelta(minutes=5) # Make one entry have old resolution for entry in sample_entries: if entry.resolved_ip: if entry.hostnames[0] == "resolved.com": entry.last_resolved = recent_time else: entry.last_resolved = old_time # Test that entries are still found regardless of age # (Age filtering might be added in future versions) options = FilterOptions(resolved_only=True) result = entry_filter.filter_by_resolution_status(sample_entries, options) assert len(result) == 1 # Only entry5 has resolved status def test_preset_name_preservation(self, entry_filter): """Test that preset names are preserved in FilterOptions.""" preset_options = FilterOptions( active_only=True, preset_name="Active Only" ) # Apply filters and check preset name is preserved sample_entry = HostEntry("192.168.1.1", ["test.com"], "Test", True) entry_filter.apply_filters([sample_entry], preset_options) # The original preset name should be accessible assert preset_options.preset_name == "Active Only"