""" Unit tests for DNS service. Tests TXT record querying, domain verification, and error handling. Uses mocking to avoid actual DNS queries. """ from unittest.mock import MagicMock, patch import pytest import dns.resolver from dns.exception import DNSException from gondulf.dns import DNSError, DNSService class TestDNSServiceInit: """Tests for DNSService initialization.""" def test_init_creates_resolver(self): """Test DNSService initializes with resolver.""" service = DNSService() assert service.resolver is not None assert isinstance(service.resolver, dns.resolver.Resolver) class TestGetTxtRecords: """Tests for get_txt_records method.""" @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_success(self, mock_resolve): """Test getting TXT records successfully.""" # Mock TXT record response mock_rdata = MagicMock() mock_rdata.strings = [b"v=spf1 include:example.com ~all"] mock_resolve.return_value = [mock_rdata] service = DNSService() records = service.get_txt_records("example.com") assert len(records) == 1 assert records[0] == "v=spf1 include:example.com ~all" mock_resolve.assert_called_once_with("example.com", "TXT") @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_multiple(self, mock_resolve): """Test getting multiple TXT records.""" # Mock multiple TXT records mock_rdata1 = MagicMock() mock_rdata1.strings = [b"record1"] mock_rdata2 = MagicMock() mock_rdata2.strings = [b"record2"] mock_resolve.return_value = [mock_rdata1, mock_rdata2] service = DNSService() records = service.get_txt_records("example.com") assert len(records) == 2 assert "record1" in records assert "record2" in records @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_multipart(self, mock_resolve): """Test getting TXT record with multiple strings (joined).""" # Mock TXT record with multiple strings mock_rdata = MagicMock() mock_rdata.strings = [b"part1", b"part2", b"part3"] mock_resolve.return_value = [mock_rdata] service = DNSService() records = service.get_txt_records("example.com") assert len(records) == 1 assert records[0] == "part1part2part3" @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_no_answer(self, mock_resolve): """Test getting TXT records when none exist returns empty list.""" mock_resolve.side_effect = dns.resolver.NoAnswer() service = DNSService() records = service.get_txt_records("example.com") assert records == [] @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_nxdomain(self, mock_resolve): """Test DNSError raised when domain doesn't exist.""" mock_resolve.side_effect = dns.resolver.NXDOMAIN() service = DNSService() with pytest.raises(DNSError, match="Domain does not exist"): service.get_txt_records("nonexistent.example") @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_timeout(self, mock_resolve): """Test DNSError raised on timeout.""" mock_resolve.side_effect = dns.resolver.Timeout() service = DNSService() with pytest.raises(DNSError, match="timeout"): service.get_txt_records("example.com") @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_get_txt_records_dns_exception(self, mock_resolve): """Test DNSError raised on other DNS exceptions.""" mock_resolve.side_effect = DNSException("DNS query failed") service = DNSService() with pytest.raises(DNSError, match="DNS query failed"): service.get_txt_records("example.com") class TestVerifyTxtRecord: """Tests for verify_txt_record method.""" @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_success(self, mock_resolve): """Test TXT record verification succeeds when value found.""" mock_rdata = MagicMock() mock_rdata.strings = [b"gondulf-verify=ABC123"] mock_resolve.return_value = [mock_rdata] service = DNSService() result = service.verify_txt_record("example.com", "gondulf-verify=ABC123") assert result is True @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_partial_match(self, mock_resolve): """Test TXT record verification succeeds with partial match.""" mock_rdata = MagicMock() mock_rdata.strings = [b"some prefix gondulf-verify=ABC123 some suffix"] mock_resolve.return_value = [mock_rdata] service = DNSService() result = service.verify_txt_record("example.com", "gondulf-verify=ABC123") assert result is True @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_not_found(self, mock_resolve): """Test TXT record verification fails when value not found.""" mock_rdata = MagicMock() mock_rdata.strings = [b"different-value"] mock_resolve.return_value = [mock_rdata] service = DNSService() result = service.verify_txt_record("example.com", "gondulf-verify=ABC123") assert result is False @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_no_txt_records(self, mock_resolve): """Test TXT record verification fails when no TXT records exist.""" mock_resolve.side_effect = dns.resolver.NoAnswer() service = DNSService() result = service.verify_txt_record("example.com", "gondulf-verify=ABC123") assert result is False @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_nxdomain(self, mock_resolve): """Test TXT record verification fails when domain doesn't exist.""" mock_resolve.side_effect = dns.resolver.NXDOMAIN() service = DNSService() result = service.verify_txt_record("nonexistent.example", "value") assert result is False @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_timeout(self, mock_resolve): """Test TXT record verification fails on timeout.""" mock_resolve.side_effect = dns.resolver.Timeout() service = DNSService() result = service.verify_txt_record("example.com", "value") assert result is False @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_verify_txt_record_among_multiple(self, mock_resolve): """Test TXT record verification finds value among multiple records.""" mock_rdata1 = MagicMock() mock_rdata1.strings = [b"unrelated-record"] mock_rdata2 = MagicMock() mock_rdata2.strings = [b"gondulf-verify=ABC123"] mock_rdata3 = MagicMock() mock_rdata3.strings = [b"another-record"] mock_resolve.return_value = [mock_rdata1, mock_rdata2, mock_rdata3] service = DNSService() result = service.verify_txt_record("example.com", "gondulf-verify=ABC123") assert result is True class TestCheckDomainExists: """Tests for check_domain_exists method.""" @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_check_domain_exists_a_record(self, mock_resolve): """Test domain exists check succeeds with A record.""" mock_resolve.return_value = [MagicMock()] service = DNSService() result = service.check_domain_exists("example.com") assert result is True mock_resolve.assert_called_with("example.com", "A") @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_check_domain_exists_aaaa_record(self, mock_resolve): """Test domain exists check succeeds with AAAA record.""" # First call (A record) fails, second call (AAAA) succeeds mock_resolve.side_effect = [ dns.resolver.NoAnswer(), [MagicMock()], # AAAA record exists ] service = DNSService() result = service.check_domain_exists("example.com") assert result is True @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_check_domain_exists_no_records(self, mock_resolve): """Test domain exists check succeeds even with no A/AAAA records.""" # Both A and AAAA fail with NoAnswer (but not NXDOMAIN) mock_resolve.side_effect = [ dns.resolver.NoAnswer(), dns.resolver.NoAnswer(), ] service = DNSService() result = service.check_domain_exists("example.com") # Domain exists even if no A/AAAA records (might have MX, TXT, etc.) assert result is True @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_check_domain_not_exists_nxdomain(self, mock_resolve): """Test domain exists check fails with NXDOMAIN.""" mock_resolve.side_effect = dns.resolver.NXDOMAIN() service = DNSService() result = service.check_domain_exists("nonexistent.example") assert result is False @patch("gondulf.dns.dns.resolver.Resolver.resolve") def test_check_domain_exists_dns_error(self, mock_resolve): """Test domain exists check returns False on DNS error.""" mock_resolve.side_effect = DNSException("DNS failure") service = DNSService() result = service.check_domain_exists("example.com") assert result is False class TestResolverFallback: """Tests for DNS resolver fallback configuration.""" @patch("gondulf.dns.dns.resolver.Resolver") def test_resolver_uses_system_dns(self, mock_resolver_class): """Test resolver uses system DNS when available.""" mock_resolver = MagicMock() mock_resolver.nameservers = ["192.168.1.1"] # System DNS mock_resolver_class.return_value = mock_resolver service = DNSService() # System DNS should be used assert service.resolver.nameservers == ["192.168.1.1"] @patch("gondulf.dns.dns.resolver.Resolver") def test_resolver_fallback_to_public_dns(self, mock_resolver_class): """Test resolver falls back to public DNS when system DNS unavailable.""" mock_resolver = MagicMock() mock_resolver.nameservers = [] # No system DNS mock_resolver_class.return_value = mock_resolver service = DNSService() # Should fall back to public DNS assert service.resolver.nameservers == ["8.8.8.8", "1.1.1.1"]