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:
parent
e6f3e9f3d4
commit
1c8396f020
21 changed files with 4988 additions and 266 deletions
546
tests/test_import_export.py
Normal file
546
tests/test_import_export.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue