Move NPM configuration and client behind plugin boundary

This commit is contained in:
Lyncoln
2026-09-20 22:50:37 -04:00
parent 332f994c26
commit c9a3898055
8 changed files with 289 additions and 256 deletions
+13 -33
View File
@@ -47,28 +47,27 @@ class EndpointParsingTests(unittest.TestCase):
)
class AppConfigEndpointTests(unittest.TestCase):
def test_legacy_proxy_ip_remains_supported(self):
class AppConfigPolicyTests(unittest.TestCase):
def test_explicit_dns_target_is_used(self):
with patch.dict(
os.environ,
{"PROXY_IP": "192.168.1.101"},
{
"DNS_TARGET_IP": "192.168.1.101",
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
},
clear=True,
):
config = AppConfig.from_env()
self.assertEqual(
tuple(endpoint.identity for endpoint in config.npm_endpoints),
("npm://192.168.1.101:81",),
config.target_ip,
"192.168.1.101",
)
self.assertEqual(config.target_ip, "192.168.1.101")
def test_endpoint_list_controls_api_and_proxy_ip_remains_target(self):
def test_legacy_proxy_ip_remains_target_fallback(self):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": (
"192.168.1.3,192.168.1.4:8081,192.168.1.5"
),
"PROXY_IP": "192.168.1.101",
},
clear=True,
@@ -76,33 +75,15 @@ class AppConfigEndpointTests(unittest.TestCase):
config = AppConfig.from_env()
self.assertEqual(
tuple(endpoint.identity for endpoint in config.npm_endpoints),
(
"npm://192.168.1.3:81",
"npm://192.168.1.4:8081",
"npm://192.168.1.5:81",
),
config.target_ip,
"192.168.1.101",
)
self.assertEqual(config.target_ip, "192.168.1.101")
def test_explicit_dns_target_supports_multiple_endpoints(self):
def test_dns_target_is_required_without_legacy_fallback(self):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
"DNS_TARGET_IP": "192.168.1.101",
},
clear=True,
):
config = AppConfig.from_env()
self.assertEqual(config.target_ip, "192.168.1.101")
def test_multiple_endpoints_require_target_without_proxy_ip(self):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
"NPM_ENDPOINTS": "192.168.1.3",
},
clear=True,
):
@@ -113,6 +94,5 @@ class AppConfigEndpointTests(unittest.TestCase):
AppConfig.from_env()
if __name__ == "__main__":
unittest.main()
+120 -112
View File
@@ -16,73 +16,103 @@ from dns_sync.adapters import (
from dns_sync.certificates.material import CertificateMaterial
from dns_sync.config import AppConfig
from dns_sync.object_normalizer import normalize_discovered_object
from dns_sync.registry import load_adapter
from dns_sync.registry import (
configured_adapter_endpoints,
load_adapter,
)
class NpmAdapterTests(unittest.TestCase):
def config(self):
with patch.dict(
@staticmethod
def npm_env(
endpoints="192.168.1.3,192.168.1.4",
):
return patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
"NPM_ENDPOINTS": endpoints,
"DNS_TARGET_IP": "192.168.1.101",
},
clear=True,
):
)
def config(self):
with self.npm_env():
return AppConfig.from_env()
def adapter(self, client):
config = self.config()
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=client,
):
return load_adapter(
with self.npm_env():
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
config.npm_endpoints[0],
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=client,
):
return load_adapter(
"npm",
config,
endpoints[0],
)
def target_adapter_with_sources(
self,
target_client,
source_clients,
):
config = self.config()
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
):
adapter = load_adapter(
with self.npm_env():
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
config.npm_endpoints[1],
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
):
adapter = load_adapter(
"npm",
config,
endpoints[1],
)
client_by_identity = {
endpoint_id: client
for endpoint_id, client in source_clients.items()
}
def client_factory(config, endpoint):
def client_factory(endpoint):
return client_by_identity[endpoint.identity]
return adapter, client_factory
def test_registry_creates_one_adapter_per_endpoint(self):
config = self.config()
first_client = Mock()
second_client = Mock()
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
side_effect=[first_client, second_client],
) as client_class:
adapters = [
load_adapter("npm", config, endpoint)
for endpoint in config.npm_endpoints
]
with self.npm_env():
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
side_effect=[first_client, second_client],
) as client_class:
adapters = [
load_adapter(
"npm",
config,
endpoint,
)
for endpoint in endpoints
]
self.assertEqual(
tuple(adapter.endpoint.identity for adapter in adapters),
@@ -517,25 +547,14 @@ class NpmAdapterTests(unittest.TestCase):
source_client.certificate_material.assert_not_called()
def test_certificate_material_source_rejects_conflicting_material(self):
config = self.config()
# Add a third endpoint for this ambiguity test.
endpoint_type = type(config.npm_endpoints[0])
third_endpoint = endpoint_type(
"npm",
"192.168.1.5:81",
)
config = __import__(
"dataclasses"
).replace(
config,
npm_endpoints=(
config.npm_endpoints[0],
config.npm_endpoints[1],
third_endpoint,
),
)
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
)
target_client = Mock()
source_a = Mock()
@@ -568,22 +587,25 @@ class NpmAdapterTests(unittest.TestCase):
)
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
adapter = load_adapter(
"npm",
config,
config.npm_endpoints[1],
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
):
adapter = load_adapter(
"npm",
config,
endpoints[1],
)
clients = {
"npm://192.168.1.3:81": source_a,
"npm://192.168.1.5:81": source_b,
}
def client_factory(config, endpoint):
def client_factory(endpoint):
return clients[endpoint.identity]
desired = NormalizedObject(
@@ -607,24 +629,14 @@ class NpmAdapterTests(unittest.TestCase):
adapter._certificate_material_source(desired)
def test_certificate_material_source_accepts_identical_replicas(self):
config = self.config()
endpoint_type = type(config.npm_endpoints[0])
third_endpoint = endpoint_type(
"npm",
"192.168.1.5:81",
)
config = __import__(
"dataclasses"
).replace(
config,
npm_endpoints=(
config.npm_endpoints[0],
config.npm_endpoints[1],
third_endpoint,
),
)
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
)
target_client = Mock()
source_a = Mock()
@@ -660,22 +672,25 @@ class NpmAdapterTests(unittest.TestCase):
source_a.certificate_material.return_value = common_material
source_b.certificate_material.return_value = common_material
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
adapter = load_adapter(
"npm",
config,
config.npm_endpoints[1],
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
):
adapter = load_adapter(
"npm",
config,
endpoints[1],
)
clients = {
"npm://192.168.1.3:81": source_a,
"npm://192.168.1.5:81": source_b,
}
def client_factory(config, endpoint):
def client_factory(endpoint):
return clients[endpoint.identity]
desired = NormalizedObject(
@@ -699,24 +714,14 @@ class NpmAdapterTests(unittest.TestCase):
self.assertEqual(resolved, common_material)
def test_certificate_material_source_tolerates_unavailable_unrelated_source(self):
config = self.config()
endpoint_type = type(config.npm_endpoints[0])
third_endpoint = endpoint_type(
"npm",
"192.168.1.5:81",
)
config = __import__(
"dataclasses"
).replace(
config,
npm_endpoints=(
config.npm_endpoints[0],
config.npm_endpoints[1],
third_endpoint,
),
)
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
config = AppConfig.from_env()
endpoints = configured_adapter_endpoints(
"npm",
config,
)
target_client = Mock()
unavailable = Mock()
@@ -743,22 +748,25 @@ class NpmAdapterTests(unittest.TestCase):
)
available.certificate_material.return_value = material
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
with self.npm_env(
"192.168.1.3,192.168.1.4,192.168.1.5"
):
adapter = load_adapter(
"npm",
config,
config.npm_endpoints[1],
)
with patch(
"dns_sync.plugins.adapters.npm.NpmClient",
return_value=target_client,
):
adapter = load_adapter(
"npm",
config,
endpoints[1],
)
clients = {
"npm://192.168.1.3:81": unavailable,
"npm://192.168.1.5:81": available,
}
def client_factory(config, endpoint):
def client_factory(endpoint):
return clients[endpoint.identity]
desired = NormalizedObject(
+74 -27
View File
@@ -5,48 +5,49 @@ from zipfile import ZipFile
from unittest.mock import Mock, patch
from dns_sync.adapters import Endpoint
from dns_sync.config import AppConfig
from dns_sync.certificates.material import CertificateMaterial
from dns_sync.npm_client import NpmClient
from dns_sync.plugins.adapters.npm.client import NpmClient
class NpmClientEndpointTests(unittest.TestCase):
def config(self):
return AppConfig.from_env()
def test_default_client_uses_first_configured_endpoint(self):
def test_client_uses_explicit_endpoint(self):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4:8081",
"DNS_TARGET_IP": "192.168.1.101",
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
},
clear=True,
):
config = self.config()
endpoint = Endpoint(
"npm",
"192.168.1.3:81",
)
with patch(
"dns_sync.npm_client.requests.Session.request"
"dns_sync.plugins.adapters.npm.client.requests.Session.request"
) as request:
client = NpmClient(config)
client = NpmClient(endpoint)
request.assert_not_called()
self.assertEqual(client.api_url, "http://192.168.1.3:81/api")
self.assertEqual(
client.api_url,
"http://192.168.1.3:81/api",
)
def test_client_can_select_specific_configured_endpoint(self):
def test_client_can_select_specific_endpoint(self):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4:8081",
"DNS_TARGET_IP": "192.168.1.101",
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
},
clear=True,
):
config = self.config()
client = NpmClient(config, config.npm_endpoints[1])
endpoint = Endpoint(
"npm",
"192.168.1.4:8081",
)
client = NpmClient(endpoint)
self.assertEqual(
client.api_url,
@@ -61,19 +62,66 @@ class NpmClientEndpointTests(unittest.TestCase):
with patch.dict(
os.environ,
{
"PROXY_IP": "192.168.1.101",
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
},
clear=True,
):
config = self.config()
endpoint = Endpoint("verizon", "192.168.1.1")
endpoint = Endpoint(
"verizon",
"192.168.1.1:443",
)
with self.assertRaisesRegex(RuntimeError, "npm endpoint"):
NpmClient(config, endpoint)
with self.assertRaisesRegex(
RuntimeError,
"npm endpoint",
):
NpmClient(endpoint)
def test_client_rejects_invalid_api_scheme(self):
with patch.dict(
os.environ,
{
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
"PROXY_API_SCHEME": "ftp",
},
clear=True,
):
with self.assertRaisesRegex(
RuntimeError,
"PROXY_API_SCHEME",
):
NpmClient(
Endpoint(
"npm",
"192.168.1.3:81",
)
)
def test_client_owns_tls_verification_setting(self):
with patch.dict(
os.environ,
{
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
"PROXY_API_SCHEME": "https",
"PROXY_VERIFY_TLS": "false",
},
clear=True,
):
client = NpmClient(
Endpoint(
"npm",
"192.168.1.3:81",
)
)
self.assertEqual(
client.api_url,
"https://192.168.1.3:81/api",
)
self.assertFalse(client.verify_tls)
class NpmClientMutationTests(unittest.TestCase):
@@ -81,17 +129,16 @@ class NpmClientMutationTests(unittest.TestCase):
with patch.dict(
os.environ,
{
"NPM_ENDPOINTS": "192.168.1.3",
"DNS_TARGET_IP": "192.168.1.101",
"NPM_IDENTITY": "test@example.com",
"NPM_SECRET": "not-a-real-secret",
},
clear=True,
):
config = AppConfig.from_env()
return NpmClient(
config,
config.npm_endpoints[0],
Endpoint(
"npm",
"192.168.1.3:81",
)
)
@staticmethod
+2 -3
View File
@@ -228,14 +228,13 @@ class PluginRegistryTests(unittest.TestCase):
"npm",
"192.168.1.3:81",
)
config = SimpleNamespace(
npm_endpoints=(endpoint,),
)
config = SimpleNamespace()
with patch.dict(
os.environ,
{
"ENABLED_PLUGINS": "npm",
"NPM_ENDPOINTS": "192.168.1.3",
},
clear=True,
):
+2
View File
@@ -24,6 +24,7 @@ class VerizonAdapterTests(unittest.TestCase):
with patch.dict(
os.environ,
{
"DNS_TARGET_IP": "192.168.1.101",
"NPM_ENDPOINTS": "192.168.1.3",
"VERIZON_ENDPOINTS": "192.168.1.1",
"ENABLED_PLUGINS": "npm,verizon",
@@ -141,6 +142,7 @@ class VerizonAdapterTests(unittest.TestCase):
with patch.dict(
os.environ,
{
"DNS_TARGET_IP": "192.168.1.101",
"NPM_ENDPOINTS": "192.168.1.3",
"VERIZON_ENDPOINTS": "192.168.1.1:8443",
"ENABLED_PLUGINS": "npm,verizon",