Add NPM endpoint verification
This commit is contained in:
+189
-3
@@ -7,9 +7,11 @@ from dns_sync.adapters import (
|
||||
PROXY_HOSTS,
|
||||
Capability,
|
||||
CapabilityOperation,
|
||||
NormalizedObject,
|
||||
ObjectReference,
|
||||
)
|
||||
from dns_sync.config import AppConfig
|
||||
from dns_sync.object_normalizer import normalize_discovered_object
|
||||
from dns_sync.registry import load_adapter
|
||||
|
||||
|
||||
@@ -62,7 +64,7 @@ class NpmAdapterTests(unittest.TestCase):
|
||||
self.assertIs(adapters[0].client, first_client)
|
||||
self.assertIs(adapters[1].client, second_client)
|
||||
|
||||
def test_capabilities_are_discovery_only(self):
|
||||
def test_capabilities_cover_discovery_and_verify_only(self):
|
||||
adapter = self.adapter(Mock())
|
||||
|
||||
self.assertEqual(
|
||||
@@ -73,14 +75,37 @@ class NpmAdapterTests(unittest.TestCase):
|
||||
PROXY_HOSTS,
|
||||
CapabilityOperation.DISCOVER,
|
||||
),
|
||||
Capability(
|
||||
PROXY_HOSTS,
|
||||
CapabilityOperation.VERIFY,
|
||||
),
|
||||
Capability(
|
||||
CERTIFICATES,
|
||||
CapabilityOperation.DISCOVER,
|
||||
),
|
||||
Capability(
|
||||
CERTIFICATES,
|
||||
CapabilityOperation.VERIFY,
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.assertNotIn(
|
||||
Capability(
|
||||
PROXY_HOSTS,
|
||||
CapabilityOperation.APPLY,
|
||||
),
|
||||
adapter.capabilities,
|
||||
)
|
||||
self.assertNotIn(
|
||||
Capability(
|
||||
CERTIFICATES,
|
||||
CapabilityOperation.APPLY,
|
||||
),
|
||||
adapter.capabilities,
|
||||
)
|
||||
|
||||
def test_discovers_proxy_hosts_with_endpoint_local_key(self):
|
||||
client = Mock()
|
||||
client.proxy_hosts.return_value = [
|
||||
@@ -179,6 +204,160 @@ class NpmAdapterTests(unittest.TestCase):
|
||||
certificate.payload,
|
||||
)
|
||||
|
||||
def test_verifies_proxy_host_exact_normalized_state(self):
|
||||
client = Mock()
|
||||
client.proxy_host.return_value = {
|
||||
"id": 12,
|
||||
"domain_names": ["APP.EXAMPLE.COM."],
|
||||
"forward_scheme": "HTTP",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 8080,
|
||||
"certificate_id": 67,
|
||||
"ssl_forced": True,
|
||||
"caching_enabled": False,
|
||||
"block_exploits": True,
|
||||
"allow_websocket_upgrade": True,
|
||||
"http2_support": True,
|
||||
"hsts_enabled": True,
|
||||
"hsts_subdomains": False,
|
||||
"enabled": True,
|
||||
}
|
||||
adapter = self.adapter(client)
|
||||
|
||||
expected = normalize_discovered_object(
|
||||
adapter._proxy_host_from_row(
|
||||
client.proxy_host.return_value
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(adapter.verify(expected))
|
||||
client.proxy_host.assert_called_once_with("12")
|
||||
|
||||
def test_proxy_host_verify_detects_reference_change(self):
|
||||
client = Mock()
|
||||
client.proxy_host.return_value = {
|
||||
"id": 12,
|
||||
"domain_names": ["app.example.com"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 8080,
|
||||
"certificate_id": 68,
|
||||
"enabled": True,
|
||||
}
|
||||
adapter = self.adapter(client)
|
||||
|
||||
expected = NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
local_key="12",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 8080,
|
||||
"ssl_forced": False,
|
||||
"caching_enabled": False,
|
||||
"block_exploits": False,
|
||||
"allow_websocket_upgrade": False,
|
||||
"http2_support": False,
|
||||
"hsts_enabled": False,
|
||||
"hsts_subdomains": False,
|
||||
"enabled": True,
|
||||
},
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
local_key="67",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertFalse(adapter.verify(expected))
|
||||
|
||||
def test_certificate_verify_uses_portable_certificate_state(self):
|
||||
client = Mock()
|
||||
client.certificate.return_value = {
|
||||
"id": 84,
|
||||
"domain_names": ["APP.EXAMPLE.COM."],
|
||||
# Receiving NPM owns this as a custom/imported cert.
|
||||
"provider": "other",
|
||||
"nice_name": "local imported copy",
|
||||
"valid_from": "2026-09-01T00:00:00Z",
|
||||
"expires_on": "2026-12-01T00:00:00Z",
|
||||
"meta": {
|
||||
"cluster_managed": False,
|
||||
"acme_renew": False,
|
||||
"dns_challenge": False,
|
||||
"certificate": "must-not-enter-normalized-state",
|
||||
"certificate_key": "must-not-enter-normalized-state",
|
||||
},
|
||||
}
|
||||
adapter = self.adapter(client)
|
||||
|
||||
expected = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="84",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
# Source may have been Let's Encrypt.
|
||||
"provider": "letsencrypt",
|
||||
"nice_name": "source certificate",
|
||||
"valid_from": "2026-09-01T00:00:00+00:00",
|
||||
"expires_on": "2026-12-01T00:00:00+00:00",
|
||||
"cluster_managed": True,
|
||||
"acme_renew": True,
|
||||
"dns_challenge": True,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(adapter.verify(expected))
|
||||
client.certificate.assert_called_once_with("84")
|
||||
|
||||
def test_certificate_verify_detects_expiry_change(self):
|
||||
client = Mock()
|
||||
client.certificate.return_value = {
|
||||
"id": 84,
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": "2026-09-01T00:00:00Z",
|
||||
"expires_on": "2027-01-01T00:00:00Z",
|
||||
"meta": {},
|
||||
}
|
||||
adapter = self.adapter(client)
|
||||
|
||||
expected = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key="84",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "letsencrypt",
|
||||
"nice_name": "",
|
||||
"valid_from": "2026-09-01T00:00:00+00:00",
|
||||
"expires_on": "2026-12-01T00:00:00+00:00",
|
||||
"cluster_managed": False,
|
||||
"acme_renew": False,
|
||||
"dns_challenge": False,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(adapter.verify(expected))
|
||||
|
||||
def test_verify_requires_endpoint_local_key(self):
|
||||
adapter = self.adapter(Mock())
|
||||
|
||||
object = NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertFalse(adapter.verify(object))
|
||||
|
||||
def test_unsupported_operations_fail_explicitly(self):
|
||||
adapter = self.adapter(Mock())
|
||||
|
||||
@@ -188,8 +367,15 @@ class NpmAdapterTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "does not support apply"):
|
||||
adapter.apply(Mock())
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "does not support verify"):
|
||||
adapter.verify(Mock())
|
||||
unsupported = NormalizedObject(
|
||||
object_type="unsupported_objects",
|
||||
logical_key="unsupported",
|
||||
local_key="1",
|
||||
payload={},
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot verify"):
|
||||
adapter.verify(unsupported)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user