devnomads 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. devnomads-0.2.2/.gitlab-ci.yml +82 -0
  2. {devnomads-0.2.0 → devnomads-0.2.2}/PKG-INFO +2 -2
  3. {devnomads-0.2.0 → devnomads-0.2.2}/pyproject.toml +2 -2
  4. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/__init__.py +8 -1
  5. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/client.py +11 -3
  6. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/keys.py +21 -0
  7. devnomads-0.2.2/tests/test_acme_client.py +125 -0
  8. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_acme_keys.py +20 -0
  9. devnomads-0.2.2/tools/bump_version.py +33 -0
  10. {devnomads-0.2.0 → devnomads-0.2.2}/uv.lock +1 -1
  11. devnomads-0.2.0/.gitlab-ci.yml +0 -43
  12. devnomads-0.2.0/tests/test_acme_client.py +0 -64
  13. {devnomads-0.2.0 → devnomads-0.2.2}/.flake8 +0 -0
  14. {devnomads-0.2.0 → devnomads-0.2.2}/.gitignore +0 -0
  15. {devnomads-0.2.0 → devnomads-0.2.2}/Makefile +0 -0
  16. {devnomads-0.2.0 → devnomads-0.2.2}/README.md +0 -0
  17. {devnomads-0.2.0 → devnomads-0.2.2}/openapi.json +0 -0
  18. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/__init__.py +0 -0
  19. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/challenge_server.py +0 -0
  20. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/dns01.py +0 -0
  21. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/errors.py +0 -0
  22. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/http01.py +0 -0
  23. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/acme/verify.py +0 -0
  24. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/api/__init__.py +0 -0
  25. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/api/_services.py +0 -0
  26. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/api/client.py +0 -0
  27. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/api/credentials.py +0 -0
  28. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/api/errors.py +0 -0
  29. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/dns/__init__.py +0 -0
  30. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/dns/errors.py +0 -0
  31. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/dns/names.py +0 -0
  32. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/dns/zones.py +0 -0
  33. {devnomads-0.2.0 → devnomads-0.2.2}/src/devnomads/py.typed +0 -0
  34. {devnomads-0.2.0 → devnomads-0.2.2}/tests/conftest.py +0 -0
  35. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_acme_challenges.py +0 -0
  36. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_client.py +0 -0
  37. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_credentials.py +0 -0
  38. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_dns.py +0 -0
  39. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_names.py +0 -0
  40. {devnomads-0.2.0 → devnomads-0.2.2}/tests/test_services.py +0 -0
  41. {devnomads-0.2.0 → devnomads-0.2.2}/tools/generate.py +0 -0
