Changeset - fec20b53b9ff
[Not reviewed]
0 1 1
Branko Majic (branko) - 6 years ago 2018-04-17 22:20:25
branko@majic.rs
GC-22: Refactored server command tests for testing output on success:

- Introduced custom pytest fixture that sets-up a small Gimmecert
project.
- Introduced custom pytest fixture that sets-up private key with CSR.
- Replaced all server command tests that check the resulting output
with a parametrised test. One test should actually fail, but this is
a bug in implementation. Will fix in subsequent commit.
- Introduced separate tests that ensure the server private key or CSR
do no get overwritten in case DNS name update is requested.
2 files changed with 256 insertions and 101 deletions:
0 comments (0 inline, 0 general)
tests/conftest.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
#
 
# Copyright (C) 2018 Branko Majic
 
#
 
# This file is part of Gimmecert.
 
#
 
# Gimmecert is free software: you can redistribute it and/or modify it
 
# under the terms of the GNU General Public License as published by the Free
 
# Software Foundation, either version 3 of the License, or (at your option) any
 
# later version.
 
#
 
# Gimmecert is distributed in the hope that it will be useful, but
 
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
# details.
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Gimmecert.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
import collections
 
import io
 

	
 
import gimmecert
 

	
 
import pytest
 

	
 

	
 
@pytest.fixture
 
def key_with_csr(tmpdir):
 
    """
 
    Fixture that generates a private key and CSR within tmpdir, and
 
    provides information about them.
 

	
 
    The following artefacts are generated in the directory:
 

	
 
        - custom_csr/mycustom.key.pem (private key in OpenSSL-style PEM format)
 
        - custom_csr/mycustom.csr.pem (CSR in OpenSSL-style PEM format)
 

	
 
    :param tmpdir: Temporary directory (normally pytest tmpdir fixture) created for running the test.
 
    :type tmpdir: py.path.local
 

	
 
    :returns: Named tuple that describes the generated private key and CSR. The following properties are made available:
 
      private_key (cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey) - private key object.
 
      private_key_path (str) - path to generated private key.
 
      private_key_pem (str) - private key in OpenSSL-style PEM format.
 
      csr (cryptography.x509.CertificateSigningRequest) - CSR object.
 
      csr_path (str) - path to generated CSR.
 
      csr_pem (str) - CSR in OpenSSL-style PEM format.
 
    :rtype: collections.namedtuple
 
    """
 

	
 
    # Convenience named tuple for accessing generated artefacts.
 
    TestKeyWithCSR = collections.namedtuple('TestKeyWithCSR', 'private_key, private_key_path, private_key_pem, csr, csr_path, csr_pem')
 

	
 
    # Set-up directory for holding custom CSRs.
 
    custom_csr_dir = tmpdir.ensure('custom_csr', dir=True)
 

	
 
    # Set-up naming and some files.
 
    name = "mycustom"
 
    private_key_file = custom_csr_dir.join("%s.key.pem" % name)
 
    csr_file = custom_csr_dir.join("%s.csr.pem" % name)
 

	
 
    # Generate private key and CSR, and output them.
 
    private_key = gimmecert.crypto.generate_private_key()
 
    csr = gimmecert.crypto.generate_csr(name, private_key)
 

	
 
    gimmecert.storage.write_private_key(private_key, private_key_file.strpath)
 
    gimmecert.storage.write_csr(csr, csr_file.strpath)
 

	
 
    private_key_pem = private_key_file.read()
 
    csr_pem = csr_file.read()
 

	
 
    return TestKeyWithCSR(private_key, private_key_file.strpath, private_key_pem, csr, csr_file.strpath, csr_pem)
 

	
 

	
 
@pytest.fixture
 
