"""End-to-end wiring test of app.main with a stateful fake Plesk and a fake CA."""
import datetime
import os
import pathlib
import sys
import tempfile
import xml.etree.ElementTree as ET
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
OUT = tempfile.mkdtemp()
os.environ.update(
PLESK_HOST="plesk.example.com", PLESK_API_KEY="dummy", ACME_EMAIL="a@b.de",
CERT_PASSWORD="secret123", CERT_OUTPUT_DIR=OUT, ACME_ACCOUNT_DIR=tempfile.mkdtemp(),
RSA_KEY_SIZE="2048",
)
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from app import main as appmain
from app import plesk as pleskmod
# ---------------------------------------------------------------- fake Plesk
class FakeZone:
def __init__(self):
self.next_id = 100
self.records = [] # dicts: id, type, host, value
def add(self, rtype, host, value, zone="example.com"):
self.next_id += 1
fqdn = zone if host in ("", None) else f"{host}.{zone}"
self.records.append({"id": str(self.next_id), "type": rtype, "host": fqdn + ".", "value": value})
return str(self.next_id)
ZONE = FakeZone()
ZONE.add("NS", "", "ns1.example.com.")
ZONE.add("A", "vpn", "203.0.113.1")
def fake_request(self, body):
tag = body.tag
if tag == "site":
return ET.fromstring(
'ok3'
'example.com'
)
if tag == "webspace":
return ET.fromstring('')
if body.find("get_rec") is not None:
parts = "".join(
f'ok{r["id"]}3'
f'{r["type"]}{r["host"]}{r["value"]}'
for r in ZONE.records
)
return ET.fromstring(f"{parts}")
if (add := body.find("add_rec")) is not None:
rid = ZONE.add(add.findtext("type"), add.findtext("host") or "", add.findtext("value"))
return ET.fromstring(
f"ok{rid}"
)
if (dele := body.find("del_rec")) is not None:
rid = dele.findtext("./filter/id")
before = len(ZONE.records)
ZONE.records[:] = [r for r in ZONE.records if r["id"] != rid]
assert len(ZONE.records) == before - 1, f"record {rid} not found"
return ET.fromstring(
"ok"
)
raise AssertionError("unexpected " + ET.tostring(body, encoding="unicode"))
pleskmod.PleskClient._request = fake_request
# ------------------------------------------------------------------ fake CA
issued = {}
class FakeAcme:
def __init__(self, cfg):
self.cfg = cfg
def obtain_certificate(self, domains, csr_pem, solver):
csr = x509.load_pem_x509_csr(csr_pem)
issued["domains"] = domains
# One challenge per identifier - wildcard and base name share the record name
# but carry different values, so both must be present at the same time.
expected = {}
for d in domains:
base = d[2:] if d.startswith("*.") else d
name = f"_acme-challenge.{base}"
value = "val-" + d.replace("*.", "wild-").replace(".", "")
expected.setdefault(name, set()).add(value)
solver.add_txt(name, value)
solver.wait_for_propagation(expected)
issued["expected"] = expected
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
now = datetime.datetime.now(datetime.timezone.utc)
ca = (x509.CertificateBuilder()
.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Fake CA")]))
.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Fake CA")]))
.public_key(ca_key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now).not_valid_after(now + datetime.timedelta(days=3000))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.sign(ca_key, hashes.SHA256()))
leaf = (x509.CertificateBuilder().subject_name(csr.subject).issuer_name(ca.subject)
.public_key(csr.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now).not_valid_after(now + datetime.timedelta(days=90))
.add_extension(
csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value, False)
.sign(ca_key, hashes.SHA256()))
return "".join(c.public_bytes(serialization.Encoding.PEM).decode() for c in (leaf, ca))
appmain.AcmeManager = FakeAcme
appmain.authoritative_servers = lambda zone, resolvers: ["192.0.2.53"]
appmain.wait_for_txt = lambda expected, servers, timeout, interval: True
# -------------------------------------------------------------------- tests
print("--- run 1: new record + new certificate")
rc = appmain.main(["vpn.example.com", "203.0.113.9", "--env", "/dev/null"])
assert rc == 0, rc
assert issued["domains"] == ["vpn.example.com", "*.vpn.example.com"]
a_records = [r for r in ZONE.records if r["type"] == "A"]
assert len(a_records) == 1 and a_records[0]["value"] == "203.0.113.9", a_records
assert not [r for r in ZONE.records if r["type"] == "TXT"], "challenge records not cleaned up"
assert (Path(OUT) / "vpn.example.com" / "cert.pfx").is_file()
print("\n--- run 2: nothing to do (cert still valid)")
issued.clear()
rc = appmain.main(["vpn.example.com", "203.0.113.9", "--env", "/dev/null"])
assert rc == 0 and not issued, "should not have issued again"
print("\n--- run 3: --force")
rc = appmain.main(["vpn.example.com", "203.0.113.9", "--force", "--env", "/dev/null"])
assert rc == 0 and issued
print("\n--- run 4: --no-wildcard --san + wildcard input normalised")
issued.clear()
rc = appmain.main(["*.vpn.example.com", "--no-wildcard", "--san", "alt.example.com",
"--env", "/dev/null"])
assert rc == 0 and issued["domains"] == ["vpn.example.com", "alt.example.com"], issued
print("\n--- run 5: --dns-only")
issued.clear()
rc = appmain.main(["www.example.com", "203.0.113.55", "--dns-only", "--env", "/dev/null"])
assert rc == 0 and not issued
assert any(r["host"] == "www.example.com." and r["value"] == "203.0.113.55" for r in ZONE.records)
print("\n--- error paths")
assert appmain.main(["vpn.example.com", "not-an-ip", "--env", "/dev/null"]) == 2
assert appmain.main(["vpn.otherdomain.tld", "203.0.113.1", "--env", "/dev/null"]) == 1
os.environ["PLESK_API_KEY"] = ""
os.environ["PLESK_HOST"] = ""
assert appmain.main(["vpn.example.com", "--env", "/dev/null"]) == 2
print("\nALL CLI TESTS PASSED")