mirror of
https://github.com/NewFuture/DDNS.git
synced 2026-10-19 16:15:45 +00:00
* Initial plan * refactor(tests): replace authid with id in all tests for consistency Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
390 lines
15 KiB
Python
390 lines
15 KiB
Python
# coding=utf-8
|
|
"""
|
|
Unit tests for NoipProvider (No-IP)
|
|
|
|
@author: GitHub Copilot
|
|
"""
|
|
|
|
from base_test import BaseProviderTestCase, unittest, patch, MagicMock
|
|
from ddns.provider.noip import NoipProvider
|
|
|
|
|
|
class TestNoipProvider(BaseProviderTestCase):
|
|
"""Test cases for NoipProvider"""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures"""
|
|
super(TestNoipProvider, self).setUp()
|
|
# No-IP uses both id (username) and token (password)
|
|
self.id = "test_username"
|
|
self.token = "test_password"
|
|
|
|
def test_init_with_basic_config(self):
|
|
"""Test NoipProvider initialization with basic configuration"""
|
|
provider = NoipProvider(self.id, self.token)
|
|
self.assertEqual(provider.id, self.id)
|
|
self.assertEqual(provider.token, self.token)
|
|
# After validation, endpoint should include authentication
|
|
self.assertEqual(provider.endpoint, "https://test_username:test_password@dynupdate.no-ip.com")
|
|
self.assertFalse(provider.decode_response)
|
|
|
|
def test_class_constants(self):
|
|
"""Test NoipProvider class constants"""
|
|
provider = NoipProvider(self.id, self.token)
|
|
# After validation, endpoint should include authentication
|
|
self.assertEqual(provider.endpoint, "https://test_username:test_password@dynupdate.no-ip.com")
|
|
self.assertFalse(provider.decode_response)
|
|
self.assertIsNone(provider.accept)
|
|
# ContentType should be form-encoded
|
|
from ddns.provider._base import TYPE_FORM
|
|
|
|
self.assertEqual(provider.content_type, TYPE_FORM)
|
|
|
|
def test_validate_success_with_credentials(self):
|
|
"""Test _validate method passes with proper credentials"""
|
|
provider = NoipProvider(self.id, self.token)
|
|
# Should not raise any exception
|
|
provider._validate()
|
|
|
|
def test_validate_fails_without_id(self):
|
|
"""Test _validate method fails when id is missing"""
|
|
with self.assertRaises(ValueError) as cm:
|
|
NoipProvider("", self.token)
|
|
self.assertIn("No-IP requires username", str(cm.exception))
|
|
|
|
def test_validate_fails_without_token(self):
|
|
"""Test _validate method fails when token is missing"""
|
|
with self.assertRaises(ValueError) as cm:
|
|
NoipProvider(self.id, "")
|
|
self.assertIn("No-IP requires password", str(cm.exception))
|
|
|
|
def test_validate_fails_with_both_missing(self):
|
|
"""Test _validate method fails when both credentials are missing"""
|
|
with self.assertRaises(ValueError) as cm:
|
|
NoipProvider("", "")
|
|
self.assertIn("No-IP requires username", str(cm.exception))
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_success_good_response(self, mock_http):
|
|
"""Test set_record method with 'good' response"""
|
|
mock_http.return_value = "good 192.168.1.1"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1", "A")
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Verify _http was called with correct parameters
|
|
mock_http.assert_called_once()
|
|
args, kwargs = mock_http.call_args
|
|
self.assertEqual(args[0], "GET") # method
|
|
self.assertEqual(args[1], "/nic/update") # path
|
|
|
|
# Check query parameters
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "example.com")
|
|
self.assertEqual(queries["myip"], "192.168.1.1")
|
|
|
|
# Verify that endpoint contains embedded authentication (no Authorization header needed)
|
|
# The endpoint should have been temporarily modified to include auth credentials
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_success_nochg_response(self, mock_http):
|
|
"""Test set_record method with 'nochg' response"""
|
|
mock_http.return_value = "nochg 192.168.1.1"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
|
|
result = provider.set_record("test.example.com", "192.168.1.1", "A")
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Verify _http was called with correct parameters
|
|
mock_http.assert_called_once()
|
|
args, kwargs = mock_http.call_args
|
|
self.assertEqual(args[0], "GET")
|
|
self.assertEqual(args[1], "/nic/update")
|
|
|
|
# Check query parameters
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "test.example.com")
|
|
self.assertEqual(queries["myip"], "192.168.1.1")
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_ipv6_address(self, mock_http):
|
|
"""Test set_record method with IPv6 address"""
|
|
mock_http.return_value = "good 2001:db8::1"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
|
|
result = provider.set_record("ipv6.example.com", "2001:db8::1", "AAAA")
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Check query parameters
|
|
args, kwargs = mock_http.call_args
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "ipv6.example.com")
|
|
self.assertEqual(queries["myip"], "2001:db8::1")
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_with_all_parameters(self, mock_http):
|
|
"""Test set_record method with all optional parameters"""
|
|
mock_http.return_value = "good 10.0.0.1"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
|
|
result = provider.set_record(
|
|
domain="full.example.com", value="10.0.0.1", record_type="A", ttl=300, line="default", extra_param="test"
|
|
)
|
|
|
|
# Verify the result
|
|
self.assertTrue(result)
|
|
|
|
# Check that core parameters are still correct
|
|
args, kwargs = mock_http.call_args
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "full.example.com")
|
|
self.assertEqual(queries["myip"], "10.0.0.1")
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_nohost_error(self, mock_http):
|
|
"""Test set_record method with 'nohost' error response"""
|
|
mock_http.return_value = "nohost"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("does not exist", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_badauth_error(self, mock_http):
|
|
"""Test set_record method with 'badauth' error response"""
|
|
mock_http.return_value = "badauth"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("Invalid No-IP username/password", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_badagent_error(self, mock_http):
|
|
"""Test set_record method with 'badagent' error response"""
|
|
mock_http.return_value = "badagent"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("client disabled", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_donator_error(self, mock_http):
|
|
"""Test set_record method with '!donator' error response"""
|
|
mock_http.return_value = "!donator"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("not available for No-IP free account", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_abuse_error(self, mock_http):
|
|
"""Test set_record method with 'abuse' error response"""
|
|
mock_http.return_value = "abuse"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("blocked due to abuse", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_unexpected_response(self, mock_http):
|
|
"""Test set_record method with unexpected response"""
|
|
mock_http.return_value = "unknown_response"
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("Unexpected No-IP API response", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_empty_response_error(self, mock_http):
|
|
"""Test set_record method with empty response"""
|
|
mock_http.return_value = ""
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged - empty string should be treated as
|
|
# unexpected response
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("Unexpected No-IP API response", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_none_response_error(self, mock_http):
|
|
"""Test set_record method with None response"""
|
|
mock_http.return_value = None
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertIn("Empty response from No-IP API", args[0])
|
|
|
|
@patch.object(NoipProvider, "_http")
|
|
def test_set_record_http_exception(self, mock_http):
|
|
"""Test set_record method when _http raises an exception"""
|
|
mock_http.side_effect = Exception("Network error")
|
|
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
result = provider.set_record("example.com", "192.168.1.1")
|
|
self.assertFalse(result)
|
|
|
|
# Verify error was logged
|
|
provider.logger.error.assert_called_once()
|
|
args = provider.logger.error.call_args[0]
|
|
self.assertEqual(args[0], "Error updating No-IP record for %s: %s")
|
|
self.assertEqual(args[1], "example.com")
|
|
self.assertIsInstance(args[2], Exception)
|
|
|
|
def test_authentication_url_embedding(self):
|
|
"""Test that authentication is embedded in URL correctly"""
|
|
provider = NoipProvider("test_user", "test_pass")
|
|
|
|
with patch.object(provider, "_http") as mock_http:
|
|
mock_http.return_value = "good 1.2.3.4"
|
|
|
|
# Capture the original endpoint to verify it gets restored
|
|
original_endpoint = provider.endpoint
|
|
|
|
provider.set_record("test.com", "1.2.3.4")
|
|
|
|
# Check that _http was called
|
|
mock_http.assert_called_once()
|
|
|
|
# Verify that endpoint was restored after the call
|
|
self.assertEqual(provider.endpoint, original_endpoint)
|
|
|
|
# The actual authentication happens via URL embedding
|
|
# We can't easily test the temporary endpoint change without
|
|
# more complex mocking, but we can verify the method was called
|
|
|
|
def test_set_record_logger_info_called(self):
|
|
"""Test that logger.info is called with correct parameters"""
|
|
provider = NoipProvider(self.id, self.token)
|
|
provider.logger = MagicMock()
|
|
|
|
with patch.object(provider, "_http") as mock_http:
|
|
mock_http.return_value = "good 192.168.1.1"
|
|
provider.set_record("example.com", "192.168.1.1", "A")
|
|
|
|
# Verify logger.info was called for initial log
|
|
provider.logger.info.assert_any_call("%s => %s(%s)", "example.com", "192.168.1.1", "A")
|
|
|
|
|
|
class TestNoipProviderIntegration(BaseProviderTestCase):
|
|
"""Integration tests for NoipProvider"""
|
|
|
|
def test_full_workflow_ipv4_success(self):
|
|
"""Test complete workflow for IPv4 record with success response"""
|
|
provider = NoipProvider("test_user", "test_pass")
|
|
|
|
with patch.object(provider, "_http") as mock_http:
|
|
mock_http.return_value = "good 1.2.3.4"
|
|
|
|
result = provider.set_record("test.com", "1.2.3.4", "A", 300, "default")
|
|
|
|
self.assertTrue(result)
|
|
mock_http.assert_called_once()
|
|
args, kwargs = mock_http.call_args
|
|
self.assertEqual(args[0], "GET")
|
|
self.assertEqual(args[1], "/nic/update")
|
|
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "test.com")
|
|
self.assertEqual(queries["myip"], "1.2.3.4")
|
|
|
|
# No headers needed anymore since authentication is embedded in URL
|
|
|
|
def test_full_workflow_ipv6_success(self):
|
|
"""Test complete workflow for IPv6 record with success response"""
|
|
provider = NoipProvider("test_user", "test_pass")
|
|
|
|
with patch.object(provider, "_http") as mock_http:
|
|
mock_http.return_value = "good ::1"
|
|
|
|
result = provider.set_record("test.com", "::1", "AAAA", 600, "telecom")
|
|
|
|
self.assertTrue(result)
|
|
mock_http.assert_called_once()
|
|
|
|
args, kwargs = mock_http.call_args
|
|
queries = kwargs["queries"]
|
|
self.assertEqual(queries["hostname"], "test.com")
|
|
self.assertEqual(queries["myip"], "::1")
|
|
|
|
def test_full_workflow_error_handling(self):
|
|
"""Test complete workflow with error handling"""
|
|
provider = NoipProvider("test_user", "test_pass")
|
|
|
|
with patch.object(provider, "_http") as mock_http:
|
|
mock_http.return_value = "badauth"
|
|
|
|
result = provider.set_record("test.com", "1.2.3.4", "A")
|
|
self.assertFalse(result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|