Skip to content

Commit

Permalink
Add extensive coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
rkoopmans committed Aug 11, 2024
1 parent 4655675 commit 25d7acc
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 26 deletions.
5 changes: 4 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ source = cert_chain_resolver
[paths]
source =
cert_chain_resolver
*/cert_chain_resolver
*/cert_chain_resolver

[report]
include = cert_chain_resolver/*
9 changes: 4 additions & 5 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
python -m pip install -r requirements_dev.txt
- name: Test with pytest and coverage
run: |
pytest --cov=./cert_chain_resolver --cov-report term-missing -n auto
pytest --rootdir=. --cov=./cert_chain_resolver --cov-report term-missing -n auto tests/
- name: Upload coverage artifact
if: success()
uses: actions/upload-artifact@v4
Expand All @@ -46,7 +46,7 @@ jobs:
python -m pip install -r requirements_dev.txt
- name: Test with pytest and coverage
run: |
pytest --cov=./cert_chain_resolver --cov-report term-missing -n auto
pytest --rootdir=. --cov=./cert_chain_resolver --cov-report term-missing -n auto tests/
- name: Upload coverage artifact
if: success()
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -74,17 +74,16 @@ jobs:
pattern: coverage-*
- name: Combine coverage reports
run: |
ls -lashR .
set -x
coverage combine coverage-*/.coverage
coverage report
coverage xml -o combined_coverage.xml
coverage xml -o ./coverage.xml
coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./combined_coverage.xml
file: ./coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
mypy:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.3.1

* is_issued_by now raises MissingCertProperty if no hash algorithm found. Before it would silently return False

## 1.3.0

New feature and sane defaults. for the CLI the root is now by default excluded, at first it would include it if it found one, but not all certificate authorities provide a link to their root in their certs. This resulted in sometimes a root to be included and othertimes not.
Expand Down
5 changes: 3 additions & 2 deletions cert_chain_resolver/castore/base_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
try:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from cert_chain_resolver.models import Cert
except ImportError:
except ImportError: # pragma: no cover

pass


Expand Down
4 changes: 2 additions & 2 deletions cert_chain_resolver/castore/file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
try:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from cert_chain_resolver.models import Cert
except ImportError:
except ImportError: # pragma: no cover
pass


Expand Down
4 changes: 2 additions & 2 deletions cert_chain_resolver/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
try:
from typing import Optional
from cert_chain_resolver.castore.base_store import CAStore
except ImportError:
except ImportError: # pragma: no cover
pass


Expand Down Expand Up @@ -133,5 +133,5 @@ def main():
cli(**cli_args)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
14 changes: 5 additions & 9 deletions cert_chain_resolver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.exceptions import InvalidSignature


import binascii
Expand All @@ -15,9 +16,9 @@
try:
from typing import List, Union, Optional, Type, Iterator, TYPE_CHECKING

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
import datetime
except ImportError:
except ImportError: # pragma: no cover
pass

try:
Expand All @@ -44,13 +45,8 @@ def __init__(self, x509_obj):

def __repr__(self):
# type: () -> str
try:
common_name = self.common_name
except MissingCertProperty:
common_name = None

return '<Cert common_name="{0}" subject="{1}" issuer="{2}">'.format(
common_name, self.subject, self.issuer
self.common_name, self.subject, self.issuer
)

def __eq__(self, other):
Expand Down Expand Up @@ -209,7 +205,7 @@ def is_issued_by(self, other):
ECDSA(hash_algorithm),
)
return True
except Exception:
except InvalidSignature as e:
pass

return False
Expand Down
2 changes: 1 addition & 1 deletion cert_chain_resolver/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
try:
from typing import Any, Optional
from cert_chain_resolver.castore.base_store import CAStore
except ImportError:
except ImportError: # pragma: no cover
pass


Expand Down
4 changes: 2 additions & 2 deletions cert_chain_resolver/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
try:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from cert_chain_resolver.models import Cert

except ImportError:
except ImportError: # pragma: no cover
pass


Expand Down
18 changes: 18 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from cert_chain_resolver import api
import pytest
from cert_chain_resolver.resolver import resolve
from cert_chain_resolver.models import CertificateChain, Cert
from cert_chain_resolver.castore.file_system import FileSystemStore


@pytest.mark.parametrize(
"exported,obj",
[
("resolve", resolve),
("CertificateChain", CertificateChain),
("Cert", Cert),
("FileSystemStore", FileSystemStore),
],
)
def test_api_exports_right_objects(exported, obj):
assert getattr(api, exported) == obj
9 changes: 9 additions & 0 deletions tests/test_castore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest
from cert_chain_resolver.castore.base_store import CAStore
from cert_chain_resolver.models import Cert