@@ -0,0 +1,82 @@
1
+ stages:
2
+ - check
3
+ - test
4
+ - publish
5
+
6
+ default:
7
+ image: ghcr.io/astral-sh/uv:python3.14-alpine
8
+ before_script:
9
+ # dev tools + the acme extra, so linting and tests cover devnomads.acme.
10
+ - uv sync --dev --extra acme
11
+
12
+ check:
13
+ stage: check
14
+ rules:
15
+ - if: $CI_PIPELINE_SOURCE != "trigger"
16
+ script:
17
+ - uv run python tools/generate.py --check
18
+ - uv run black --check .
19
+ - uv run isort --check-only .
20
+ - uv run flake8 src tests
21
+ - uv run bandit -r src
22
+ - uv run semgrep scan --quiet --error --config p/python src
23
+ - uv run mypy src
24
+
25
+ test:
26
+ stage: test
27
+ rules:
28
+ - if: $CI_PIPELINE_SOURCE != "trigger"
29
+ parallel:
30
+ matrix:
31
+ - PYTHON: ["3.10", "3.14"]
32
+ image: ghcr.io/astral-sh/uv:python$PYTHON-alpine
33
+ script:
34
+ - uv run pytest -v
35
+
36
+ # Runs on version tags (v0.1.0). PYPI_TOKEN (masked) and
37
+ # PYPI_PUBLISH_URL are CI variables pointing at pypi.org; the
38
+ # PYPI_TEST_* variants hold the test.pypi.org equivalents - swap them
39
+ # in to rehearse a release without publishing for real.
40
+ publish:
41
+ stage: publish
42
+ rules:
43
+ - if: $CI_COMMIT_TAG =~ /^v/
44
+ before_script: []
45
+ script:
46
+ - uv build
47
+ - uv publish --publish-url "$PYPI_PUBLISH_URL" --token "$PYPI_TOKEN"
48
+
49
+ # Triggered by the DevNomads API deploy pipeline (a pipeline trigger
50
+ # token). Refreshes the OpenAPI spec, regenerates the SDK, and - only if
51
+ # the spec actually changed - auto-bumps the patch version, commits the
52
+ # regenerated code back, and pushes a tag. The publish job above turns
53
+ # that tag into a PyPI release. Runs only on a trigger, so it never
54
+ # recurses on the branch/tag pushes it creates.
55
+ #
56
+ # Requires CI variables:
57
+ # GITLAB_PUSH_TOKEN - project/group access token (Maintainer role,
58
+ # write_repository scope) allowed to push the default branch and tags.
59
+ # PYPI_TOKEN / PYPI_PUBLISH_URL - used by the publish job on the tag.
60
+ auto-release:
61
+ stage: check
62
+ rules:
63
+ - if: $CI_PIPELINE_SOURCE == "trigger"
64
+ resource_group: auto-release
65
+ script:
66
+ - apk add --no-cache git curl
67
+ - curl -sf "https://api.devnomads.nl/docs?api-docs.json" -o openapi.json
68
+ - uv run python tools/generate.py
69
+ - uv run black -q . && uv run isort -q .
70
+ - |
71
+ if git diff --quiet -- openapi.json src/devnomads/api/_services.py; then
72
+ echo "spec unchanged; nothing to release"; exit 0
73
+ fi
74
+ - NEW=$(uv run python tools/bump_version.py)
75
+ - uv lock
76
+ - git config user.name "devnomads-ci"
77
+ - git config user.email "ci@devnomads.nl"
78
+ - git add openapi.json src/devnomads/api/_services.py pyproject.toml uv.lock
79
+ - 'git commit -m "chore: auto-regenerate sdk from api spec (v${NEW})"'
80
+ - git tag -a "v${NEW}" -m "v${NEW}"
81
+ - git remote set-url origin "https://oauth2:${GITLAB_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
82
+ - git push origin "HEAD:${CI_DEFAULT_BRANCH}" --follow-tags
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devnomads
3
- Version: 0.2.0
4
- Summary: Python client for the DevNomads API (transport, DNS, ACME)
3
+ Version: 0.2.2
4
+ Summary: Python client library for the DevNomads API (transport, DNS, ACME)
5
5
  Project-URL: Homepage, https://devnomads.nl
6
6
  Author-email: Loek Geleijn <support@devnomads.nl>
7
7
  License: MIT
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "devnomads"
7
- version = "0.2.0"
8
- description = "Python client for the DevNomads API (transport, DNS, ACME)"
7
+ version = "0.2.2"
8
+ description = "Python client library for the DevNomads API (transport, DNS, ACME)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
@@ -21,7 +21,13 @@ try:
21
21
  from .dns01 import DevNomadsDnsProvider, DnsProvider
22
22
  from .errors import AcmeError
23
23
  from .http01 import Http01Solver, StandaloneSolver, WebrootSolver
24
- from .keys import build_csr, generate_key, load_or_create_account_key, serialize_key
24
+ from .keys import (
25
+ build_csr,
26
+ generate_key,
27
+ jws_alg,
28
+ load_or_create_account_key,
29
+ serialize_key,
30
+ )
25
31
  from .verify import verify_txt_record
26
32
  except ImportError as exc: # pragma: no cover - exercised via packaging
