Add transient NPM certificate material

This commit is contained in:
Lyncoln
2026-09-20 00:27:47 -04:00
parent 381119a6b5
commit 2b4a6eea79
3 changed files with 344 additions and 10 deletions
+155 -3
View File
@@ -1,9 +1,12 @@
import os
import unittest
from io import BytesIO
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
@@ -97,6 +100,31 @@ class NpmClientMutationTests(unittest.TestCase):
response.json.return_value = data
return response
@staticmethod
def certificate_zip():
buffer = BytesIO()
with ZipFile(buffer, "w") as archive:
archive.writestr(
"cert4.pem",
b"LEAF-CERTIFICATE",
)
archive.writestr(
"fullchain4.pem",
b"FULLCHAIN",
)
archive.writestr(
"chain4.pem",
b"INTERMEDIATE-CHAIN",
)
archive.writestr(
"privkey4.pem",
b"PRIVATE-KEY",
)
return buffer.getvalue()
def test_gets_one_proxy_host_by_endpoint_local_id(self):
client = self.client()
client.get_json = Mock(
@@ -174,6 +202,125 @@ class NpmClientMutationTests(unittest.TestCase):
)
self.assertEqual(result["id"], 67)
def test_reads_custom_certificate_material_from_metadata(self):
client = self.client()
client.certificate = Mock(
return_value={
"id": 67,
"provider": "other",
"meta": {
"certificate": "CUSTOM-CERTIFICATE",
"certificate_key": "CUSTOM-PRIVATE-KEY",
"intermediate_certificate": "CUSTOM-CHAIN",
},
}
)
material = client.certificate_material("67")
self.assertEqual(
material,
CertificateMaterial(
certificate=b"CUSTOM-CERTIFICATE",
certificate_key=b"CUSTOM-PRIVATE-KEY",
intermediate_certificate=b"CUSTOM-CHAIN",
),
)
client.certificate.assert_called_once_with("67")
def test_reads_letsencrypt_material_from_download_zip(self):
client = self.client()
client.certificate = Mock(
return_value={
"id": 10,
"provider": "letsencrypt",
}
)
response = Mock()
response.content = self.certificate_zip()
client.request = Mock(return_value=response)
material = client.certificate_material("10")
self.assertEqual(
material.certificate,
b"LEAF-CERTIFICATE",
)
self.assertEqual(
material.certificate_key,
b"PRIVATE-KEY",
)
self.assertEqual(
material.intermediate_certificate,
b"INTERMEDIATE-CHAIN",
)
client.request.assert_called_once_with(
"GET",
"/nginx/certificates/10/download",
expected_status=200,
)
def test_certificate_material_repr_redacts_secrets(self):
material = CertificateMaterial(
certificate=b"VISIBLE-CERT-SECRET",
certificate_key=b"VISIBLE-KEY-SECRET",
intermediate_certificate=b"VISIBLE-CHAIN-SECRET",
)
rendered = repr(material)
self.assertNotIn(
"VISIBLE-CERT-SECRET",
rendered,
)
self.assertNotIn(
"VISIBLE-KEY-SECRET",
rendered,
)
self.assertNotIn(
"VISIBLE-CHAIN-SECRET",
rendered,
)
self.assertIn("<redacted>", rendered)
def test_certificate_material_rejects_missing_secret_parts(self):
client = self.client()
client.certificate = Mock(
return_value={
"id": 67,
"provider": "other",
"meta": {
"certificate": "CERT",
"certificate_key": "",
},
}
)
with self.assertRaisesRegex(
RuntimeError,
"certificate_key",
):
client.certificate_material("67")
def test_certificate_material_rejects_unknown_provider(self):
client = self.client()
client.certificate = Mock(
return_value={
"id": 99,
"provider": "mystery-provider",
}
)
with self.assertRaisesRegex(
RuntimeError,
"Unsupported NPM certificate provider",
):
client.certificate_material("99")
def test_creates_certificate_metadata_with_json_payload(self):
client = self.client()
payload = {
@@ -200,9 +347,13 @@ class NpmClientMutationTests(unittest.TestCase):
def test_uploads_certificate_material_as_multipart(self):
client = self.client()
client.request = Mock(
return_value=self.response({"id": 84})
response = self.response(
{
"certificate": "SECRET-CERTIFICATE",
"certificate_key": "SECRET-PRIVATE-KEY",
}
)
client.request = Mock(return_value=response)
result = client.upload_certificate(
"84",
@@ -230,7 +381,8 @@ class NpmClientMutationTests(unittest.TestCase):
),
},
)
self.assertEqual(result["id"], 84)
self.assertIsNone(result)
response.json.assert_not_called()
def test_certificate_upload_requires_both_required_parts(self):
client = self.client()