Add comprehensive tests for filtering and import/export functionality

- Created `test_filters.py` to test the EntryFilter and FilterOptions classes, covering default values, custom values, filtering by status, DNS type, resolution status, and search functionality.
- Implemented tests for combined filters and edge cases in filtering.
- Added `test_import_export.py` to test the ImportExportService class, including exporting to hosts, JSON, and CSV formats, as well as importing from these formats.
- Included tests for handling invalid formats, missing required columns, and warnings during import.
- Updated `uv.lock` to include `pytest-asyncio` as a dependency for asynchronous testing.
This commit is contained in:
Philip Henning 2025-08-18 10:32:52 +02:00
parent e6f3e9f3d4
commit 1c8396f020
21 changed files with 4988 additions and 266 deletions

546
tests/test_import_export.py Normal file
View file

@ -0,0 +1,546 @@
"""
Tests for the import/export functionality.
This module contains comprehensive tests for the ImportExportService class
and all supported file formats.
"""
import pytest
import json
import csv
import tempfile
from pathlib import Path
from datetime import datetime
from src.hosts.core.import_export import (
ImportExportService, ImportResult, ExportResult,
ExportFormat, ImportFormat
)
from src.hosts.core.models import HostEntry, HostsFile
class TestImportExportService:
"""Test ImportExportService class."""
@pytest.fixture
def service(self):
"""Create ImportExportService instance."""
return ImportExportService()
@pytest.fixture
def sample_hosts_file(self):
"""Create sample HostsFile for testing."""
entries = [
HostEntry("127.0.0.1", ["localhost"], "Local host", True),
HostEntry("192.168.1.1", ["router.local"], "Home router", True),
HostEntry("1.1.1.1", ["dns-only.com"], "DNS only entry", False), # Temp IP
HostEntry("10.0.0.1", ["test.example.com"], "Test server", True)
]
# Convert to DNS entry and set DNS data for some entries
entries[2].ip_address = "" # Remove IP after creation
entries[2].dns_name = "dns-only.com"
entries[3].resolved_ip = "10.0.0.1"
entries[3].last_resolved = datetime(2024, 1, 15, 12, 0, 0)
entries[3].dns_resolution_status = "IP_MATCH"
hosts_file = HostsFile()
hosts_file.entries = entries
return hosts_file
@pytest.fixture
def temp_dir(self):
"""Create temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_service_initialization(self, service):
"""Test service initialization."""
assert len(service.supported_export_formats) == 3
assert len(service.supported_import_formats) == 3
assert ExportFormat.HOSTS in service.supported_export_formats
assert ExportFormat.JSON in service.supported_export_formats
assert ExportFormat.CSV in service.supported_export_formats
def test_get_supported_formats(self, service):
"""Test getting supported formats."""
export_formats = service.get_supported_export_formats()
import_formats = service.get_supported_import_formats()
assert len(export_formats) == 3
assert len(import_formats) == 3
assert ExportFormat.HOSTS in export_formats
assert ImportFormat.JSON in import_formats
# Export Tests
def test_export_hosts_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to hosts format."""
export_path = temp_dir / "test_hosts.txt"
result = service.export_hosts_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.HOSTS
assert export_path.exists()
# Verify content
content = export_path.read_text()
assert "127.0.0.1" in content
assert "localhost" in content
assert "router.local" in content
def test_export_json_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to JSON format."""
export_path = temp_dir / "test_export.json"
result = service.export_json_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.JSON
assert export_path.exists()
# Verify JSON structure
with open(export_path, 'r') as f:
data = json.load(f)
assert "metadata" in data
assert "entries" in data
assert data["metadata"]["total_entries"] == 4
assert len(data["entries"]) == 4
# Check first entry
first_entry = data["entries"][0]
assert first_entry["ip_address"] == "127.0.0.1"
assert first_entry["hostnames"] == ["localhost"]
assert first_entry["is_active"] is True
# Check DNS entry
dns_entry = next((e for e in data["entries"] if e.get("dns_name")), None)
assert dns_entry is not None
assert dns_entry["dns_name"] == "dns-only.com"
def test_export_csv_format(self, service, sample_hosts_file, temp_dir):
"""Test exporting to CSV format."""
export_path = temp_dir / "test_export.csv"
result = service.export_csv_format(sample_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 4
assert len(result.errors) == 0
assert result.format == ExportFormat.CSV
assert export_path.exists()
# Verify CSV structure
with open(export_path, 'r') as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == 4
# Check header
expected_fields = [
'ip_address', 'hostnames', 'comment', 'is_active',
'dns_name', 'resolved_ip', 'last_resolved', 'dns_resolution_status'
]
assert reader.fieldnames == expected_fields
# Check first row
first_row = rows[0]
assert first_row["ip_address"] == "127.0.0.1"
assert first_row["hostnames"] == "localhost"
assert first_row["is_active"] == "True"
def test_export_invalid_path(self, service, sample_hosts_file):
"""Test export with invalid path."""
invalid_path = Path("/invalid/path/test.json")
result = service.export_json_format(sample_hosts_file, invalid_path)
assert result.success is False
assert result.entries_exported == 0
assert len(result.errors) > 0
assert "Failed to export JSON format" in result.errors[0]
# Import Tests
def test_import_hosts_format(self, service, temp_dir):
"""Test importing from hosts format."""
# Create test hosts file
hosts_content = """# Test hosts file
127.0.0.1 localhost
192.168.1.1 router.local # Home router
# 10.0.0.1 disabled.com # Disabled entry
"""
hosts_path = temp_dir / "test_hosts.txt"
hosts_path.write_text(hosts_content)
result = service.import_hosts_format(hosts_path)
assert result.success is True
assert result.total_processed >= 2
assert result.successfully_imported >= 2
assert len(result.errors) == 0
# Check imported entries
assert len(result.entries) >= 2
localhost_entry = next((e for e in result.entries if "localhost" in e.hostnames), None)
assert localhost_entry is not None
assert localhost_entry.ip_address == "127.0.0.1"
assert localhost_entry.is_active is True
def test_import_json_format(self, service, temp_dir):
"""Test importing from JSON format."""
# Create test JSON file
json_data = {
"metadata": {
"exported_at": "2024-01-15T12:00:00",
"total_entries": 3,
"version": "1.0"
},
"entries": [
{
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": "Local host",
"is_active": True
},
{
"ip_address": "",
"hostnames": ["dns-only.com"],
"comment": "DNS only",
"is_active": False,
"dns_name": "dns-only.com"
},
{
"ip_address": "10.0.0.1",
"hostnames": ["test.com"],
"comment": "Test",
"is_active": True,
"resolved_ip": "10.0.0.1",
"last_resolved": "2024-01-15T12:00:00",
"dns_resolution_status": "IP_MATCH"
}
]
}
json_path = temp_dir / "test_import.json"
with open(json_path, 'w') as f:
json.dump(json_data, f)
result = service.import_json_format(json_path)
assert result.success is True
assert result.total_processed == 3
assert result.successfully_imported == 3
assert len(result.errors) == 0
assert len(result.entries) == 3
# Check DNS entry
dns_entry = next((e for e in result.entries if e.dns_name), None)
assert dns_entry is not None
assert dns_entry.dns_name == "dns-only.com"
assert dns_entry.ip_address == ""
# Check resolved entry
resolved_entry = next((e for e in result.entries if e.resolved_ip), None)
assert resolved_entry is not None
assert resolved_entry.resolved_ip == "10.0.0.1"
assert resolved_entry.dns_resolution_status == "IP_MATCH"
def test_import_csv_format(self, service, temp_dir):
"""Test importing from CSV format."""
# Create test CSV file
csv_path = temp_dir / "test_import.csv"
with open(csv_path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'ip_address', 'hostnames', 'comment', 'is_active',
'dns_name', 'resolved_ip', 'last_resolved', 'dns_resolution_status'
])
writer.writerow([
'127.0.0.1', 'localhost', 'Local host', 'true',
'', '', '', ''
])
writer.writerow([
'', 'dns-only.com', 'DNS only', 'false',
'dns-only.com', '', '', ''
])
writer.writerow([
'10.0.0.1', 'test.com example.com', 'Test server', 'true',
'', '10.0.0.1', '2024-01-15T12:00:00', 'IP_MATCH'
])
result = service.import_csv_format(csv_path)
assert result.success is True
assert result.total_processed == 3
assert result.successfully_imported == 3
assert len(result.errors) == 0
assert len(result.entries) == 3
# Check multiple hostnames entry
multi_hostname_entry = next((e for e in result.entries if "test.com" in e.hostnames), None)
assert multi_hostname_entry is not None
assert "example.com" in multi_hostname_entry.hostnames
assert len(multi_hostname_entry.hostnames) == 2
def test_import_json_invalid_format(self, service, temp_dir):
"""Test importing invalid JSON format."""
# Create invalid JSON file
invalid_json = {"invalid": "format", "no_entries": True}
json_path = temp_dir / "invalid.json"
with open(json_path, 'w') as f:
json.dump(invalid_json, f)
result = service.import_json_format(json_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
assert len(result.errors) > 0
assert "missing 'entries' field" in result.errors[0]
def test_import_json_malformed(self, service, temp_dir):
"""Test importing malformed JSON."""
json_path = temp_dir / "malformed.json"
json_path.write_text("{invalid json content")
result = service.import_json_format(json_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
assert len(result.errors) > 0
assert "Invalid JSON file" in result.errors[0]
def test_import_csv_missing_required_columns(self, service, temp_dir):
"""Test importing CSV with missing required columns."""
csv_path = temp_dir / "missing_columns.csv"
with open(csv_path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['ip_address', 'comment']) # Missing 'hostnames'
writer.writerow(['127.0.0.1', 'test'])
result = service.import_csv_format(csv_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
assert len(result.errors) > 0
assert "Missing required columns" in result.errors[0]
def test_import_json_with_warnings(self, service, temp_dir):
"""Test importing JSON with warnings (invalid dates)."""
json_data = {
"entries": [
{
"ip_address": "127.0.0.1",
"hostnames": ["localhost"],
"comment": "Test",
"is_active": True,
"last_resolved": "invalid-date-format"
}
]
}
json_path = temp_dir / "warnings.json"
with open(json_path, 'w') as f:
json.dump(json_data, f)
result = service.import_json_format(json_path)
assert result.success is True
assert result.total_processed == 1
assert result.successfully_imported == 1
assert len(result.warnings) > 0
assert "Invalid last_resolved date format" in result.warnings[0]
def test_import_nonexistent_file(self, service):
"""Test importing non-existent file."""
nonexistent_path = Path("/nonexistent/file.json")
result = service.import_json_format(nonexistent_path)
assert result.success is False
assert result.total_processed == 0
assert result.successfully_imported == 0
assert len(result.errors) > 0
# Utility Tests
def test_detect_file_format_by_extension(self, service, temp_dir):
"""Test file format detection by extension."""
json_file = temp_dir / "test.json"
csv_file = temp_dir / "test.csv"
hosts_file = temp_dir / "hosts"
txt_file = temp_dir / "test.txt"
# Create empty files
for f in [json_file, csv_file, hosts_file, txt_file]:
f.touch()
assert service.detect_file_format(json_file) == ImportFormat.JSON
assert service.detect_file_format(csv_file) == ImportFormat.CSV
assert service.detect_file_format(hosts_file) == ImportFormat.HOSTS
assert service.detect_file_format(txt_file) == ImportFormat.HOSTS
def test_detect_file_format_by_content(self, service, temp_dir):
"""Test file format detection by content."""
# JSON content
json_file = temp_dir / "no_extension"
json_file.write_text('{"entries": []}')
assert service.detect_file_format(json_file) == ImportFormat.JSON
# CSV content
csv_file = temp_dir / "csv_no_ext"
csv_file.write_text('ip_address,hostnames,comment')
assert service.detect_file_format(csv_file) == ImportFormat.CSV
# Hosts content
hosts_file = temp_dir / "hosts_no_ext"
hosts_file.write_text('127.0.0.1 localhost')
assert service.detect_file_format(hosts_file) == ImportFormat.HOSTS
def test_detect_file_format_nonexistent(self, service):
"""Test file format detection for non-existent file."""
result = service.detect_file_format(Path("/nonexistent/file.txt"))
assert result is None
def test_validate_export_path(self, service, temp_dir):
"""Test export path validation."""
# Valid path
valid_path = temp_dir / "export.json"
warnings = service.validate_export_path(valid_path, ExportFormat.JSON)
assert len(warnings) == 0
# Existing file
existing_file = temp_dir / "existing.json"
existing_file.touch()
warnings = service.validate_export_path(existing_file, ExportFormat.JSON)
assert any("already exists" in w for w in warnings)
# Wrong extension
wrong_ext = temp_dir / "file.txt"
warnings = service.validate_export_path(wrong_ext, ExportFormat.JSON)
assert any("doesn't match format" in w for w in warnings)
def test_validate_export_path_invalid_directory(self, service):
"""Test export path validation with invalid directory."""
invalid_path = Path("/invalid/nonexistent/directory/file.json")
warnings = service.validate_export_path(invalid_path, ExportFormat.JSON)
assert any("does not exist" in w for w in warnings)
# Integration Tests
def test_export_import_roundtrip_json(self, service, sample_hosts_file, temp_dir):
"""Test export-import roundtrip for JSON format."""
export_path = temp_dir / "roundtrip.json"
# Export
export_result = service.export_json_format(sample_hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_json_format(export_path)
assert import_result.success is True
assert import_result.successfully_imported == len(sample_hosts_file.entries)
# Verify data integrity
original_entries = sample_hosts_file.entries
imported_entries = import_result.entries
assert len(imported_entries) == len(original_entries)
# Check specific entries
for orig, imported in zip(original_entries, imported_entries):
assert orig.ip_address == imported.ip_address
assert orig.hostnames == imported.hostnames
assert orig.comment == imported.comment
assert orig.is_active == imported.is_active
assert orig.dns_name == imported.dns_name
def test_export_import_roundtrip_csv(self, service, sample_hosts_file, temp_dir):
"""Test export-import roundtrip for CSV format."""
export_path = temp_dir / "roundtrip.csv"
# Export
export_result = service.export_csv_format(sample_hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_csv_format(export_path)
assert import_result.success is True
assert import_result.successfully_imported == len(sample_hosts_file.entries)
def test_import_result_properties(self):
"""Test ImportResult properties."""
# Result with errors
result_with_errors = ImportResult(
success=False,
entries=[],
errors=["Error 1", "Error 2"],
warnings=[],
total_processed=5,
successfully_imported=0
)
assert result_with_errors.has_errors is True
assert result_with_errors.has_warnings is False
# Result with warnings
result_with_warnings = ImportResult(
success=True,
entries=[],
errors=[],
warnings=["Warning 1"],
total_processed=5,
successfully_imported=5
)
assert result_with_warnings.has_errors is False
assert result_with_warnings.has_warnings is True
def test_empty_hosts_file_export(self, service, temp_dir):
"""Test exporting empty hosts file."""
empty_hosts_file = HostsFile()
export_path = temp_dir / "empty.json"
result = service.export_json_format(empty_hosts_file, export_path)
assert result.success is True
assert result.entries_exported == 0
assert export_path.exists()
# Verify empty file structure
with open(export_path, 'r') as f:
data = json.load(f)
assert data["metadata"]["total_entries"] == 0
assert len(data["entries"]) == 0
def test_large_hostnames_list_csv(self, service, temp_dir):
"""Test CSV export/import with large hostnames list."""
entry = HostEntry(
"192.168.1.1",
["host1.com", "host2.com", "host3.com", "host4.com", "host5.com"],
"Multiple hostnames",
True
)
hosts_file = HostsFile()
hosts_file.entries = [entry]
export_path = temp_dir / "multi_hostnames.csv"
# Export
export_result = service.export_csv_format(hosts_file, export_path)
assert export_result.success is True
# Import
import_result = service.import_csv_format(export_path)
assert import_result.success is True
imported_entry = import_result.entries[0]
assert len(imported_entry.hostnames) == 5
assert "host1.com" in imported_entry.hostnames
assert "host5.com" in imported_entry.hostnames