tests.testing

Resources to assist in testing Certbot Deployer and its plugins

Functions here should be made available as pytest fixtures.

Other documented resources can be used by import from certbot_deployer.testing, e.g.:

from certbot_deployer.testing import COMMON_NAME
  1"""
  2Resources to assist in testing Certbot Deployer and its plugins
  3
  4Functions here should be made available as pytest
  5[fixtures](https://docs.pytest.org/en/latest/how-to/fixtures.html#how-to-fixtures).
  6
  7Other documented resources can be used by import from `certbot_deployer.testing`, e.g.:
  8
  9```
 10from certbot_deployer.testing import COMMON_NAME
 11```
 12"""
 13
 14from datetime import datetime
 15from pathlib import Path
 16from typing import List, Optional, Tuple
 17
 18from cryptography import x509
 19from cryptography.hazmat.primitives import hashes, serialization
 20from cryptography.hazmat.primitives.asymmetric import rsa
 21from cryptography.x509.oid import NameOID
 22
 23import pytest
 24
 25from certbot_deployer import (
 26    CERT_FILENAME,
 27    FULLCHAIN_FILENAME,
 28    INTERMEDIATES_FILENAME,
 29    KEY_FILENAME,
 30)
 31from certbot_deployer.deployer import CertificateBundle
 32
 33
 34COMMON_NAME: str = "test_common_name"
 35""" A hard-coded default value used by testing fixtures """
 36NOT_VALID_BEFORE: datetime = datetime(2020, 1, 1)
 37""" A hard-coded default value used by testing fixtures """
 38NOT_VALID_AFTER: datetime = datetime(2099, 1, 1)
 39""" A hard-coded default value used by testing fixtures """
 40
 41
 42@pytest.fixture(name="certbot_deployer_self_signed_certificate_bundle")
 43# pylint: disable=too-many-locals
 44def fixture_self_signed_certificate_bundle(
 45    request: pytest.FixtureRequest,
 46    tmp_path: Path,
 47) -> CertificateBundle:
 48    """
 49    Pytest fixture which generates a self-signed certificate bundle for testing.
 50
 51    Example usage:
 52
 53    ```
 54    import certbot_deployer
 55    from certbot_deployer.testing import COMMON_NAME
 56
 57    def test_your_plugin_function(
 58        certbot_deployer_self_signed_certificate_bundle: certbot_deployer.CertificateBundle
 59    ) -> None:
 60        assert certbot_deployer_self_signed_certificate_bundle.common_name == COMMON_NAME
 61    ```
 62
 63    Args:
 64        request (`pytest.FixtureRequest`): The pytest request object to access
 65            indirect fixture parameters.
 66
 67    Parameters for Pytest indirect parameterization via `request`:
 68
 69    * common_name (str): The desired Common Name for the certificate, else COMMON_NAME
 70    * not_valid_before (datetime): The certificate validity start time, else NOT_VALID_BEFORE
 71    * not_valid_after (datetime): The certificate validity end time, else NOT_VALID_AFTER
 72    * path (pathlib.Path): optional path in which to create the bundle files
 73
 74    Returns:
 75        `certbot_deployer.deployer.CertificateBundle` corresponding to the created bundle.
 76    """
 77    req_params: dict = getattr(request, "param", {})
 78    not_valid_before: datetime = NOT_VALID_BEFORE
 79    if "not_valid_before" in req_params:
 80        assert isinstance(req_params["not_valid_before"], datetime)
 81        not_valid_before = req_params["not_valid_before"]
 82    not_valid_after: datetime = NOT_VALID_AFTER
 83    if "not_valid_after" in req_params:
 84        assert isinstance(req_params["not_valid_after"], datetime)
 85        not_valid_after = req_params["not_valid_after"]
 86    path: Optional[Path] = None
 87    if "path" in req_params:
 88        assert isinstance(req_params["path"], (Path, type(None)))
 89        path = req_params["path"]
 90    subject_alternative_names: List[str] = []
 91    if "subject_alternative_names" in req_params:
 92        assert isinstance(req_params["subject_alternative_names"], list)
 93        subject_alternative_names = req_params["subject_alternative_names"]
 94    common_name: Optional[str] = COMMON_NAME
 95    if "common_name" in req_params:
 96        assert isinstance(req_params["common_name"], (str, type(None)))
 97        common_name = req_params["common_name"]
 98
 99    bundle_path: Path = path if path is not None else tmp_path
