diff --git a/pytest.ini b/pytest.ini index 5ee3d35dc215f1e2343609d892d7e574504d7e98..abdd410e5f9e9835a3233b22687ad49cc1109235 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,3 @@ [pytest] -testpaths = unittests \ No newline at end of file +testpaths = unittests +addopts = -vv --cov=caosdb diff --git a/setup.cfg b/setup.cfg index 208d7f783324f2159b24acb8b925f3cb12b9cbc5..74c5620b86c6fd5ee96abea95dab4010c9fb87a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,2 @@ -[nosetests] -verbosity=2 -detailed-errors=1 -with-coverage=1 -cover-package=caosdb -cover-inclusive=1 -cover-branches=1 -cover-erase=1 -#with-doctest=1 - [pycodestyle] ignore=E501,E121,E123,E126,E226,E24,E704,W503,W504 diff --git a/setup.py b/setup.py index b9d76430c84d3625c8445c503d45d75966948cc9..f80c619591a55190d70ee105b0cff2e3d79894c1 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/env python # -*- encoding: utf-8 -*- # # ** header v3.0 @@ -35,5 +35,5 @@ setup(name='PyCaosDB', install_requires=['lxml>=3.6.4', 'coverage>=4.4.2', 'PyYaml>=3.12', 'future'], extras_require={'keyring': ['keyring>=13.0.0']}, - tests_require=["nose>=1.0"], + tests_require=["pytest"], ) diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py index 5052cc1bea1845ad68984514980f4b2c243e52e9..8c32b94877d38b52caeed722c4b9821033f82cdf 100644 --- a/src/caosdb/common/models.py +++ b/src/caosdb/common/models.py @@ -1923,7 +1923,7 @@ class Container(list): inserted/updated/deleted/retrieved at once.""" list.__init__(self) self._timestamp = None - self._session = None + self._srid = None self.messages = _Messages() def extend(self, entities): @@ -2115,7 +2115,7 @@ class Container(list): # ignore pass c._timestamp = xml.get("timestamp") - c._session = xml.get("srid") + c._srid = xml.get("srid") return c else: raise CaosDBException( @@ -2160,7 +2160,7 @@ class Container(list): self.add_message(m) self._timestamp = container._timestamp - self._session = container._session + self._srid = container._srid def _calc_sync_dict(self, remote_container, unique, raise_exception_on_error, name_case_sensitive): diff --git a/src/caosdb/connection/authentication/interface.py b/src/caosdb/connection/authentication/interface.py index d3512174c405f0d86bc4edc11ca10962936f3093..975c43a4b8070944ec4a33fb2993a27b3047f3cb 100644 --- a/src/caosdb/connection/authentication/interface.py +++ b/src/caosdb/connection/authentication/interface.py @@ -31,7 +31,7 @@ except ImportError: from urllib import quote from caosdb.connection.utils import urlencode from caosdb.connection.interface import CaosDBServerConnection -from caosdb.connection.utils import parse_session_token +from caosdb.connection.utils import parse_auth_token from caosdb.exceptions import LoginFailedException # meta class compatible with Python 2 *and* 3: @@ -53,10 +53,15 @@ class AbstractAuthenticator(ABC): configure on_request on_response + + Attributes + ---------- + auth_token : str + A string representation of a CaosDB Auth Token. """ def __init__(self): - self._session_token = None + self.auth_token = None @abstractmethod def login(self): @@ -113,7 +118,7 @@ class AbstractAuthenticator(ABC): Returns ------- """ - self._session_token = parse_session_token( + self.auth_token = parse_auth_token( response.getheader("Set-Cookie")) def on_request(self, method, path, headers, **kwargs): @@ -137,8 +142,8 @@ class AbstractAuthenticator(ABC): Returns ------- """ - if self._session_token is not None: - headers['Cookie'] = self._session_token + if self.auth_token is not None: + headers['Cookie'] = self.auth_token class CredentialsAuthenticator(AbstractAuthenticator): @@ -165,7 +170,7 @@ class CredentialsAuthenticator(AbstractAuthenticator): super(CredentialsAuthenticator, self).__init__() self._credentials_provider = credentials_provider self._connection = None - self._session_token = None + self._auth_token = None def login(self): self._login() @@ -175,9 +180,9 @@ class CredentialsAuthenticator(AbstractAuthenticator): def _logout(self): _LOGGER.debug("[LOGOUT]") - if self._session_token is not None: + if self._auth_token is not None: self._connection.request(method="DELETE", path="logout") - self._session_token = None + self._auth_token = None def _login(self): username = self._credentials_provider.username diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py index faa66297c027bcddea9fb8ae5a1939e12c7678d5..9c35e7cf5997854dfff97e7de8822461e5708d89 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -37,7 +37,8 @@ import logging from caosdb.exceptions import (CaosDBException, LoginFailedException, AuthorizationException, URITooLongException, ConnectionException, ServerErrorException, - EntityDoesNotExistError, ClientErrorException) + EntityDoesNotExistError, ClientErrorException, + ConfigurationException) from caosdb.configuration import get_config from .utils import urlencode, make_uri_path, parse_url from .interface import CaosDBServerConnection, CaosDBHTTPResponse @@ -256,12 +257,34 @@ class _Connection(object): # pylint: disable=useless-object-inheritance cls.__instance = _Connection() return cls.__instance - def configure(self, **kwargs): + def configure(self, **config): self.is_configured = True - self._delegate_connection = kwargs.get("implementation")() - self._delegate_connection.configure(**kwargs) + if "implementation" not in config: + raise ConfigurationException( + "Missing CaosDBServerConnection implementation. You did not " + "specify an `implementation` for the connection.") + try: + self._delegate_connection = config["implementation"]() + if not isinstance(self._delegate_connection, + CaosDBServerConnection): + raise TypeError("The `implementation` callable did not return " + "an instance of CaosDBServerConnection.") + except TypeError as type_err: + raise ConfigurationException( + "Bad CaosDBServerConnection implementation. The " + "implementation must be a callable object which returns an " + "instance of `CaosDBServerConnection` (e.g. a constructor " + "or a factory).", type_err) + self._delegate_connection.configure(**config) + if "password_method" not in config: + raise ConfigurationException("Missing password_method. You did " + "not specify a `password_method` for" + "the connection.") self._authenticator = _get_authenticator( - connection=self._delegate_connection, **kwargs) + connection=self._delegate_connection, **config) + if "auth_token" in config: + self._authenticator.auth_token = config["auth_token"] + return self def retrieve(self, entity_uri_segments=None, query_dict=None, **kwargs): diff --git a/src/caosdb/connection/utils.py b/src/caosdb/connection/utils.py index 57581f90eac7c8ac68bdaac3e1cf1d697ed31e6c..dfee2383b724fba91bd0ead9746b0d541a950244 100644 --- a/src/caosdb/connection/utils.py +++ b/src/caosdb/connection/utils.py @@ -164,11 +164,11 @@ def check_python_ssl_version(hexversion): ) -def parse_session_token(cookie): - session_token = None +def parse_auth_token(cookie): + auth_token = None if cookie is not None: try: - session_token = re.compile(r";\s*.*$").split(cookie)[0] + auth_token = re.compile(r";\s*.*$").split(cookie)[0] except IndexError: pass - return session_token + return auth_token diff --git a/src/caosdb/exceptions.py b/src/caosdb/exceptions.py index 581acc2cb4a447c423309f66e2a8ae218cb1a7c0..a5d6e99662c38e6f74e2e974a68ce8152997e271 100644 --- a/src/caosdb/exceptions.py +++ b/src/caosdb/exceptions.py @@ -28,11 +28,38 @@ from lxml import etree class CaosDBException(Exception): """Base class of all CaosDB exceptions.""" - def __init__(self, msg=None): - Exception.__init__(self, msg) + def __init__(self, msg=None, *args): + Exception.__init__(self, msg, *args) self.msg = msg +class ConfigurationException(CaosDBException): + """ConfigurationException. + + Indicates a misconfiguration. + + Parameters + ---------- + msg : str + A descriptin of the misconfiguration. The constructor adds + a few lines with explainingg where to find the configuration. + *args + + Attributes + ---------- + msg : str + A description of the misconfiguration. + """ + + def __init__(self, msg, *args): + super(ConfigurationException, self).__init__(msg + + ConfigurationException._INFO, + *args) + + _INFO = ("\n\nPlease check your ~/.pycaosdb.ini and your $PWD/" + ".pycaosdb.ini. Do at least one of them exist and are they correct?") + + class ClientErrorException(CaosDBException): def __init__(self, msg, status, body): self.status = status diff --git a/tox.ini b/tox.ini index 5a7cee258b4936038d36d59cadd38534bbe7c941..74d8c643c735b0ad287e36aa1841bf8a6f5d328c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,4 +3,6 @@ envlist= py27, py34, py35, py36 skip_missing_interpreters = true [testenv] deps=nose -commands= nosetests {posargs} + pytest + pytest-cov +commands=py.test --cov=caosdb -vv {posargs} diff --git a/unittests/test_connection.py b/unittests/test_connection.py index b25b56214f47aa494607589b6265f7180b1e10ba..3bc6957e2f82e4951c1f0cbccfb20e49efef59bf 100644 --- a/unittests/test_connection.py +++ b/unittests/test_connection.py @@ -26,9 +26,11 @@ from __future__ import unicode_literals, print_function from builtins import bytes, str # pylint: disable=redefined-builtin import re +from pytest import raises from nose.tools import (assert_equal as eq, assert_raises as raiz, assert_true as tru, assert_is_not_none as there, assert_false as falz) +from caosdb.exceptions import ConfigurationException, LoginFailedException from caosdb.connection.utils import urlencode, make_uri_path, quote from caosdb.connection.connection import ( configure_connection, CaosDBServerConnection, @@ -196,19 +198,70 @@ def test_request_basics(): def setup_two_resources(): def r1(**kwargs): if kwargs["method"] == "GET": - return MockUpResponse(status=123, headers={}, body="response r1") + return MockUpResponse(status=200, headers=kwargs["headers"], body="response r1") def r2(**kwargs): if kwargs["path"] == "matching/path/": return MockUpResponse( status=456, headers={"key": "val"}, body="response r2") + def r3(**kwargs): + if kwargs["path"] == "401": + return MockUpResponse( + status=401, headers={}, body="please login") + connection = test_init_connection() - connection.resources.extend([r1, r2]) + connection.resources.extend([r1, r2, r3]) return connection def test_test_request_with_two_responses(): connection = setup_two_resources() - eq(connection.request(method="GET", path="any").status, 123) + eq(connection.request(method="GET", path="any", headers={}).status, 200) eq(connection.request(method="POST", path="matching/path/").status, 456) + + +def test_missing_implementation(): + connection = configure_connection() + with raises(ConfigurationException) as exc_info: + connection.configure() + assert exc_info.value.args[0].startswith( + "Missing CaosDBServerConnection implementation.") + + +def test_bad_implementation_not_callable(): + connection = configure_connection() + with raises(ConfigurationException) as exc_info: + connection.configure(implementation="str") + assert exc_info.value.args[0].startswith( + "Bad CaosDBServerConnection implementation.") + assert exc_info.value.args[1].args[0] == "'str' object is not callable" + + +def test_bad_implementation_wrong_class(): + connection = configure_connection() + with raises(ConfigurationException) as exc_info: + connection.configure(implementation=dict) + assert exc_info.value.args[0].startswith( + "Bad CaosDBServerConnection implementation.") + assert exc_info.value.args[1].args[0] == ( + "The `implementation` callable did not return an instance of " + "CaosDBServerConnection.") + + +def test_missing_auth_method(): + connection = configure_connection() + with raises(ConfigurationException) as exc_info: + connection.configure(implementation=MockUpServerConnection) + assert exc_info.value.args[0].startswith("Missing password_method.") + + +def test_missing_password(): + connection = configure_connection() + connection.configure(implementation=setup_two_resources, + password_method="plain", auth_token="test-auth-token") + assert connection.retrieve(["some"]).headers["Cookie"] == "test-auth-token" + connection.configure(implementation=setup_two_resources, + password_method="plain") + with raises(LoginFailedException): + connection.delete(["401"])