AssertRaises fails even exception is raised when using "http.client" Python

416 views Asked by At

I'm creating an API class for my project that uses the library http.Client.

The issue is with unittesting, When I am trying to raise an error, and test it with assertRaises, the assertRaises fails even with correct Exception being raised.

I have a module 'Foo'.

+---Foo
¦    __init__.py
¦    api.py

inside my __init__.py are just my Exception classes and decorators

import errno
import json
import os
import signal
from dataclasses import dataclass
from functools import wraps
from http.client import HTTPMessage, HTTPException
from typing import Optional


@dataclass
class APIResponse:
    method: str
    code: int
    reason: str
    length: Optional[int]
    headers: HTTPMessage
    message: HTTPMessage


@dataclass
class Token:
    access_token: str
    expires_in: int
    token_type: str


@dataclass
class ClientCredentials:
    client_id: str
    client_secret: str
    audience: str
    grant_type: str = "client_credentials"


class ClientCredentialsEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, ClientCredentials):
            return o.__dict__
        return json.JSONEncoder.default(self, o)


class AuthenticationError(HTTPException):
    pass


def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return wraps(func)(wrapper)

    return decorator

my api.py consist my API class which I'm testing authenticate method to get Access token

import http.client
import json
import logging

from Foo import ClientCredentials, Token, ClientCredentialsEncoder, AuthenticationError, timeout
from settings import config, API_HOST, AUTH_DOMAIN


class API:

    def __init__(self, auth0_domain, client_credentials: ClientCredentials):
        self.auth0_domain: str = auth0_domain
        self.client_credentials = client_credentials

        self._session = http.client.HTTPSConnection(self.auth0_domain)

        self.token: Token = self.authenticate()

    @property
    def session(self):
        return self._session

    @timeout(10, "API Takes too long to respond.")
    def api_request(self, method: str, url: str, body: str, headers: dict):
        logging.debug(f"Senging {method} request to {url} for data: {body}...")

        self.session.request(method=method, url=url, body=body, headers=headers)

        res = self.session.getresponse()

        return res

    def authenticate(self) -> Token:
        logging.debug("Getting API authentiation...")
        method = "POST"
        url = "/oauth/token"
        body = json.dumps(
            obj=self.client_credentials,
            cls=ClientCredentialsEncoder
        )
        headers = {'content-type': "application/json"}

        response = self.api_request(method=method, url=url, body=body, headers=headers)
        status = response.status
        reason = response.reason

        data = response.read()

        json_dict = json.loads(data)
        self._session.close()

        if status == 403:
            logging.error(f"Error {status}: {reason}. Authentication fail.")
            raise AuthenticationError(
                f"Invalid Credentials. Error: '{json_dict['error']}', Description: {json_dict['error_description']}"
            )

        if status == 401:
            logging.error(f"Error {status}: {reason}. Authentication fail.")
            raise AuthenticationError(
                f"Invalid Credentials. Error: '{json_dict['error']}', Description: {json_dict['error_description']}"
            )

        logging.info(f"Response {status}. Authentication successful!")

        return Token(
            access_token=json_dict['access_token'],
            expires_in=json_dict['expires_in'],
            token_type=json_dict['token_type'],

        )

Now I am trying to test my authentication if it is raising the right Error if it got invalid credentials. Here is my test script

import logging
import unittest
from typing import Tuple

from Foo import ClientCredentials, Token, AuthenticationError
from Foo.api import API
from settings import config, API_HOST


class MyTestCase(unittest.TestCase):
    def setUp(self):
        pass

    def test_token(self):
        api = API(*self.good_credentials)

        self.assertIsInstance(api.authenticate(), Token)

    def test_invalid_credentials(self):
        api = API(*self.invalid_credentials)

        # self.assertRaises(AuthenticationError, api.authenticate)
        with self.assertRaises(AuthenticationError) as error_msg:
            api.authenticate()
        self.assertEqual(error_msg, "Invalid Credentials. Error: 'access_denied', Description: Unauthorized")

    @unittest.skip("Can't get the right assertRaises")
    def test_invalid_auth_domain(self):
        api = API(*self.invalid_auth_domain)
        with self.assertRaises(TimeoutError) as e:
            api.authenticate()
        self.assertEqual(e, "API Takes too long to respond.")
        # self.assertRaises(TimeoutError, api2.authenticate())

    @property
    def good_credentials(self) -> Tuple[str, ClientCredentials]:
        auth0_domain = "my.auth.domain"
        credentials = ClientCredentials(
            client_id=config.read_param('api.value.client-id'),
            # client_id='aw;oieja;wlejf',
            client_secret=config.read_param('api.value.client-secret'),
            audience=API_HOST,
        )

        return auth0_domain, credentials

    @property
    def invalid_auth_domain(self) -> Tuple[str, ClientCredentials]:
        auth0_domain = "my.auth.domain"
        credentials = ClientCredentials(
            client_id=config.read_param('api.value.client-id'),
            client_secret=config.read_param('api.value.client-secret'),
            audience=API_HOST,
        )

        return auth0_domain, credentials

    @property
    def invalid_credentials(self) -> Tuple[str, ClientCredentials]:
        auth0_domain = "my.auth.domain"
        credentials = ClientCredentials(
            # client_id=config.read_param('api.value.client-id'),
            client_id='aw;oieja;wlejf',
            client_secret=config.read_param('api.value.client-secret'),
            audience=API_HOST,
        )

        return auth0_domain, credentials


if __name__ == '__main__':
    unittest.main()

As you can see in my test, I've tried it both ways for assertRaises:

# Trial 1
with self.assertRaises(AuthenticationError) as e:
    api.authenticate()
self.assertEqual(error_msg, "Invalid Credentials. Error: 'access_denied', Description: Unauthorized")

# Trial 2
self.assertRaises(AuthenticationError, api.authenticate)

The assertRaises fails even with correct Exception being raised. Here is the logs I got from terminal after running the unittest:

Ran 3 tests in 0.842s

FAILED (errors=1, skipped=1)

Error
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 615, in run
    testMethod()
  File "/Users/pluggle/Documents/Gitlab/Laveness/onboarding/test/api_authentication_test.py", line 22, in test_invalid_credentials
    api = API(*self.invalid_credentials)
  File "/Users/pluggle/Documents/Gitlab/Laveness/onboarding/Foo/api.py", line 17, in __init__
    self.token: Token = self.authenticate()
  File "/Users/pluggle/Documents/Gitlab/Laveness/onboarding/Foo/api.py", line 66, in authenticate
    f"Invalid Credentials. Error: '{json_dict['error']}', Description: {json_dict['error_description']}"
Foo.AuthenticationError: Invalid Credentials. Error: 'access_denied', Description: Unauthorized


Assertion failed

Assertion failed

Process finished with exit code 1

Assertion failed

Assertion failed

From this problem: AssertRaises fails even though exception is raised

It seems like he has the same issue with me, but he was importing Error from 2 different Paths, wherein for me, I am pretty sure that my Exception was imported from my __init__.py on my Foo package.

Really hope someone can help me. I really feel like there's just something that I am overlooking here.

Thanks a lot!

1

There are 1 answers

1
anthony sottile On BEST ANSWER

if you read the stacktrace, it is raising on this line:

        api = API(*self.invalid_credentials)

which is one line before the assertRaises and so it will not be captured by it

this is because API.__init__ itself calls self.authenticate()