100    key: rsa.RSAPrivateKey = rsa.generate_private_key(
101        public_exponent=65537, key_size=2048
102    )
103    subject: x509.Name
104    issuer: x509.Name
105    if common_name is None:
106        subject = issuer = x509.Name([])
107    else:
108        subject = issuer = x509.Name(
109            [x509.NameAttribute(NameOID.COMMON_NAME, common_name)]
110        )
111    cert_builder: x509.CertificateBuilder = (
112        x509.CertificateBuilder()
113        .subject_name(subject)
114        .issuer_name(issuer)
115        .public_key(key.public_key())
116        .serial_number(x509.random_serial_number())
117        .not_valid_before(not_valid_before)
118        .not_valid_after(not_valid_after)
119        .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
120    )
121    if subject_alternative_names:
122        san_extension = x509.SubjectAlternativeName(
123            [x509.DNSName(fqdn) for fqdn in subject_alternative_names]
124        )
125        cert_builder = cert_builder.add_extension(san_extension, critical=False)
126    cert: x509.Certificate = cert_builder.sign(key, hashes.SHA256())
127    pem_bytes: bytes = cert.public_bytes(encoding=serialization.Encoding.PEM)
128
129    key_text: str = key.private_bytes(
130        encoding=serialization.Encoding.PEM,
131        format=serialization.PrivateFormat.PKCS8,
132        encryption_algorithm=serialization.NoEncryption(),
133    ).decode("utf-8")
134    cert_text: str = pem_bytes.decode("utf-8")
135
136    (bundle_path / CERT_FILENAME).write_text(cert_text, encoding="utf-8")
137    (bundle_path / KEY_FILENAME).write_text(key_text, encoding="utf-8")
138    (bundle_path / INTERMEDIATES_FILENAME).write_text(cert_text, encoding="utf-8")
139    (bundle_path / FULLCHAIN_FILENAME).write_text(
140        f"{cert_text}\n{cert_text}", encoding="utf-8"
141    )
142
143    return CertificateBundle(path_obj=bundle_path)
144
145
146# pylint: enable=too-many-locals
COMMON_NAME: str = 'test_common_name'

A hard-coded default value used by testing fixtures

NOT_VALID_BEFORE: datetime.datetime = datetime.datetime(2020, 1, 1, 0, 0)

A hard-coded default value used by testing fixtures

NOT_VALID_AFTER: datetime.datetime = datetime.datetime(2099, 1, 1, 0, 0)

A hard-coded default value used by testing fixtures

@pytest.fixture(name='certbot_deployer_self_signed_certificate_bundle')
def fixture_self_signed_certificate_bundle( request: _pytest.fixtures.FixtureRequest, tmp_path: pathlib.Path) -> certbot_deployer.deployer.CertificateBundle:
 43@pytest.fixture(name="certbot_deployer_self_signed_certificate_bundle")
 44# pylint: disable=too-many-locals
 45def fixture_self_signed_certificate_bundle(
 46    request: pytest.FixtureRequest,
 47    tmp_path: Path,
 48) -> CertificateBundle:
 49    """
 50    Pytest fixture which generates a self-signed certificate bundle for testing.
 51
 52    Example usage:
 53
 54    ```
 55    import certbot_deployer
 56    from certbot_deployer.testing import COMMON_NAME
 57
 58    def test_your_plugin_function(
 59        certbot_deployer_self_signed_certificate_bundle: certbot_deployer.CertificateBundle
 60    ) -> None:
 61        assert certbot_deployer_self_signed_certificate_bundle.common_name == COMMON_NAME
 62    ```
 63
 64    Args:
 65        request (`pytest.FixtureRequest`): The pytest request object to access
 66            indirect fixture parameters.
 67
 68    Parameters for Pytest indirect parameterization via `request`:
 69
 70    * common_name (str): The desired Common Name for the certificate, else COMMON_NAME
 71    * not_valid_before (datetime): The certificate validity start time, else NOT_VALID_BEFORE
 72    * not_valid_after (datetime): The certificate validity end time, else NOT_VALID_AFTER
 73    * path (pathlib.Path): optional path in which to create the bundle files
 74
 75    Returns:
 76        `certbot_deployer.deployer.CertificateBundle` corresponding to the created bundle.
 77    """
 78    req_params: dict = getattr(request, "param", {})
 79    not_valid_before: datetime = NOT_VALID_BEFORE
 80    if "not_valid_before" in req_params:
 81        assert isinstance(req_params["not_valid_before"], datetime)
 82        not_valid_before = req_params["not_valid_before"]
 83    not_valid_after: datetime = NOT_VALID_AFTER
 84    if "not_valid_after" in req_params:
 85        assert isinstance(req_params["not_valid_after"], datetime)
 86        not_valid_after = req_params["not_valid_after"]
 87    path: Optional[Path] = None
 88    if "path" in req_params:
 89        assert isinstance(req_params["path"], (Path, type(None)))
 90        path = req_params["path"]
 91    subject_alternative_names: List[str] = []
 92    if "subject_alternative_names" in req_params:
 93        assert isinstance(req_params["subject_alternative_names"], list)
 94        subject_alternative_names = req_params["subject_alternative_names"]
 95    common_name: Optional[str] = COMMON_NAME
 96    if "common_name" in req_params:
 97        assert isinstance(req_params["common_name"], (str, type(None)))
 98        common_name = req_params["common_name"]
 99