27
33
  if (exc.name or "").split(".")[0] in _EXTRA_MODULES:
@@ -44,6 +50,7 @@ __all__ = [
44
50
  "build_csr",
45
51
  "generate_key",
46
52
  "serialize_key",
53
+ "jws_alg",
47
54
  "load_or_create_account_key",
48
55
  "verify_txt_record",
49
56
  ]
@@ -14,7 +14,13 @@ from acme import messages
14
14
  from .dns01 import DnsProvider
15
15
  from .errors import AcmeError
16
16
  from .http01 import Http01Solver
17
- from .keys import PrivateKey, build_csr, load_or_create_account_key, serialize_key
17
+ from .keys import (
18
+ PrivateKey,
19
+ build_csr,
20
+ jws_alg,
21
+ load_or_create_account_key,
22
+ serialize_key,
23
+ )
18
24
  from .verify import verify_txt_record
19
25
 
20
26
  log = logging.getLogger("devnomads.acme")
@@ -72,11 +78,13 @@ class AcmeClient:
72
78
  account_key = load_or_create_account_key(
73
79
  self.account_key_path, self.account_key_algorithm
74
80
  )
75
- net = acme_client.ClientNetwork(account_key, user_agent=self.user_agent)
81
+ net = acme_client.ClientNetwork(
82
+ account_key, alg=jws_alg(account_key), user_agent=self.user_agent
83
+ )
76
84
  directory = messages.Directory.from_json(net.get(self.directory_url).json())
77
85
  client = acme_client.ClientV2(directory, net)
78
86
 