def sample_project_directory(tmpdir):
 
    """
 
    Fixture that initialises a sample Gimmecert project within tmpdir,
 
    and issues a couple of client and server certificates using
 
    different methods (internal private key generation + issuance via
 
    CSR).
 

	
 
    Initialised CA hierarchy is 1 level deep, with basename used being
 
    identical to temporary directory base name.
 

	
 
    The following server certificates are issued:
 

	
 
        - server-with-csr-1 (server certificate, issued using custom CSR)
 
        - server-with-csr-2 (server certificate, issued using custom CSR)
 
        - server-with-privkey-1 (server certificate, Gimmecert-generated private key)
 
        - server-with-privkey-2 (server certificate, Gimmecert-generated private key)
 
        - client-with-csr-1 (client certificate, issued using custom CSR)
 
        - client-with-csr-2 (client certificate, issued using custom CSR)
 
        - client-with-privkey-1 (client certificate, Gimmecert-generated private key)
 
        - client-with-privkey-2 (client certificate, Gimmecert-generated private key)
 

	
 
    The following artefacts are created "external" to Gimmecert
 
    standard usage, mainly for the purpose of issuing certificates
 
    using CSR:
 

	
 
        - custom_csr/server-with-csr-1.key.pem (private key in OpenSSL-style PEM format, generated for creating CSR for issuing server-with-csr-1 certificate)
 
        - custom_csr/server-with-csr-1.csr.pem (CSR in OpenSSL-style PEM format, used for issuing sever-with-csr-1 certificate)
 
        - custom_csr/server-with-csr-2.key.pem (private key in OpenSSL-style PEM format, generated for creating CSR for issuing server-with-csr-2 certificate)
 
        - custom_csr/server-with-csr-2.csr.pem (CSR in OpenSSL-style PEM format, used for issuing sever-with-csr-2 certificate)
 
        - custom_csr/client-with-csr-1.key.pem (private key in OpenSSL-style PEM format, generated for creating CSR for issuing client-with-csr-1 certificate)
 
        - custom_csr/client-with-csr-1.csr.pem (CSR in OpenSSL-style PEM format, used for issuing sever-with-csr-1 certificate)
 
        - custom_csr/client-with-csr-2.key.pem (private key in OpenSSL-style PEM format, generated for creating CSR for issuing client-with-csr-2 certificate)
 
        - custom_csr/client-with-csr-2.csr.pem (CSR in OpenSSL-style PEM format, used for issuing sever-with-csr-2 certificate)
 

	
 
    :param tmpdir: Temporary directory (normally pytest tmpdir fixture) created for running the test.
 
    :type tmpdir: py.path.local
 

	
 
    :returs: Parent directory where Gimmecert has been initialised. Essentially the tmpdir fixture.
 
    :rtype: py.path.local
 
    """
 

	
 
    # Total amount of each type certificate to issue.
 
    per_type_count = 2
 

	
 
    # Set-up directory for holding custom CSRs.
 
    custom_csr_dir = tmpdir.ensure('custom_csr', dir=True)
 

	
 
    # Set-up some custom CSRs.
 
    for i in range(1, per_type_count + 1):
 
        # Used in generated samples.
 
        name = "server-with-csr-%d" % i
 
        private_key = gimmecert.crypto.generate_private_key()
 
        csr = gimmecert.crypto.generate_csr(name, private_key)
 
        gimmecert.storage.write_private_key(private_key, custom_csr_dir.join("%s.key.pem" % name).strpath)
 
        gimmecert.storage.write_csr(csr, custom_csr_dir.join("%s.csr.pem" % name).strpath)
 

	
 
        # Used in generated samples.
 
        name = "client-with-csr-%d" % i
 
        private_key = gimmecert.crypto.generate_private_key()
 
        csr = gimmecert.crypto.generate_csr(name, private_key)
 
        gimmecert.storage.write_private_key(private_key, custom_csr_dir.join("%s.key.pem" % name).strpath)
 
        gimmecert.storage.write_csr(csr, custom_csr_dir.join("%s.csr.pem" % name).strpath)
 

	
 
    # Initialise one-level deep hierarchy.
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, 1)
 

	
 
    # Issue a bunch of certificates.
 
    for i in range(1, per_type_count + 1):
 
        entity_name = "server-with-privkey-%d" % i
 
        gimmecert.commands.server(io.StringIO(), io.StringIO(), tmpdir.strpath, entity_name, None, False, None)
 

	
 
        entity_name = "server-with-csr-%d" % i
 
        custom_csr_path = custom_csr_dir.join("server-with-csr-%d.csr.pem" % i).strpath
 
        gimmecert.commands.server(io.StringIO(), io.StringIO(), tmpdir.strpath, entity_name, None, False, custom_csr_path)
 

	
 
        entity_name = "client-with-privkey-%d" % i
 
        gimmecert.commands.client(io.StringIO(), io.StringIO(), tmpdir.strpath, entity_name, None)
 

	
 
        entity_name = "client-with-csr-%d" % i
 
        custom_csr_path = custom_csr_dir.join("client-with-csr-%d.csr.pem" % i).strpath
 
        gimmecert.commands.client(io.StringIO(), io.StringIO(), tmpdir.strpath, entity_name, custom_csr_path)
 

	
 
    return tmpdir