100    bundle_path: Path = path if path is not None else tmp_path
101    key: rsa.RSAPrivateKey = rsa.generate_private_key(
102        public_exponent=65537, key_size=2048
103    )
104    subject: x509.Name
105    issuer: x509.Name
106    if common_name is None:
107        subject = issuer = x509.Name([])
108    else:
109        subject = issuer = x509.Name(
110            [x509.NameAttribute(NameOID.COMMON_NAME, common_name)]
111        )
112    cert_builder: x509.CertificateBuilder = (
113        x509.CertificateBuilder()
114        .subject_name(subject)
115        .issuer_name(issuer)
116        .public_key(key.public_key())
117        .serial_number(x509.random_serial_number())
118        .not_valid_before(not_valid_before)
119        .not_valid_after(not_valid_after)
120        .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
121    )
122    if subject_alternative_names:
123        san_extension = x509.SubjectAlternativeName(
124            [x509.DNSName(fqdn) for fqdn in subject_alternative_names]
125        )
126        cert_builder = cert_builder.add_extension(san_extension, critical=False)
127    cert: x509.Certificate = cert_builder.sign(key, hashes.SHA256())
128    pem_bytes: bytes = cert.public_bytes(encoding=serialization.Encoding.PEM)
129
130    key_text: str = key.private_bytes(
131        encoding=serialization.Encoding.PEM,
132        format=serialization.PrivateFormat.PKCS8,
133        encryption_algorithm=serialization.NoEncryption(),
134    ).decode("utf-8")
135    cert_text: str = pem_bytes.decode("utf-8")
136
137    (bundle_path / CERT_FILENAME).write_text(cert_text, encoding="utf-8")
138    (bundle_path / KEY_FILENAME).write_text(key_text, encoding="utf-8")
139    (bundle_path / INTERMEDIATES_FILENAME).write_text(cert_text, encoding="utf-8")
140    (bundle_path / FULLCHAIN_FILENAME).write_text(
141        f"{cert_text}\n{cert_text}", encoding="utf-8"
142    )
143
144    return CertificateBundle(path_obj=bundle_path)

Pytest fixture which generates a self-signed certificate bundle for testing.

Example usage:

import certbot_deployer
from certbot_deployer.testing import COMMON_NAME

def test_your_plugin_function(
    certbot_deployer_self_signed_certificate_bundle: certbot_deployer.CertificateBundle
) -> None:
    assert certbot_deployer_self_signed_certificate_bundle.common_name == COMMON_NAME
Arguments:
  • request (pytest.FixtureRequest): The pytest request object to access indirect fixture parameters.

Parameters for Pytest indirect parameterization via request:

  • common_name (str): The desired Common Name for the certificate, else COMMON_NAME
  • not_valid_before (datetime): The certificate validity start time, else NOT_VALID_BEFORE
  • not_valid_after (datetime): The certificate validity end time, else NOT_VALID_AFTER
  • path (pathlib.Path): optional path in which to create the bundle files
Returns:

certbot_deployer.deployer.CertificateBundle corresponding to the created bundle.