| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- # coding=utf-8
- # type: ignore[index]
- """
- Unit tests for remote configuration loading in ddns.config.file module
- @author: GitHub Copilot
- """
- from __future__ import unicode_literals
- from __init__ import unittest, patch
- import tempfile
- import shutil
- import os
- import json
- import sys
- import socket
- from ddns.config.file import load_config
- from ddns.util.http import HttpResponse
- # Import HTTP exceptions for Python 2/3 compatibility
- try: # Python 3
- from urllib.error import URLError, HTTPError
- from io import StringIO
- unicode = str
- except ImportError: # Python 2
- from urllib2 import URLError, HTTPError # type: ignore[no-redef]
- from StringIO import StringIO # type: ignore[no-redef]
- else:
- try:
- from StringIO import StringIO # type: ignore[no-redef]
- except ImportError:
- from io import StringIO # type: ignore[no-redef]
- class TestRemoteConfigFile(unittest.TestCase):
- """Test cases for remote configuration file loading via HTTP(S)"""
- def setUp(self):
- """Set up test fixtures"""
- self.temp_dir = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, self.temp_dir, ignore_errors=True)
- # Capture stdout and stderr output for testing
- self.stdout_capture = StringIO()
- self.stderr_capture = StringIO()
- self.original_stdout = sys.stdout
- self.original_stderr = sys.stderr
- def tearDown(self):
- """Clean up after tests"""
- sys.stdout = self.original_stdout
- sys.stderr = self.original_stderr
- @patch("ddns.config.file.request")
- def test_load_config_remote_http_success(self, mock_http):
- """Test loading configuration from HTTP URL"""
- # Patch stdout to capture output
- import ddns.config.file
- original_stdout = ddns.config.file.stdout
- ddns.config.file.stdout = self.stdout_capture
- try:
- config_data = {"dns": "cloudflare", "id": "[email protected]", "token": "secret123"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "http://example.com/config.json"
- result = load_config(config_url)
- self.assertEqual(result, config_data)
- mock_http.assert_called_once_with("GET", config_url, proxies=None, verify="auto", retries=3)
- finally:
- ddns.config.file.stdout = original_stdout
- @patch("ddns.config.file.request")
- def test_load_config_remote_https_success(self, mock_http):
- """Test loading configuration from HTTPS URL"""
- config_data = {"dns": "dnspod", "id": "test123", "token": "abc456"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://secure.example.com/config.json"
- result = load_config(config_url, ssl=True)
- self.assertEqual(result, config_data)
- mock_http.assert_called_once_with("GET", config_url, proxies=None, verify=True, retries=3)
- @patch("ddns.config.file.request")
- def test_load_config_remote_with_proxy(self, mock_http):
- """Test loading configuration from remote URL with proxy settings"""
- config_data = {"dns": "alidns", "id": "test", "token": "xyz"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://example.com/config.json"
- proxy_list = ["http://proxy1.example.com:8080", "DIRECT"]
- result = load_config(config_url, proxy=proxy_list, ssl=False)
- self.assertEqual(result, config_data)
- mock_http.assert_called_once_with("GET", config_url, proxies=proxy_list, verify=False, retries=3)
- @patch("ddns.config.file.request")
- def test_load_config_remote_with_embedded_auth(self, mock_http):
- """Test loading configuration from URL with embedded authentication"""
- config_data = {"dns": "cloudflare", "ttl": 300}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://user:[email protected]/secure/config.json"
- result = load_config(config_url)
- self.assertEqual(result, config_data)
- # The HTTP module handles embedded auth automatically
- mock_http.assert_called_once_with("GET", config_url, proxies=None, verify="auto", retries=3)
- @patch("ddns.config.file.request")
- def test_load_config_remote_http_error(self, mock_http):
- """Test handling HTTP error responses"""
- mock_http.return_value = HttpResponse(404, "Not Found", {}, "Not Found")
- config_url = "https://example.com/missing.json"
- with self.assertRaises(Exception) as context:
- load_config(config_url)
- self.assertIn("HTTP 404: Not Found", str(context.exception))
- @patch("ddns.config.file.request")
- def test_load_config_remote_http_500_error(self, mock_http):
- """Test handling HTTP 5xx server errors"""
- mock_http.return_value = HttpResponse(500, "Internal Server Error", {}, "Server Error")
- config_url = "https://example.com/config.json"
- with self.assertRaises(Exception) as context:
- load_config(config_url)
- self.assertIn("HTTP 500: Internal Server Error", str(context.exception))
- @patch("ddns.config.file.request")
- def test_load_config_remote_network_error(self, mock_http):
- """Test handling network errors during HTTP request"""
- mock_http.side_effect = URLError("Network is unreachable")
- config_url = "https://unreachable.example.com/config.json"
- with self.assertRaises(Exception):
- load_config(config_url)
- @patch("ddns.config.file.request")
- def test_load_config_remote_invalid_json(self, mock_http):
- """Test handling invalid JSON in remote response"""
- # Invalid JSON content
- invalid_json = '{"dns": "test", invalid syntax}'
- mock_http.return_value = HttpResponse(200, "OK", {}, invalid_json)
- config_url = "https://example.com/bad-config.json"
- with self.assertRaises((ValueError, SyntaxError)):
- load_config(config_url)
- @patch("ddns.config.file.request")
- def test_load_config_remote_ast_fallback(self, mock_http):
- """Test AST parsing fallback for remote content"""
- # Patch stdout to capture output
- import ddns.config.file
- original_stdout = ddns.config.file.stdout
- ddns.config.file.stdout = self.stdout_capture
- try:
- # Valid Python dict but invalid JSON (trailing comma)
- python_content = '{"dns": "alidns", "id": "test", "token": "xyz",}'
- mock_http.return_value = HttpResponse(200, "OK", {}, python_content)
- config_url = "https://example.com/config.py"
- result = load_config(config_url)
- expected = {"dns": "alidns", "id": "test", "token": "xyz"}
- self.assertEqual(result, expected)
- # Verify AST fallback success message
- stdout_output = self.stdout_capture.getvalue()
- self.assertIn("Successfully loaded config file with AST parser", stdout_output)
- finally:
- ddns.config.file.stdout = original_stdout
- @patch("ddns.config.file.request")
- def test_load_config_remote_v41_providers_format(self, mock_http):
- """Test loading remote configuration with v4.1 providers format"""
- config_data = {
- "$schema": "https://ddns.newfuture.cc/schema/v4.1.json",
- "ssl": "auto",
- "cache": True,
- "providers": [
- {"provider": "cloudflare", "id": "[email protected]", "token": "token1", "ipv4": ["test1.example.com"]},
- {"provider": "dnspod", "id": "[email protected]", "token": "token2", "ipv4": ["test2.example.com"]},
- ],
- }
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://example.com/multi-provider.json"
- result = load_config(config_url)
- # Should return a list of configs
- self.assertIsInstance(result, list)
- self.assertEqual(len(result), 2)
- # Test first provider config
- config1 = result[0]
- self.assertEqual(config1["dns"], "cloudflare")
- self.assertEqual(config1["id"], "[email protected]")
- self.assertEqual(config1["ssl"], "auto") # Global config inherited
- # Test second provider config
- config2 = result[1]
- self.assertEqual(config2["dns"], "dnspod")
- self.assertEqual(config2["id"], "[email protected]")
- self.assertEqual(config2["ssl"], "auto") # Global config inherited
- @patch("ddns.config.file.request")
- def test_load_config_remote_with_comments(self, mock_http):
- """Test loading remote configuration with comments"""
- json_with_comments = """{
- // Remote configuration for DDNS
- "dns": "cloudflare", // DNS provider
- "id": "[email protected]",
- "token": "secret123", // API token
- "ttl": 300
- // End of config
- }"""
- mock_http.return_value = HttpResponse(200, "OK", {}, json_with_comments)
- config_url = "https://example.com/config-with-comments.json"
- result = load_config(config_url)
- expected = {"dns": "cloudflare", "id": "[email protected]", "token": "secret123", "ttl": 300}
- self.assertEqual(result, expected)
- @patch("ddns.config.file.request")
- def test_load_config_remote_unicode_content(self, mock_http):
- """Test loading remote configuration with unicode characters"""
- unicode_config = {"dns": "cloudflare", "description": "测试配置文件", "symbols": "αβγδε", "emoji": "🌍🔧⚡"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(unicode_config, ensure_ascii=False))
- config_url = "https://example.com/unicode-config.json"
- result = load_config(config_url)
- self.assertEqual(result["dns"], "cloudflare")
- self.assertEqual(result["description"], "测试配置文件")
- self.assertEqual(result["symbols"], "αβγδε")
- self.assertEqual(result["emoji"], "🌍🔧⚡")
- def test_load_config_local_file_still_works(self):
- """Test that local file loading still works without changes"""
- # Create a local test file
- config_data = {"dns": "local", "id": "test", "token": "local123"}
- config_file = os.path.join(self.temp_dir, "local.json")
- with open(config_file, "w") as f:
- json.dump(config_data, f)
- # Load local file
- result = load_config(config_file)
- self.assertEqual(result, config_data)
- def test_load_config_url_detection(self):
- """Test URL detection logic works correctly"""
- # These should be detected as URLs
- urls = [
- "http://example.com/config.json",
- "https://example.com/config.json",
- "ftp://example.com/config.json",
- "file://path/to/config.json",
- ]
- # These should NOT be detected as URLs
- non_urls = [
- "/path/to/config.json",
- "./config.json",
- "config.json",
- "C:\\path\\to\\config.json",
- "~/config.json",
- ]
- # Test URL detection (we'll mock the HTTP request to avoid actual network calls)
- with patch("ddns.config.file.request") as mock_http:
- mock_http.return_value = HttpResponse(200, "OK", {}, '{"dns": "test"}')
- for url in urls:
- try:
- load_config(url)
- mock_http.assert_called_with("GET", url, proxies=None, verify="auto", retries=3)
- except Exception:
- pass # We're just testing URL detection, not full functionality
- # Reset mock call count
- mock_http.reset_mock()
- # Test non-URLs (these should not trigger HTTP requests)
- for non_url in non_urls:
- try:
- load_config(non_url) # This will fail because files don't exist, but shouldn't call HTTP
- except Exception:
- pass # Expected - files don't exist
- # HTTP request should not have been called for non-URLs
- mock_http.assert_not_called()
- @patch("ddns.config.file.request")
- def test_load_config_remote_ssl_configurations(self, mock_http):
- """Test different SSL verification configurations"""
- config_data = {"dns": "test"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://example.com/config.json"
- # Test different SSL settings
- ssl_configs = [True, False, "auto", "/path/to/cert.pem"]
- for ssl_config in ssl_configs:
- load_config(config_url, ssl=ssl_config)
- mock_http.assert_called_with("GET", config_url, proxies=None, verify=ssl_config, retries=3)
- mock_http.reset_mock()
- @patch("ddns.config.file.request")
- def test_load_config_remote_proxy_configurations(self, mock_http):
- """Test different proxy configurations"""
- config_data = {"dns": "test"}
- mock_http.return_value = HttpResponse(200, "OK", {}, json.dumps(config_data))
- config_url = "https://example.com/config.json"
- # Test different proxy settings
- proxy_configs = [
- None,
- [],
- ["http://proxy.example.com:8080"],
- ["http://proxy1.com:8080", "http://proxy2.com:8080"],
- ["DIRECT"],
- ["SYSTEM"],
- ["http://proxy.com:8080", "DIRECT"],
- ]
- for proxy_config in proxy_configs:
- load_config(config_url, proxy=proxy_config)
- mock_http.assert_called_with("GET", config_url, proxies=proxy_config, verify="auto", retries=3)
- mock_http.reset_mock()
- def test_load_config_real_remote_url(self):
- """Test loading configuration from the actual remote URL for real integration testing"""
- # This tests the real URL provided in the specification
- config_url = "https://ddns.newfuture.cc/tests/config/debug.json"
- # This is a real integration test - it should succeed if the URL is accessible
- # If the URL is not accessible due to network issues, the test will be skipped
- try:
- result = load_config(config_url)
- # Handle both single config (dict) and multi-provider config (list)
- if isinstance(result, list):
- # Multi-provider format - verify we got at least one configuration
- self.assertGreater(len(result), 0, "Should load at least one configuration")
- config = result[0]
- else:
- # Single provider format
- config = result
- # Verify that the config has the expected structure
- self.assertIsInstance(config, dict, "Config should be a dictionary")
- # Check for at least one expected field (debug is common in debug configs)
- self.assertTrue(
- "debug" in config or "dns" in config or "id" in config,
- "Config should have at least one expected field (debug, dns, or id)",
- )
- except (URLError, HTTPError, socket.timeout, socket.gaierror, socket.herror) as e:
- # Only skip for network connection issues (URLError, HTTPError 5xx, timeout)
- if isinstance(e, HTTPError):
- # For HTTPError, only skip if it's a server error (5xx)
- if e.code >= 500:
- self.skipTest("Real remote URL test skipped due to server error %s: %s" % (e.code, str(e)))
- else:
- # For client errors (4xx), the test should fail as it indicates a real problem
- self.fail("Remote URL returned client error %s: %s" % (e.code, str(e)))
- else:
- # For URLError, socket errors, skip the test
- self.skipTest("Real remote URL test skipped due to network error: %s" % str(e))
- except Exception as e:
- # For other exceptions (like JSON parsing errors), the test should fail
- self.fail("Real remote URL test failed with unexpected error: %s" % str(e))
- if __name__ == "__main__":
- unittest.main()
|