tests/test_commands.py
Show inline comments
 
@@ -149,25 +149,90 @@ def test_server_reports_error_if_directory_is_not_initialised(tmpdir):
 
    assert status_code == gimmecert.commands.ExitCode.ERROR_NOT_INITIALISED
 

	
 

	
 
def test_server_reports_success_and_paths_to_generated_artifacts(tmpdir):
 
    depth = 1
 
@pytest.mark.parametrize(
 
    "entity_name, update_dns_names, custom_csr_path, strings_expected_in_output, strings_not_expected_in_output",
 
    [
 
        # New server certificate, no DNS update, generate private key.
 
        ("myserver", False, None,
 
         [".gimmecert/server/myserver.key.pem", ".gimmecert/server/myserver.cert.pem"],
 
         [".gimmecert/server/myserver.csr.pem"]),
 

	
 
        # New server certificate, no DNS update, use custom CSR.
 
        ("myserver", False, "custom_csr/mycustom.csr.pem",
 
         [".gimmecert/server/myserver.cert.pem", ".gimmecert/server/myserver.csr.pem"],
 
         [".gimmecert/server/myserver.key.pem"]),
 

	
 
        # New server certificate, DNS update, generate private key (since no previous key exists).
 
        ("myserver", True, None,
 
         [".gimmecert/server/myserver.key.pem", ".gimmecert/server/myserver.cert.pem"],
 
         ["renewed with new DNS subject alternative names", "key has remained unchanged",
 
          ".gimmecert/server/myserver.csr.pem"]),
 

	
 
        # New server certificate, DNS update, reuse existing private key.
 
        ("server-with-privkey-1", True, None,
 
         ["renewed with new DNS subject alternative names", "key has remained unchanged",
 
          ".gimmecert/server/server-with-privkey-1.key.pem", ".gimmecert/server/server-with-privkey-1.cert.pem"],
 
         [".gimmecert/server/server-with-privkey-1.csr.pem"]),
 

	
 
        # New server certificate, DNS update, reuse existing CSR.
 
        ("server-with-csr-1", True, None,
 
         ["renewed with new DNS subject alternative names", "CSR has remained unchanged",
 
          ".gimmecert/server/server-with-csr-1.cert.pem", ".gimmecert/server/server-with-csr-1.csr.pem"],
 
         [".gimmecert/server/server-with-csr-1.key.pem"]),
 

	
 
        # New server certificate, DNS update, replace existing private key with CSR. @TODO: This should really error-out.
 
        ("server-with-privkey-1", True, "custom_csr/mycustom.csr.pem",
 
         ["certificate renewed", "private key has remained unchanged",
 
          ".gimmecert/server/server-with-privkey-1.key.pem", ".gimmecert/server/server-with-privkey-1.cert.pem"],
 
         [".gimmecert/server/server-with-privkey-1.csr.pem"]),
 
    ]
 
)
 
def test_server_reports_success_and_outputs_correct_information(sample_project_directory, key_with_csr,
 
                                                                entity_name, update_dns_names, custom_csr_path,
 
                                                                strings_expected_in_output, strings_not_expected_in_output):
 
    """
 
    Tests if the server command reports success and outputs correct
 
    information for user.
 

	
 
    Tests is parametrised in order to avoid code
 
    duplication. Unfortunately, the parameters are fairly complex, but
 
    it still beats code duplication.
 

	
 
    Tests has been designed to cater to different permutations of the
 
    following conditions/parameters:
 

	
 
        - Existence of certificate under the same name (mainly for
 
          testing updates to DNS names).
 
        - Request to update DNS names.
 
        - Request to use a custom CSR for issuing the certificate.
 

	
 
    For each variation we have lines we expect in standard output, and
 
    lines which we expect _not_ to be in standard output. E.g. if
 
    private key was generated by Gimmecert, we don't expect to see
 
    information about the CSR and vice-versa.
 
    """
 

	
 
    sample_project_directory.chdir()
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 

	
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, depth)
 

	
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream, tmpdir.strpath, 'myserver', None, False, None)
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream,
 
                                            sample_project_directory.strpath, entity_name, None,
 
                                            update_dns_names, custom_csr_path)
 

	
 
    stdout = stdout_stream.getvalue()
 
    stderr = stderr_stream.getvalue()
 

	
 
    assert status_code == gimmecert.commands.ExitCode.SUCCESS
 
    assert ".gimmecert/server/myserver.key.pem" in stdout
 
    assert ".gimmecert/server/myserver.cert.pem" in stdout
 
    assert ".gimmecert/server/myserver.csr.pem" not in stdout
 
    assert stderr == ""
 

	
 
    for expected in strings_expected_in_output:
 
        assert expected in stdout
 

	
 
    for not_expected in strings_not_expected_in_output:
 
        assert not_expected not in stdout
 

	
 

	
 
