Skip to content

Commit

Permalink
run test suite in the future to test resilience against progress of t…
Browse files Browse the repository at this point in the history
…ime (and move failing tests to pytest, while at it)
  • Loading branch information
mathiasertl committed Apr 21, 2024
1 parent 87ef68a commit 8e3d77d
Show file tree
Hide file tree
Showing 21 changed files with 1,038 additions and 811 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/faketime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Tests with faked time
on:
push:
pull_request:
types: [opened, reopened]
pull_request_target:

jobs:
run:
runs-on: ubuntu-latest

name: libfaketime test
steps:
- name: Install APT dependencies
run: sudo apt-get install -y firefox faketime

- name: Acquire sources
uses: actions/[email protected]

- name: Setup Python
uses: actions/[email protected]
with:
python-version: 3.12
architecture: x64

- name: Apply caching of dependencies
uses: actions/[email protected]
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('**/requirements-*.txt') }}

- name: Install dependencies
run: |
pip install -U pip setuptools wheel
pip install -r requirements.txt -r requirements/requirements-test.txt
- name: Run tests
run: faketime -f +100y pytest -v --cov-report term-missing --durations=20
1 change: 1 addition & 0 deletions ca/django_ca/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ def lookups( # type: ignore # we are more specific here

def queryset(self, request: HttpRequest, queryset: QuerySetTypeVar) -> QuerySetTypeVar:
now = timezone.now()
print("###", now)

if self.value() == "0":
return queryset.filter(expires__gt=now)
Expand Down
3 changes: 3 additions & 0 deletions ca/django_ca/tests/admin/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def assert_changelist_response(response: "HttpResponse", *objects: models.Model)
def sorter(obj: models.Model) -> Any:
return obj.pk

print("### found", response.context["cl"].result_list)
for found in response.context["cl"].result_list:
print(found.expires)
assert sorted(response.context["cl"].result_list, key=sorter) == sorted(objects, key=sorter)
templates = [t.name for t in response.templates]
assert "admin/base.html" in templates
Expand Down
2 changes: 1 addition & 1 deletion ca/django_ca/tests/admin/test_add_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,7 @@ def test_only_ca_prefill(self) -> None:
cn = "test-only-ca.example.com"
assert self.ca.sign_authority_information_access is not None
assert self.ca.sign_crl_distribution_points is not None
self.ca.sign_certificate_policies = self.certificate_policies(
self.ca.sign_certificate_policies = certificate_policies(
x509.PolicyInformation(
policy_identifier=x509.ObjectIdentifier("1.2.3"),
policy_qualifiers=[
Expand Down
3 changes: 2 additions & 1 deletion ca/django_ca/tests/admin/test_admin_ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from django_ca.models import CertificateAuthority
from django_ca.tests.admin.assertions import assert_change_response
from django_ca.tests.base.mixins import AdminTestCaseMixin, StandardAdminViewTestCaseMixin
from django_ca.tests.base.utils import certificate_policies


class CertificateAuthorityAdminViewTestCase(StandardAdminViewTestCaseMixin[CertificateAuthority], TestCase):
Expand Down Expand Up @@ -55,7 +56,7 @@ def test_complex_sign_certificate_policies(self) -> None:
# used for that extension.
self.assertNotIn(ExtensionOID.CERTIFICATE_POLICIES, ca.extensions)

ca.sign_certificate_policies = self.certificate_policies(
ca.sign_certificate_policies = certificate_policies(
x509.PolicyInformation(
policy_identifier=CertificatePoliciesOID.ANY_POLICY,
policy_qualifiers=[
Expand Down
34 changes: 28 additions & 6 deletions ca/django_ca/tests/base/conftest_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,19 @@ def fixture(
db: Any, # pylint: disable=unused-argument # usefixtures does not work for fixtures
) -> Iterator[CertificateAuthority]:
data = CERT_DATA[name]
pub = request.getfixturevalue(f"{name}_pub")
ca_fixture_name = f"{name}_pub"
if data["cat"] == "sphinx-contrib":
ca_fixture_name = f"contrib_{ca_fixture_name}"
pub = request.getfixturevalue(ca_fixture_name)

# Load any parent
parent = None
if parent_name := data.get("parent"):
parent = request.getfixturevalue(parent_name)

kwargs = {
"sign_crl_distribution_points": data["sign_crl_distribution_points"],
"sign_authority_information_access": data["sign_authority_information_access"],
"sign_crl_distribution_points": data.get("sign_crl_distribution_points"),
"sign_authority_information_access": data.get("sign_authority_information_access"),
}

ca = load_ca(name, pub, parent, **kwargs)
Expand Down Expand Up @@ -204,8 +207,16 @@ def generate_cert_fixture(name: str) -> typing.Callable[["SubRequest"], Iterator
def fixture(request: "SubRequest") -> Iterator[Certificate]:
sanitized_name = name.replace("-", "_")
data = CERT_DATA[name]
ca = request.getfixturevalue(data["ca"])
pub = request.getfixturevalue(f"{sanitized_name}_pub")

ca_fixture_name = data["ca"]
if data["cat"] == "sphinx-contrib":
ca_fixture_name = f"contrib_{ca_fixture_name}"
ca = request.getfixturevalue(ca_fixture_name)

pub_fixture_name = f"{sanitized_name}_pub"
if data["cat"] in ("contrib", "sphinx-contrib"):
pub_fixture_name = f"contrib_{pub_fixture_name}"
pub = request.getfixturevalue(pub_fixture_name)
cert = load_cert(ca, None, pub, data.get("profile", ""))

yield cert # NOTE: Yield must be outside the freeze-time block, or durations are wrong
Expand All @@ -219,8 +230,11 @@ def load_pub(name: str) -> x509.Certificate:
if conf["cat"] == "sphinx-contrib":
with open(conf["pub_path"], "rb") as stream:
return x509.load_pem_x509_certificate(stream.read())
if conf["cat"] == "contrib":
with open(FIXTURES_DIR / "contrib" / f"{name}.pub", "rb") as stream:
return x509.load_der_x509_certificate(stream.read())
else:
with open(os.path.join(FIXTURES_DIR, f"{name}.pub"), "rb") as stream:
with open(FIXTURES_DIR / f"{name}.pub", "rb") as stream:
return x509.load_der_x509_certificate(stream.read())


Expand Down Expand Up @@ -290,6 +304,9 @@ def load_cert(
usable_ca_names = [
name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and conf.get("key_filename")
]
contrib_ca_names = [
name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and conf["cat"] == "sphinx-contrib"
]
unusable_ca_names = [
name for name, conf in CERT_DATA.items() if conf["type"] == "ca" and name not in usable_ca_names
]
Expand All @@ -302,6 +319,11 @@ def load_cert(
usable_cert_names = [
name for name, conf in CERT_DATA.items() if conf["type"] == "cert" and conf["cat"] == "generated"
]
contrib_cert_names = [
name
for name, conf in CERT_DATA.items()
if conf["type"] == "cert" and conf["cat"] in ("contrib", "sphinx-contrib")
]
unusable_cert_names = [
name for name, conf in CERT_DATA.items() if conf["type"] == "cert" and name not in usable_ca_names
]
Expand Down
2 changes: 1 addition & 1 deletion ca/django_ca/tests/base/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def _load_latest_version(versions: list[str]) -> tuple[int, int]:
"cn": "",
"key_filename": False,
"csr_filename": False,
"pub_filename": os.path.join("contrib", "multiple_ous_and_no_ext.pub"),
"pub_filename": os.path.join("contrib", "multiple_ous.pub"),
"key_type": "RSA",
"cat": "contrib",
"type": "cert",
Expand Down
32 changes: 24 additions & 8 deletions ca/django_ca/tests/base/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from cryptography import x509
from cryptography.x509.oid import CertificatePoliciesOID, ExtensionOID, NameOID

from django.core.cache import cache
from django.core.files.storage import storages

import pytest
Expand All @@ -34,13 +35,15 @@
from django_ca.key_backends.storages import StoragesBackend
from django_ca.models import Certificate, CertificateAuthority
from django_ca.tests.base.conftest_helpers import (
all_ca_names,
all_cert_names,
ca_cert_names,
precertificate_signed_certificate_timestamps_cert_names,
signed_certificate_timestamp_cert_names,
signed_certificate_timestamps_cert_names,
usable_ca_names,
usable_cert_names,
)
from django_ca.tests.base.constants import CERT_DATA


@pytest.fixture(params=all_cert_names)
Expand Down Expand Up @@ -206,8 +209,7 @@ def key_backend(request: "SubRequest") -> Iterator[StoragesBackend]:
def precertificate_signed_certificate_timestamps_pub(request: "SubRequest") -> Iterator[x509.Certificate]:
"""Parametrized fixture for certificates that have a PrecertSignedCertificateTimestamps extension."""
name = request.param.replace("-", "_")

yield request.getfixturevalue(f"{name}_pub")
yield request.getfixturevalue(f"contrib_{name}_pub")


@pytest.fixture()
Expand All @@ -231,8 +233,7 @@ def secondary_backend(request: "SubRequest") -> Iterator[StoragesBackend]:
def signed_certificate_timestamp_pub(request: "SubRequest") -> Iterator[x509.Certificate]:
"""Parametrized fixture for certificates that have any SCT extension."""
name = request.param.replace("-", "_")

yield request.getfixturevalue(f"{name}_pub")
yield request.getfixturevalue(f"contrib_{name}_pub")


@pytest.fixture(params=signed_certificate_timestamps_cert_names)
Expand Down Expand Up @@ -293,6 +294,15 @@ def tmpcadir(tmp_path: Path, settings: SettingsWrapper) -> Iterator[Path]:
settings.STORAGES = orig_storages


@pytest.fixture(params=all_ca_names)
def ca(request: "SubRequest") -> Iterator[CertificateAuthority]:
"""Parametrized fixture for all certificate authorities known to the test suite."""
fixture_name = request.param
if CERT_DATA[fixture_name]["cat"] in ("contrib", "sphinx-contrib"):
fixture_name = f"contrib_{fixture_name}"
yield request.getfixturevalue(fixture_name)


@pytest.fixture(params=usable_ca_names)
def usable_ca_name(request: "SubRequest") -> Iterator[CertificateAuthority]:
"""Parametrized fixture for the name of every usable CA."""
Expand All @@ -314,9 +324,15 @@ def usable_cas(request: "SubRequest") -> Iterator[list[CertificateAuthority]]:
yield cas


@pytest.fixture(params=ca_cert_names)
@pytest.fixture(params=usable_cert_names)
def usable_cert(request: "SubRequest") -> Iterator[Certificate]:
"""Parametrized fixture for every ``{ca}-cert`` certificate."""
cert = request.getfixturevalue(request.param.replace("-", "_"))
"""Parametrized fixture for every ``{ca}-cert`` certificate.
The name of the certificate can be retrieved from the non-standard `test_name` property of the
certificate.
"""
name = request.param
cert = request.getfixturevalue(name.replace("-", "_"))
cert.test_name = name
request.getfixturevalue(f"usable_{cert.ca.name}")
yield cert
67 changes: 1 addition & 66 deletions ca/django_ca/tests/base/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import copy
import json
import re
import textwrap
import typing
from collections.abc import Iterable, Iterator
from contextlib import contextmanager
Expand All @@ -37,7 +36,6 @@
from django.contrib.messages import get_messages
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.storage import storages
from django.test.testcases import SimpleTestCase
from django.urls import reverse

Expand All @@ -46,14 +44,12 @@

from django_ca import ca_settings
from django_ca.deprecation import crl_last_update, crl_next_update, revoked_certificate_revocation_date
from django_ca.extensions import extension_as_text
from django_ca.models import Certificate, CertificateAuthority, DjangoCAModel, X509CertMixin
from django_ca.signals import post_revoke_cert, post_sign_cert, pre_sign_cert
from django_ca.tests.admin.assertions import assert_change_response, assert_changelist_response
from django_ca.tests.base.constants import CERT_DATA, TIMESTAMPS
from django_ca.tests.base.mocks import mock_signal
from django_ca.tests.base.typehints import DjangoCAModelTypeVar
from django_ca.tests.base.utils import certificate_policies

if typing.TYPE_CHECKING:
# Use SimpleTestCase as base class when type checking. This way mypy will know about attributes/methods
Expand All @@ -66,7 +62,7 @@
TestCaseProtocol = object


class TestCaseMixin(TestCaseProtocol): # pylint: disable=too-many-public-methods
class TestCaseMixin(TestCaseProtocol):
"""Mixin providing augmented functionality to all test cases."""

load_cas: Union[str, tuple[str, ...]] = tuple()
Expand Down Expand Up @@ -345,27 +341,6 @@ def assertValidationError( # pylint: disable=invalid-name; unittest standard
yield
self.assertEqual(cmex.exception.message_dict, errors)

@property
def ca_certs(self) -> Iterator[tuple[str, Certificate]]:
"""Yield loaded certificates for each certificate authority."""
for name, cert in self.certs.items():
if name in [
"root-cert",
"child-cert",
"ec-cert",
"dsa-cert",
"pwd-cert",
"ed448-cert",
"ed25519-cert",
]:
yield name, cert

def certificate_policies(
self, *policies: x509.PolicyInformation, critical: bool = False
) -> x509.Extension[x509.CertificatePolicies]:
"""Shortcut for getting a Certificate Policy extension."""
return certificate_policies(*policies, critical=critical)

def crl_distribution_points(
self,
full_name: Optional[Iterable[x509.GeneralName]] = None,
Expand Down Expand Up @@ -446,39 +421,6 @@ def freeze_time(
with freeze_time(timestamp) as frozen:
yield frozen

def get_cert_context(self, name: str) -> dict[str, Any]:
"""Get a dictionary suitable for testing output based on the dictionary in basic.certs."""
ctx: dict[str, Any] = {}

for key, value in sorted(CERT_DATA[name].items()):
# Handle cryptography extensions
if key == "extensions":
ctx["extensions"] = {ext["type"]: ext for ext in CERT_DATA[name].get("extensions", [])}
elif key == "precert_poison":
ctx["precert_poison"] = "* Precert Poison (critical):\n Yes"
elif isinstance(value, x509.Extension):
if value.critical:
ctx[f"{key}_critical"] = " (critical)"
else:
ctx[f"{key}_critical"] = ""

ctx[f"{key}_text"] = textwrap.indent(extension_as_text(value.value), " ")
elif key == "path_length":
ctx[key] = value
ctx[f"{key}_text"] = "unlimited" if value is None else value
else:
ctx[key] = value

if parent := CERT_DATA[name].get("parent"):
ctx["parent_name"] = CERT_DATA[parent]["name"]
ctx["parent_serial"] = CERT_DATA[parent]["serial"]
ctx["parent_serial_colons"] = CERT_DATA[parent]["serial_colons"]

if CERT_DATA[name]["key_filename"] is not False:
storage = storages["django-ca"]
ctx["key_path"] = storage.path(CERT_DATA[name]["key_filename"])
return ctx

@classmethod
def load_ca(
cls,
Expand Down Expand Up @@ -586,13 +528,6 @@ def usable_cas(self) -> Iterator[tuple[str, CertificateAuthority]]:
if CERT_DATA[name]["key_filename"]:
yield name, ca

@property
def usable_certs(self) -> Iterator[tuple[str, Certificate]]:
"""Yield loaded generated certificates."""
for name, cert in self.certs.items():
if CERT_DATA[name]["cat"] == "generated":
yield name, cert


class AdminTestCaseMixin(TestCaseMixin, typing.Generic[DjangoCAModelTypeVar]):
"""Common mixin for testing admin classes for models."""
Expand Down
Loading

0 comments on commit 8e3d77d

Please sign in to comment.