def test_find_issuer_candidates_needs_impl(mocker):
m = mocker.Mock(spec=Cert)
with pytest.raises(NotImplementedError):
CAStore().find_issuer_candidates(m)
11 changes: 10 additions & 1 deletion tests/test_castore_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
RootCertificateNotFound,
)
from cert_chain_resolver.models import Cert
from cert_chain_resolver.castore.file_system import FileSystemStore
from cert_chain_resolver.castore.file_system import FileSystemStore, eligible_paths
from tests.fixtures import BUNDLE_FIXTURES, certfixture_to_id
import tempfile
import pytest
Expand Down Expand Up @@ -35,3 +35,12 @@ def test_custom_bundle_path_that_does_not_resolve_certs(bundle):
def test_bundle_path_does_not_exist():
with pytest.raises(CertificateChainResolverError):
store = FileSystemStore("/tmp/addd/a/sd/df/g/h/j/x/vz/a/i-dont-exist.pem")


def test_bundle_path_cannot_be_found(monkeypatch):
monkeypatch.setattr(
"cert_chain_resolver.castore.file_system.eligible_paths",
["/tmp/i-do-not-exist"],
)
with pytest.raises(CertificateChainResolverError, match="Can't detect CA bundle"):
FileSystemStore()
138 changes: 137 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import importlib
import sys
from tempfile import NamedTemporaryFile
import pytest
from cert_chain_resolver import __is_py3__

from cert_chain_resolver.cli import cli
from cert_chain_resolver.cli import cli, main, parse_args
from cert_chain_resolver.castore.file_system import FileSystemStore
from .fixtures import BUNDLE_FIXTURES, certfixture_to_id

Expand All @@ -11,6 +14,82 @@
unicode = str


@pytest.mark.parametrize(
"cli_args, expected",
[
(
[],
{
"file_name": "-",
"info": False,
"include_root": False,
"ca_bundle_path": None,
},
),
(
["test.crt"],
{
"file_name": "test.crt",
"info": False,
"include_root": False,
"ca_bundle_path": None,
},
),
(
["-i"],
{
"file_name": "-",
"info": True,
"include_root": False,
"ca_bundle_path": None,
},
),
(
["--include-root"],
{
"file_name": "-",
"info": False,
"include_root": True,
"ca_bundle_path": None,
},
),
(
["--ca-bundle-path", "/path/to/ca/bundle"],
{
"file_name": "-",
"info": False,
"include_root": False,
"ca_bundle_path": "/path/to/ca/bundle",
},
),
(
[
"test.crt",
"-i",
"--include-root",
"--ca-bundle-path",
"/path/to/ca/bundle",
],
{
"file_name": "test.crt",
"info": True,
"include_root": True,
"ca_bundle_path": "/path/to/ca/bundle",
},
),
],
)
def test_parse_args(cli_args, expected, monkeypatch):
monkeypatch.setattr("sys.argv", ["script_name"] + cli_args)

args = parse_args()

assert args.file_name == expected["file_name"]
assert args.info == expected["info"]
assert args.include_root == expected["include_root"]
assert args.ca_bundle_path == expected["ca_bundle_path"]


@pytest.mark.parametrize("bundle", BUNDLE_FIXTURES, ids=certfixture_to_id)
def test_cert_returns_completed_chain(capsys, bundle):
cli(file_bytes=bundle[0]["cert_pem"])
Expand Down Expand Up @@ -97,3 +176,60 @@ def test_display_flag_is_properly_formatted(capsys):
)

assert expected == captured


def test_display_flag_includes_warning_when_root_was_requested_but_not_found(capsys):
bundle = BUNDLE_FIXTURES[0]
cli(file_bytes=bundle[0]["cert_pem"], show_details=True, include_root=True)
captured = unicode(capsys.readouterr().err)
assert captured == "WARNING: Root certificate was requested, but not found!\n"


@pytest.mark.parametrize(
"file_name, expected_content",
[("test.pem", b"test certificate data"), ("-", b"stdin data")],
)
def test_main_handles_different_file_input(mocker, file_name, expected_content):
args = mocker.Mock(
info=True, include_root=False, ca_bundle_path="/test/path", file_name="test.pem"
)
args.file_name = file_name
mocker.patch("cert_chain_resolver.cli.parse_args", return_value=args)

fs_store = mocker.patch("cert_chain_resolver.cli.FileSystemStore")

if file_name == "-":
if __is_py3__:
mocker.patch("sys.stdin.buffer.read", return_value=expected_content)
else:
mocker.patch("sys.stdin.read", return_value=expected_content)
else:
if __is_py3__:
mocker.patch("builtins.open", mocker.mock_open(read_data=expected_content))
else:
mocker.patch(
"__builtin__.open", mocker.mock_open(read_data=expected_content)
)

mock_cli = mocker.patch("cert_chain_resolver.cli.cli")

main()

assert fs_store.call_args == mocker.call(
"/test/path",
)
assert mock_cli.call_args == mocker.call(
file_bytes=expected_content,
show_details=True,
include_root=False,
root_ca_store=mocker.ANY,
)


def test_main_no_args_tty_shows_help_and_exits(mocker):
mocker.patch("sys.stdin.isatty", return_value=True)
mocker.patch("sys.argv", ["script_name"])

with pytest.raises(SystemExit):
main()
assert sys.argv == ["script_name", "-h"]
Loading

0 comments on commit 25d7acc

Please sign in to comment.