def test_server_outputs_private_key_to_file_without_csr(tmpdir):
 
    depth = 1
 
@@ -428,53 +493,42 @@ def test_client_errors_out_if_certificate_already_issued(tmpdir):
 
    assert tmpdir.join('.gimmecert', 'client', 'myclient.cert.pem').read() == certificate
 

	
 

	
 
def test_server_reports_success_if_certificate_already_issued_but_update_was_requested(tmpdir):
 
    depth = 1
 
def test_server_update_dns_does_not_generate_new_private_key(sample_project_directory):
 
    entity_name = "server-with-privkey-1"
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 
    private_key_file = sample_project_directory.join('.gimmecert', 'server', '%s.key.pem' % entity_name)
 
    certificate_file = sample_project_directory.join('.gimmecert', 'server', '%s.cert.pem' % entity_name)
 

	
 
    # Previous run.
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, depth)
 
    gimmecert.commands.server(io.StringIO(), io.StringIO(), tmpdir.strpath, 'myserver', None, False, None)
 
    existing_private_key = tmpdir.join('.gimmecert', 'server', 'myserver.key.pem').read()
 
    certificate = tmpdir.join('.gimmecert', 'server', 'myserver.cert.pem').read()
 
    old_private_key = private_key_file.read()
 
    old_certificate = certificate_file.read()
 

	
 
    # New run.
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream, tmpdir.strpath, 'myserver', None, True, None)
 
    status_code = gimmecert.commands.server(io.StringIO(), io.StringIO(), sample_project_directory.strpath, entity_name, None, True, None)
 

	
 
    stdout = stdout_stream.getvalue()
 
    stderr = stderr_stream.getvalue()
 
    new_private_key = private_key_file.read()
 
    new_certificate = certificate_file.read()
 

	
 
    assert status_code == gimmecert.commands.ExitCode.SUCCESS
 
    assert ".gimmecert/server/myserver.key.pem" in stdout
 
    assert ".gimmecert/server/myserver.cert.pem" in stdout
 
    assert ".gimmecert/server/myserver.csr.pem" not in stdout
 
    assert "renewed with new DNS subject alternative names" in stdout
 
    assert "key has remained unchanged" in stdout
 
    assert stderr == ""
 
    assert tmpdir.join('.gimmecert', 'server', 'myserver.key.pem').read() == existing_private_key
 
    assert tmpdir.join('.gimmecert', 'server', 'myserver.cert.pem').read() != certificate
 
    assert new_private_key == old_private_key
 
    assert new_certificate != old_certificate
 

	
 

	
 
def test_server_reports_success_if_certificate_not_already_issued_but_update_was_requested(tmpdir):
 
    depth = 1
 
def test_server_update_dns_does_not_generate_new_csr(sample_project_directory):
 
    entity_name = "server-with-csr-1"
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 
    csr_file = sample_project_directory.join('.gimmecert', 'server', '%s.csr.pem' % entity_name)
 
    certificate_file = sample_project_directory.join('.gimmecert', 'server', '%s.cert.pem' % entity_name)
 

	
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, depth)
 
    old_csr = csr_file.read()
 
    old_certificate = certificate_file.read()
 

	
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream, tmpdir.strpath, 'myserver', None, True, None)
 
    status_code = gimmecert.commands.server(io.StringIO(), io.StringIO(), sample_project_directory.strpath, entity_name, None, True, None)
 

	
 
    stdout = stdout_stream.getvalue()
 
    stderr = stderr_stream.getvalue()
 
    new_csr = csr_file.read()
 
    new_certificate = certificate_file.read()
 

	
 
    assert status_code == gimmecert.commands.ExitCode.SUCCESS
 
    assert ".gimmecert/server/myserver.key.pem" in stdout
 
    assert ".gimmecert/server/myserver.cert.pem" in stdout
 
    assert ".gimmecert/server/myserver.csr.pem" not in stdout
 
    assert stderr == ""
 
    assert new_csr == old_csr
 
    assert new_certificate != old_certificate
 

	
 

	
 
def test_renew_returns_status_code(tmpdir):
 
@@ -1026,31 +1080,6 @@ def test_client_uses_correct_public_key_but_no_naming_with_csr(tmpdir):
 
    assert csr.subject != certificate.subject
 

	
 

	
 
def test_server_reports_success_and_paths_to_generated_artifacts_with_csr(tmpdir):
 
    depth = 1
 
    custom_csr_file = tmpdir.join('mycustom.csr.pem')
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 

	
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, depth)
 

	
 
    private_key = gimmecert.crypto.generate_private_key()
 
    custom_csr = gimmecert.crypto.generate_csr('blah', private_key)
 
    gimmecert.storage.write_csr(custom_csr, custom_csr_file.strpath)
 

	
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream, tmpdir.strpath, 'myserver', None, False, custom_csr_file.strpath)
 

	
 
    stdout = stdout_stream.getvalue()
 
    stderr = stderr_stream.getvalue()
 

	
 
    assert status_code == gimmecert.commands.ExitCode.SUCCESS
 
    assert ".gimmecert/server/myserver.csr.pem" in stdout
 
    assert ".gimmecert/server/myserver.cert.pem" in stdout
 
    assert ".gimmecert/server/myserver.key.pem" not in stdout
 
    assert stderr == ""
 

	
 

	
 
def test_server_outputs_passed_in_csr_to_file_without_private_key(tmpdir):
 
    depth = 1
 

	
 
@@ -1098,41 +1127,6 @@ def test_server_uses_correct_public_key_but_no_naming_with_csr(tmpdir):
 
    assert csr.subject != certificate.subject
 

	
 

	
 
def test_server_reports_success_if_certificate_already_issued_but_update_was_requested_with_csr(tmpdir):
 
    depth = 1
 

	
 
    custom_csr_file = tmpdir.join('customcsr.pem')
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 

	
 
    private_key = gimmecert.crypto.generate_private_key()
 
    csr = gimmecert.crypto.generate_csr('mycustomcsr', private_key)
 
    gimmecert.storage.write_csr(csr, custom_csr_file.strpath)
 

	
 
    # Previous run.
 
    gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, depth)
 
    gimmecert.commands.server(io.StringIO(), io.StringIO(), tmpdir.strpath, 'myserver', None, False, custom_csr_file.strpath)
 
    existing_csr = tmpdir.join('.gimmecert', 'server', 'myserver.csr.pem').read()
 
    certificate = tmpdir.join('.gimmecert', 'server', 'myserver.cert.pem').read()
 

	
 
    # New run.
 
    status_code = gimmecert.commands.server(stdout_stream, stderr_stream, tmpdir.strpath, 'myserver', None, True, None)
 

	
 
    stdout = stdout_stream.getvalue()
 
    stderr = stderr_stream.getvalue()
 

	
 
    assert status_code == gimmecert.commands.ExitCode.SUCCESS
 
    assert ".gimmecert/server/myserver.csr.pem" in stdout
 
    assert ".gimmecert/server/myserver.cert.pem" in stdout
 
    assert ".gimmecert/server/myserver.key.pem" not in stdout
 
    assert "renewed with new DNS subject alternative names" in stdout
 
    assert "CSR has remained unchanged" in stdout
 
    assert stderr == ""
 
    assert tmpdir.join('.gimmecert', 'server', 'myserver.csr.pem').read() == existing_csr
 
    assert tmpdir.join('.gimmecert', 'server', 'myserver.cert.pem').read() != certificate
 

	
 

	
 
def test_server_reports_success_if_certificate_not_already_issued_but_update_was_requested_with_csr(tmpdir):
 
    depth = 1
 

	
0 comments (0 inline, 0 general)