From 88f7fc602f97d999b3297e001a328abd22bbe9ec Mon Sep 17 00:00:00 2001 From: Christopher Eck Date: Mon, 13 Feb 2017 21:42:57 -0800 Subject: [PATCH] Code cleanup in preparation for next release * Migrated from nose to pytest * Enabled pylint via `setup.py test` * Cleaned up pylint complaints --- .gitignore | 2 + circle.yml | 13 +++ setup.cfg | 11 +++ setup.py | 11 ++- test/ca_test.py | 153 ++++++++++++++++++--------------- test/cert_test.py | 199 +++++++++++++++++++++++-------------------- test/session_test.py | 125 +++++++++++++-------------- tinycert/cert.py | 1 + tinycert/session.py | 35 +++++--- 9 files changed, 310 insertions(+), 240 deletions(-) create mode 100644 circle.yml mode change 100644 => 100755 setup.py diff --git a/.gitignore b/.gitignore index 46747bf..7362b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +.cache/ +.coverage build dist *.egg diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..59c96d4 --- /dev/null +++ b/circle.yml @@ -0,0 +1,13 @@ +machine: + post: + - pyenv global 2.7.11 3.4.4 + +dependencies: + pre: + - python2.7 setup.py install + - python3.4 setup.py install + +test: + override: + - python2.7 setup.py test + - python3.4 setup.py test diff --git a/setup.cfg b/setup.cfg index 5e40900..66002b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,13 @@ [wheel] universal = 1 + +[aliases] +test = pytest + +[tool:pytest] +addopts = --pylint --pylint-rcfile=setup.cfg + +[pylint] +max-line-length = 120 +known-standard-library = future +disable = redefined-builtin,duplicate-code diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 2bf8e48..91bfafc --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """ TinyCert -------- @@ -48,10 +49,15 @@ 'requests>=2.9.1' ] +SETUP_DEPS = [ + 'pytest-runner', + 'pytest-pylint', +] + TEST_DEPS = [ 'mock>=1.3.0', - 'nose>=1.3.7', 'pylint>=1.5.5', + 'pytest>=3.0.6', 'requests-mock>=0.7.0' ] @@ -63,13 +69,12 @@ author='Christopher Eck', author_email='chrisleck@gmail.com', url='https://github.com/chrisleck/tinycert', - bugtrack_url='https://github.com/chrisleck/tinycert/issues', license='MIT', long_description=__doc__, packages=['tinycert'], install_requires=INSTALL_DEPS, + setup_requires=SETUP_DEPS, tests_require=TEST_DEPS, - test_suite='nose.collector', platforms='any', zip_safe=True, classifiers=[ diff --git a/test/ca_test.py b/test/ca_test.py index 08fdba5..fa39804 100644 --- a/test/ca_test.py +++ b/test/ca_test.py @@ -1,79 +1,90 @@ """Unit tests for the ca module.""" +# pylint: disable=missing-docstring + from __future__ import unicode_literals -import unittest import mock +import pytest from tinycert.ca import CertificateAuthorityApi -class CertificateAuthorityApiTest(unittest.TestCase): - """Unit tests for the CertificateAuthorityApi class.""" - def setUp(self): - self.session = mock.MagicMock() - self.session.request = mock.MagicMock() - self.api = CertificateAuthorityApi(self.session) - - def list_test(self): - expected_list = [ - {'id': 123, 'name': 'test ca'}, - {'id': 456, 'name': 'another test ca'} - ] - self.session.request.return_value = expected_list - - result = self.api.list() - self.assertEqual(result, expected_list) - self.session.request.assert_called_with('ca/list') - - def details_test(self): - expected_result = { - 'id': 123, - 'C': 'US', - 'ST': 'Washington', - 'L': 'Seattle', - 'O': 'Acme, Inc.', - 'OU': 'Secure Digital Certificate Signing', - 'CN': 'Acme, Inc. CA', - 'E': 'admin@acme.com', - 'hash_alg': 'SHA256' - } - self.session.request.return_value = expected_result - - result = self.api.details(123) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('ca/details', {'ca_id': 123}) - - def get_test(self): - expected_result = { - 'pem': ('-----BEGIN CERTIFICATE-----' - 'ABUNCHOFSTUFFHERE...' - '-----END CERTIFICATE-----') - } - self.session.request.return_value = expected_result - - result = self.api.get(123) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('ca/get', {'ca_id': 123, 'what': 'cert'}) - - def delete_test(self): - self.session.request.return_value = {} - result = self.api.delete(123) - self.assertEqual(result, {}) - self.session.request.assert_called_with('ca/delete', {'ca_id': 123}) - - def create_test(self): - expected_result = { - 'ca_id': 123 - } - self.session.request.return_value = expected_result - - create_detail = { - 'C': 'US', - 'O': 'Acme, Inc.', - 'L': 'Seattle', - 'ST': 'Washington', - 'hash_method': 'sha256' - } - result = self.api.create(create_detail) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('ca/new', create_detail) +@pytest.fixture(name='session') +def fixture_session(): + fixture = mock.MagicMock() + fixture.request = mock.MagicMock() + return fixture + + +@pytest.fixture(name='api') +def fixture_api(session): + return CertificateAuthorityApi(session) + + +def list_test(session, api): + expected_list = [ + {'id': 123, 'name': 'test ca'}, + {'id': 456, 'name': 'another test ca'} + ] + session.request.return_value = expected_list + + result = api.list() + assert result == expected_list + session.request.assert_called_with('ca/list') + + +def details_test(session, api): + expected_result = { + 'id': 123, + 'C': 'US', + 'ST': 'Washington', + 'L': 'Seattle', + 'O': 'Acme, Inc.', + 'OU': 'Secure Digital Certificate Signing', + 'CN': 'Acme, Inc. CA', + 'E': 'admin@acme.com', + 'hash_alg': 'SHA256' + } + session.request.return_value = expected_result + + result = api.details(123) + assert result == expected_result + session.request.assert_called_with('ca/details', {'ca_id': 123}) + + +def get_test(session, api): + expected_result = { + 'pem': ('-----BEGIN CERTIFICATE-----' + 'ABUNCHOFSTUFFHERE...' + '-----END CERTIFICATE-----') + } + session.request.return_value = expected_result + + result = api.get(123) + assert result == expected_result + session.request.assert_called_with('ca/get', {'ca_id': 123, 'what': 'cert'}) + + +def delete_test(session, api): + session.request.return_value = {} + result = api.delete(123) + assert result == {} + session.request.assert_called_with('ca/delete', {'ca_id': 123}) + + +def create_test(session, api): + expected_result = { + 'ca_id': 123 + } + session.request.return_value = expected_result + + create_detail = { + 'C': 'US', + 'O': 'Acme, Inc.', + 'L': 'Seattle', + 'ST': 'Washington', + 'hash_method': 'sha256' + } + result = api.create(create_detail) + assert result == expected_result + session.request.assert_called_with('ca/new', create_detail) diff --git a/test/cert_test.py b/test/cert_test.py index a82a2b9..35bf4b4 100644 --- a/test/cert_test.py +++ b/test/cert_test.py @@ -1,103 +1,114 @@ """Unit tests for the cert module.""" -from __future__ import unicode_literals +# pylint: disable=missing-docstring -import unittest +from __future__ import unicode_literals import mock +import pytest from tinycert.cert import CertificateApi from tinycert.cert import State -class CertificateApiTest(unittest.TestCase): - """Unit tests for the CertificateApi class.""" - def setUp(self): - self.session = mock.MagicMock() - self.session.request = mock.MagicMock() - self.api = CertificateApi(self.session) - - def list_test(self): - expected_list = [ - {'id': 123, 'name': 'test cert', 'status': 'good', 'expires': 987654321}, - {'id': 456, 'name': 'another test cert', 'status': 'revoked', 'expires': 987654322} - ] - self.session.request.return_value = expected_list - - result = self.api.list(555, State.good.value | State.revoked.value) - self.assertEqual(result, expected_list) - self.session.request.assert_called_with('cert/list', {'ca_id': 555, 'what': 6}) - - def details_test(self): - expected_result = { - 'id': 123, - 'status': 'good', - 'C': 'US', - 'ST': 'Washington', - 'L': 'Seattle', - 'O': 'Acme, Inc.', - 'OU': 'IT Department', - 'CN': 'Acme, Inc. CA', - 'Alt': [ - {'DNS': 'www.example.com'}, - {'DNS': 'example.com'} - ], - 'hash_alg': 'SHA256' - } - self.session.request.return_value = expected_result - - result = self.api.details(123) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('cert/details', {'cert_id': 123}) - - def get_test(self): - expected_result = { - 'pem': ('-----BEGIN RSA PRIVATE KEY-----' - 'KEYMATERIALHERE...' - '-----END RSA PRIVATE KEY-----') - } - self.session.request.return_value = expected_result - - result = self.api.get(123, 'key.dec') - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('cert/get', {'cert_id': 123, 'what': 'key.dec'}) - - def reissue_test(self): - expected_result = { - 'cert_id': 456 - } - self.session.request.return_value = expected_result - - result = self.api.reissue(123) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('cert/reissue', {'cert_id': 123}) - - def set_status_test(self): - self.session.request.return_value = {} - result = self.api.set_status(123, 'hold') - self.assertEqual(result, {}) - self.session.request.assert_called_with('cert/status', {'cert_id': 123, 'status': 'hold'}) - - def create_test(self): - expected_result = { - 'cert_id': 456 - } - self.session.request.return_value = expected_result - - create_detail = { - 'C': 'US', - 'CN': '*.example.com', - 'L': 'Seattle', - 'O': 'Acme, Inc.', - 'OU': 'IT Department', - 'SANs': [ - {'DNS': 'www.example.com'}, - {'DNS': 'example.com'} - ], - 'ST': 'Washington' - } - expected_detail = create_detail.copy() - expected_detail['ca_id'] = 123 - - result = self.api.create(123, create_detail) - self.assertEqual(result, expected_result) - self.session.request.assert_called_with('cert/new', expected_detail) +@pytest.fixture(name='session') +def fixture_session(): + fixture = mock.MagicMock() + fixture.request = mock.MagicMock() + return fixture + + +@pytest.fixture(name='api') +def fixture_api(session): + return CertificateApi(session) + + +def list_test(session, api): + expected_list = [ + {'id': 123, 'name': 'test cert', 'status': 'good', 'expires': 987654321}, + {'id': 456, 'name': 'another test cert', 'status': 'revoked', 'expires': 987654322} + ] + session.request.return_value = expected_list + + result = api.list(555, State.good.value | State.revoked.value) + assert result == expected_list + session.request.assert_called_with('cert/list', {'ca_id': 555, 'what': 6}) + + +def details_test(session, api): + expected_result = { + 'id': 123, + 'status': 'good', + 'C': 'US', + 'ST': 'Washington', + 'L': 'Seattle', + 'O': 'Acme, Inc.', + 'OU': 'IT Department', + 'CN': 'Acme, Inc. CA', + 'Alt': [ + {'DNS': 'www.example.com'}, + {'DNS': 'example.com'} + ], + 'hash_alg': 'SHA256' + } + session.request.return_value = expected_result + + result = api.details(123) + assert result == expected_result + session.request.assert_called_with('cert/details', {'cert_id': 123}) + + +def get_test(session, api): + expected_result = { + 'pem': ('-----BEGIN RSA PRIVATE KEY-----' + 'KEYMATERIALHERE...' + '-----END RSA PRIVATE KEY-----') + } + session.request.return_value = expected_result + + result = api.get(123, 'key.dec') + assert result == expected_result + session.request.assert_called_with('cert/get', {'cert_id': 123, 'what': 'key.dec'}) + + +def reissue_test(session, api): + expected_result = { + 'cert_id': 456 + } + session.request.return_value = expected_result + + result = api.reissue(123) + assert result == expected_result + session.request.assert_called_with('cert/reissue', {'cert_id': 123}) + + +def set_status_test(session, api): + session.request.return_value = {} + result = api.set_status(123, 'hold') + assert result == {} + session.request.assert_called_with('cert/status', {'cert_id': 123, 'status': 'hold'}) + + +def create_test(session, api): + expected_result = { + 'cert_id': 456 + } + session.request.return_value = expected_result + + create_detail = { + 'C': 'US', + 'CN': '*.example.com', + 'L': 'Seattle', + 'O': 'Acme, Inc.', + 'OU': 'IT Department', + 'SANs': [ + {'DNS': 'www.example.com'}, + {'DNS': 'example.com'} + ], + 'ST': 'Washington' + } + expected_detail = create_detail.copy() + expected_detail['ca_id'] = 123 + + result = api.create(123, create_detail) + assert result == expected_result + session.request.assert_called_with('cert/new', expected_detail) diff --git a/test/session_test.py b/test/session_test.py index 716f07c..05c53f8 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -1,7 +1,7 @@ """Unit tests for the tiny_cert module.""" -from __future__ import unicode_literals +# pylint: disable=missing-docstring,protected-access -import unittest +from __future__ import unicode_literals import requests_mock @@ -9,46 +9,48 @@ from tinycert.session import auto_session -class SessionTest(unittest.TestCase): - """Unit tests for the tiny_cert module.""" - FAKE_API_KEY = 'somekey' - FAKE_SESSION_TOKEN = 'sometoken' - FAKE_ACCOUNT = 'me@foo.com' - FAKE_PASSPHRASE = 'my passphrase' +FAKE_API_KEY = 'somekey' +FAKE_SESSION_TOKEN = 'sometoken' +FAKE_ACCOUNT = 'me@foo.com' +FAKE_PASSPHRASE = 'my passphrase' - def _setup_mock_requests(self, mock_requests): - expected_connect_body = ('email=me%40foo.com' - '&passphrase=my+passphrase' - '&digest=9b8062b8ab91dd2ff4bb9d24d7be5234659ba94c772a8e056d26388e052b8537') - def json_connect_response(request, context): - if request.body == expected_connect_body: - context.status_code = 200 - return {'token': SessionTest.FAKE_SESSION_TOKEN} - context.status_code = 400 - return {} +def setup_requests_mock(mock): + expected_connect_body = ('email=me%40foo.com' + '&passphrase=my+passphrase' + '&digest=9b8062b8ab91dd2ff4bb9d24d7be5234659ba94c772a8e056d26388e052b8537') - mock_requests.register_uri('POST', - 'https://www.tinycert.org/api/v1/connect', - request_headers={'content-type': 'application/x-www-form-urlencoded'}, - json=json_connect_response) + def json_connect_response(request, context): + if request.body == expected_connect_body: + context.status_code = 200 + return {'token': FAKE_SESSION_TOKEN} + context.status_code = 400 + return {} - expected_disconnect_body = ('token=sometoken' - '&digest=a83d65e81eb4e6cae1b0fc95c26f6ac838e278f22b0a94d8a42c4a193a58420d') + mock.register_uri('POST', + 'https://www.tinycert.org/api/v1/connect', + request_headers={'content-type': 'application/x-www-form-urlencoded'}, + json=json_connect_response) - def json_disconnect_response(request, context): - if request.body == expected_disconnect_body: - context.status_code = 200 - return {} - context.status_code = 400 + expected_disconnect_body = ('token=sometoken' + '&digest=a83d65e81eb4e6cae1b0fc95c26f6ac838e278f22b0a94d8a42c4a193a58420d') + + def json_disconnect_response(request, context): + if request.body == expected_disconnect_body: + context.status_code = 200 return {} + context.status_code = 400 + return {} + + mock.register_uri('POST', + 'https://www.tinycert.org/api/v1/disconnect', + request_headers={'content-type': 'application/x-www-form-urlencoded'}, + json=json_disconnect_response) - mock_requests.register_uri('POST', - 'https://www.tinycert.org/api/v1/disconnect', - request_headers={'content-type': 'application/x-www-form-urlencoded'}, - json=json_disconnect_response) - def testSigningRequestPayload(self): +def test_signing_request_payload(): + with requests_mock.Mocker() as mock: + setup_requests_mock(mock) session = Session('ThisIsMySuperSecretAPIKey') params = { @@ -79,35 +81,34 @@ def testSigningRequestPayload(self): '&digest=16b436bd8779dadf0327a97eac54b631e02c4643cbf52ccc1358431691f74b21') signed_payload = session._sign_request_payload(params) - self.assertEquals(signed_payload, expected_payload) - - @requests_mock.Mocker() - def testConnect(self, mock_requests): - self._setup_mock_requests(mock_requests) - session = Session(SessionTest.FAKE_API_KEY) - session.connect(SessionTest.FAKE_ACCOUNT, SessionTest.FAKE_PASSPHRASE) - self.assertEquals(session._session_token, SessionTest.FAKE_SESSION_TOKEN) - self.assertIsNotNone(session.ca) - self.assertIsNotNone(session.cert) - - @requests_mock.Mocker() - def testDisconnect(self, mock_requests): - self._setup_mock_requests(mock_requests) - session = Session(SessionTest.FAKE_API_KEY, SessionTest.FAKE_SESSION_TOKEN) - self.assertEquals(session._session_token, SessionTest.FAKE_SESSION_TOKEN) - session.disconnect() - self.assertIsNone(session._session_token) + assert signed_payload == expected_payload + + +def test_connect(): + with requests_mock.Mocker() as mock: + setup_requests_mock(mock) + session = Session(FAKE_API_KEY) + session.connect(FAKE_ACCOUNT, FAKE_PASSPHRASE) + assert session._session_token == FAKE_SESSION_TOKEN + assert session.ca + assert session.cert - @requests_mock.Mocker() - def testContextManager(self, mock_requests): - self._setup_mock_requests(mock_requests) - with auto_session(SessionTest.FAKE_API_KEY, SessionTest.FAKE_ACCOUNT, SessionTest.FAKE_PASSPHRASE) as session: - self.assertEquals(session._session_token, SessionTest.FAKE_SESSION_TOKEN) - self.assertIsNotNone(session.ca) - self.assertIsNotNone(session.cert) - self.assertIsNone(session._session_token) +def test_disconnect(): + with requests_mock.Mocker() as mock: + setup_requests_mock(mock) + session = Session(FAKE_API_KEY, FAKE_SESSION_TOKEN) + assert session._session_token == FAKE_SESSION_TOKEN + session.disconnect() + assert session._session_token is None + +def test_context_manager(): + with requests_mock.Mocker() as mock: + setup_requests_mock(mock) + with auto_session(FAKE_API_KEY, FAKE_ACCOUNT, FAKE_PASSPHRASE) as session: + assert session._session_token == FAKE_SESSION_TOKEN + assert session.ca + assert session.cert -if __name__ == '__main__': - unittest.main() + assert session._session_token is None diff --git a/tinycert/cert.py b/tinycert/cert.py index 99a5c7b..67e8449 100644 --- a/tinycert/cert.py +++ b/tinycert/cert.py @@ -5,6 +5,7 @@ from enum import Enum +# pylint: disable=too-few-public-methods class State(Enum): """Certificate state enum used for list filtering.""" expired = 1 diff --git a/tinycert/session.py b/tinycert/session.py index 4eb2eb9..dddab17 100644 --- a/tinycert/session.py +++ b/tinycert/session.py @@ -2,17 +2,25 @@ This module provides the Session class which wraps an authenticated session with TinyCert's rest API. """ -from __future__ import unicode_literals +# pylint: disable=wrong-import-order,wrong-import-position + +from __future__ import unicode_literals from future import standard_library standard_library.install_aliases() + from past.builtins import basestring from builtins import object import collections from contextlib import contextmanager import hashlib import hmac -import urllib.request, urllib.parse, urllib.error + +# pylint: disable=import-error +import urllib.error +import urllib.parse +import urllib.request +# pylint: enable=import-error import requests @@ -23,6 +31,7 @@ class NoSessionException(Exception): """Raised if an attempt is made to use a session without first calling connect()""" def __init__(self): + Exception.__init__(self) self.value = 'Must connect the session first' def __str__(self): @@ -31,6 +40,7 @@ def __str__(self): @contextmanager def auto_session(api_key, account, passphrase): + """Context manager for holding a connected session object within scope.""" session = Session(api_key) session.connect(account, passphrase) try: @@ -55,7 +65,11 @@ def __init__(self, api_key, session_token=None): self._api_key = api_key self._session_token = session_token - def request(self, path, params={}): + def request(self, path, params=None): + """Make a request via the TinyCert API.""" + if params is None: + params = {} + if self._session_token: params['token'] = self._session_token @@ -71,16 +85,16 @@ def request(self, path, params={}): def _flatten_array_elements(params): """Flattens arrays into numerically indexed entries, further flattening if the array elements are dicts.""" flattened_params = {} - for k, v in params.items(): - if not isinstance(v, (list, tuple)): - flattened_params[k] = v + for key, value in params.items(): + if not isinstance(value, (list, tuple)): + flattened_params[key] = value continue - for index, entry in enumerate(v): + for index, entry in enumerate(value): if isinstance(entry, basestring): - flattened_params['%s[%i]' % (k, index)] = entry + flattened_params['%s[%i]' % (key, index)] = entry else: - for dk, dv in entry.items(): - flattened_params['%s[%i][%s]' % (k, index, dk)] = dv + for dict_key, dict_value in entry.items(): + flattened_params['%s[%i][%s]' % (key, index, dict_key)] = dict_value return flattened_params def _sign_request_payload(self, params): @@ -121,6 +135,7 @@ def disconnect(self): self.request('disconnect') self._session_token = None + # pylint: disable=invalid-name @property def ca(self): """Retrieve the CertificateAuthority API wrapper."""