79
- new_account = messages.NewAccount(
87
+ new_account = messages.NewRegistration(
80
88
  contact=(f"mailto:{self.contact_email}",) if self.contact_email else (),
81
89
  terms_of_service_agreed=True,
82
90
  only_return_existing=False,
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
 
11
+ import josepy
11
12
  from cryptography import x509
12
13
  from cryptography.hazmat.backends import default_backend
13
14
  from cryptography.hazmat.primitives import hashes, serialization
@@ -88,6 +89,26 @@ def load_or_create_account_key(path: str, algorithm: str):
88
89
  raise AcmeError("unsupported account key type")
89
90
 
90
91
 
92
+ def jws_alg(account_key):
93
+ """Return the JWS signing algorithm matching an account JWK.
94
+
95
+ ``acme.client.ClientNetwork`` defaults to RS256, so pairing it with an
96
+ EC account key makes josepy reject the signature (the key is not an
97
+ instance of the algorithm's key type). The alg must follow the key.
98
+ """
99
+
100
+ if isinstance(account_key, jwk.JWKRSA):
101
+ return josepy.RS256
102
+ if isinstance(account_key, jwk.JWKEC):
103
+ curve = account_key.key.curve.name
104
+ if curve == "secp256r1":
105
+ return josepy.ES256
106
+ if curve == "secp384r1":
107
+ return josepy.ES384
108
+ raise AcmeError(f"unsupported account key curve: {curve}")
109
+ raise AcmeError("unsupported account key type")
110
+
111
+
91
112
  def build_csr(private_key: PrivateKey, identifiers: list[str]) -> bytes:
92
113
  """Build a PEM CSR: ``identifiers[0]`` is the CN, all are SANs."""
93
114
 
@@ -0,0 +1,125 @@
1
+ """Tests for the pure helpers of the ACME client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import SimpleNamespace
6
+
7
+ import pytest
8
+
9
+ from devnomads.acme import AcmeError
10
+ from devnomads.acme.client import (
11
+ _challenge_for_authz,
12
+ _normalize_challenge_types,
13
+ _normalize_identifiers,
14
+ _split_fullchain,
15
+ )
16
+
17
+
18
+ def test_normalize_identifiers_dedup_preserves_order():
19
+ assert _normalize_identifiers(["a.com", "b.com", "a.com", ""]) == ["a.com", "b.com"]
20
+
21
+
22
+ def test_normalize_challenge_types_string_applies_to_all():
23
+ assert _normalize_challenge_types("dns-01", ["a.com", "b.com"]) == {
24
+ "a.com": "dns-01",
25
+ "b.com": "dns-01",
26
+ }
27
+
28
+
29
+ def test_normalize_challenge_types_dict_per_identifier():
30
+ mapping = {"a.com": "dns-01", "b.com": "http-01"}
31
+ assert _normalize_challenge_types(mapping, ["a.com", "b.com"]) == mapping
32
+
33
+
34
+ def test_normalize_challenge_types_missing_raises():
35
+ with pytest.raises(AcmeError):
36
+ _normalize_challenge_types({"a.com": "dns-01"}, ["a.com", "b.com"])
37
+
38
+
39
+ def _authz(value, wildcard):
40
+ return SimpleNamespace(identifier=SimpleNamespace(value=value), wildcard=wildcard)
41
+
42
+
43
+ def test_challenge_for_authz_plain():
44
+ body = _authz("example.com", False)
45
+ assert _challenge_for_authz({"example.com": "dns-01"}, body) == "dns-01"
46
+
47
+
48
+ def test_challenge_for_authz_wildcard_reprefixes():
49
+ body = _authz("example.com", True)
50
+ assert _challenge_for_authz({"*.example.com": "dns-01"}, body) == "dns-01"
51
+
52
+
53
+ def test_split_fullchain():
54
+ leaf = "-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n"
55
+ rest = "-----BEGIN CERTIFICATE-----\nBBB\n-----END CERTIFICATE-----\n"
56
+ leaf_pem, chain_pem = _split_fullchain(leaf + rest)
57
+ assert leaf_pem == leaf
58
+ assert chain_pem == rest
59
+
60
+
61
+ def test_split_fullchain_no_marker():
62
+ leaf_pem, chain_pem = _split_fullchain("garbage")
63
+ assert leaf_pem == "garbage"
64
+ assert chain_pem == ""
65
+
66
+
67
+ def test_init_client_registers_with_new_registration(monkeypatch, tmp_path):
68
+ """_init_client must build a real acme.messages.NewRegistration and hand
69
+ it to new_account; this exercises the message construction that a wrong
70
+ class name (e.g. the non-existent NewAccount) would break."""
71
+
72
+ import josepy
73
+ from acme import messages
74
+ from cryptography.hazmat.backends import default_backend
75
+ from cryptography.hazmat.primitives.asymmetric import ec
76
+ from josepy import jwk
77
+
78
+ from devnomads.acme import client as client_mod
79
+
80
+ captured = {}
81
+ account_key = jwk.JWKEC(
82
+ key=ec.generate_private_key(ec.SECP256R1(), backend=default_backend())
83
+ )
84
+
85
+ class FakeResponse:
86
+ def json(self):
87
+ return {}
88
+
89
+ class FakeNetwork:
90
+ def __init__(self, *args, **kwargs):
91
+ captured["alg"] = kwargs.get("alg")
92
+ self.account = None
93
+
94
+ def get(self, url):
95
+ return FakeResponse()
96
+
97
+ class FakeClientV2:
98
+ def __init__(self, directory, net):
99
+ self.net = net
100
+
101
+ def new_account(self, registration):
102
+ captured["registration"] = registration
103
+ return SimpleNamespace(uri="https://acme.example/acct/1")
104
+
105
+ monkeypatch.setattr(
106
+ client_mod, "load_or_create_account_key", lambda *a, **k: account_key
107
+ )
108
+ monkeypatch.setattr(client_mod.acme_client, "ClientNetwork", FakeNetwork)
109
+ monkeypatch.setattr(client_mod.acme_client, "ClientV2", FakeClientV2)
110
+ monkeypatch.setattr(
111
+ client_mod.messages.Directory, "from_json", lambda data: object()
112
+ )
113
+
114
+ ac = client_mod.AcmeClient(
115
+ str(tmp_path / "account.pem"), contact_email="ops@example.com"
116
+ )
117
+ client, _account_key = ac._init_client()
118
+
119
+ # an EC account key must be signed with ES256, not the ClientNetwork
120
+ # default of RS256, or josepy rejects the JWS.
121
+ assert captured["alg"] == josepy.ES256
122
+ registration = captured["registration"]
123
+ assert isinstance(registration, messages.NewRegistration)
124
+ assert registration.contact == ("mailto:ops@example.com",)
125
+ assert client.net.account is not None
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import os
6
6
  import stat
7
7
 
8
+ import josepy
8
9
  import pytest
9
10
  from cryptography import x509
10
11
  from cryptography.hazmat.primitives.asymmetric import ec, rsa
@@ -14,9 +15,11 @@ from devnomads.acme import (
14
15
  AcmeError,
15
16
  build_csr,
16
17
  generate_key,
18
+ jws_alg,
17
19
  load_or_create_account_key,
18
20
  serialize_key,
19
21
  )
22
+ from devnomads.acme.keys import jwk
20
23
 
21
24
 
22
25
  def test_generate_rsa():
@@ -68,3 +71,20 @@ def test_account_key_reused(tmp_path):
68
71
  content = open(path, "rb").read()
69
72
  load_or_create_account_key(path, "ec256")
70
73
  assert open(path, "rb").read() == content
74
+
75
+
76
+ def test_jws_alg_rsa():
77
+ assert jws_alg(jwk.JWKRSA(key=generate_key("rsa2048"))) is josepy.RS256
78
+
79
+
80
+ def test_jws_alg_ec256():
81
+ assert jws_alg(jwk.JWKEC(key=generate_key("ec256"))) is josepy.ES256
82
+
83
+
84
+ def test_jws_alg_ec384():
85
+ assert jws_alg(jwk.JWKEC(key=generate_key("ec384"))) is josepy.ES384
86
+
87
+
88
+ def test_jws_alg_unsupported_type():
89
+ with pytest.raises(AcmeError):
90
+ jws_alg(object())
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ """Bump the patch version in pyproject.toml and print the new version.
3
+
4
+ Used by the auto-release CI job so each spec-driven regeneration gets a
5
+ fresh, monotonically increasing version (PyPI versions are immutable).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import pathlib
11
+ import re
12
+ import sys
13
+
14
+ PYPROJECT = pathlib.Path(__file__).resolve().parent.parent / "pyproject.toml"
15
+
16
+
17
+ def main() -> int:
18
+ text = PYPROJECT.read_text()
19
+ match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', text, re.MULTILINE)
20
+ if not match:
21
+ print("error: could not find a version in pyproject.toml", file=sys.stderr)
22
+ return 1
23
+ major, minor, patch = (int(g) for g in match.groups())
24
+ new_version = f"{major}.{minor}.{patch + 1}"
25
+ PYPROJECT.write_text(
26
+ text[: match.start()] + f'version = "{new_version}"' + text[match.end() :]
27
+ )
28
+ print(new_version)
29
+ return 0
30
+
31
+
32
+ if __name__ == "__main__":
33
+ raise SystemExit(main())
@@ -468,7 +468,7 @@ wheels = [
468
468
 
469
469
  [[package]]
470
470
  name = "devnomads"
471
- version = "0.2.0"
471
+ version = "0.2.2"
472
472
  source = { editable = "." }
473
473
  dependencies = [
474
474
  { name = "httpx" },
@@ -1,43 +0,0 @@
1
- stages:
2
- - check
3
- - test
4
- - publish
5
-
6
- default:
7
- image: ghcr.io/astral-sh/uv:python3.14-alpine
8
- before_script:
9
- # dev tools + the acme extra, so linting and tests cover devnomads.acme.
10
- - uv sync --dev --extra acme
11
-
12
- check:
13
- stage: check
14
- script:
15
- - uv run python tools/generate.py --check
16
- - uv run black --check .
17
- - uv run isort --check-only .
18
- - uv run flake8 src tests
19
- - uv run bandit -r src
20
- - uv run semgrep scan --quiet --error --config p/python src
21
- - uv run mypy src
22
-
23
- test:
24
- stage: test
25
- parallel:
26
- matrix:
27
- - PYTHON: ["3.10", "3.14"]
28
- image: ghcr.io/astral-sh/uv:python$PYTHON-alpine
29
- script:
30
- - uv run pytest -v
31
-
32
- # Runs on version tags (v0.1.0). PYPI_TOKEN (masked) and
33
- # PYPI_PUBLISH_URL are CI variables pointing at pypi.org; the
34
- # PYPI_TEST_* variants hold the test.pypi.org equivalents - swap them
35
- # in to rehearse a release without publishing for real.
36
- publish:
37
- stage: publish
38
- rules:
39
- - if: $CI_COMMIT_TAG =~ /^v/
40
- before_script: []
41
- script:
42
- - uv build
43
- - uv publish --publish-url "$PYPI_PUBLISH_URL" --token "$PYPI_TOKEN"
@@ -1,64 +0,0 @@
1
- """Tests for the pure helpers of the ACME client."""
2
-
3
- from __future__ import annotations
4
-
5
- from types import SimpleNamespace
6
-
7
- import pytest
8
-
9
- from devnomads.acme import AcmeError
10
- from devnomads.acme.client import (
11
- _challenge_for_authz,
12
- _normalize_challenge_types,
13
- _normalize_identifiers,
14
- _split_fullchain,
15
- )
16
-
17
-
18
- def test_normalize_identifiers_dedup_preserves_order():
19
- assert _normalize_identifiers(["a.com", "b.com", "a.com", ""]) == ["a.com", "b.com"]
20
-
21
-
22
- def test_normalize_challenge_types_string_applies_to_all():
23
- assert _normalize_challenge_types("dns-01", ["a.com", "b.com"]) == {
24
- "a.com": "dns-01",
25
- "b.com": "dns-01",
26
- }
27
-
28
-
29
- def test_normalize_challenge_types_dict_per_identifier():
30
- mapping = {"a.com": "dns-01", "b.com": "http-01"}
31
- assert _normalize_challenge_types(mapping, ["a.com", "b.com"]) == mapping
32
-
33
-
34
- def test_normalize_challenge_types_missing_raises():
35
- with pytest.raises(AcmeError):
36
- _normalize_challenge_types({"a.com": "dns-01"}, ["a.com", "b.com"])
37
-
38
-
39
- def _authz(value, wildcard):
40
- return SimpleNamespace(identifier=SimpleNamespace(value=value), wildcard=wildcard)
41
-
42
-
43
- def test_challenge_for_authz_plain():
44
- body = _authz("example.com", False)
45
- assert _challenge_for_authz({"example.com": "dns-01"}, body) == "dns-01"
46
-
47
-
48
- def test_challenge_for_authz_wildcard_reprefixes():
49
- body = _authz("example.com", True)
50
- assert _challenge_for_authz({"*.example.com": "dns-01"}, body) == "dns-01"
51
-
52
-
53
- def test_split_fullchain():
54
- leaf = "-----BEGIN CERTIFICATE-----\nAAA\n-----END CERTIFICATE-----\n"
55
- rest = "-----BEGIN CERTIFICATE-----\nBBB\n-----END CERTIFICATE-----\n"
56
- leaf_pem, chain_pem = _split_fullchain(leaf + rest)
57
- assert leaf_pem == leaf
58
- assert chain_pem == rest
59
-
60
-
61
- def test_split_fullchain_no_marker():
62
- leaf_pem, chain_pem = _split_fullchain("garbage")
63
- assert leaf_pem == "garbage"
64
- assert chain_pem == ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes