vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.25k stars 431 forks source link

How to test api.exception_handler #1171

Closed cosgus closed 4 months ago

cosgus commented 5 months ago

I have api.exception handlers to return a particular payload when that error is raised. Works great but I cant figure out how to handle the tests. Here is a condensed version of my code:

api.py

api_v1 = NinjaAPI()
router = Router()

# I have also tried this with api_v1.get
@router.get("/endpoint1")
def get_endpoint(request):
    data = thing.do()  #This is where the exception would get raised
    return api_v1.create_response(request=request, data=response_data, status=200)

@api.exception_handler(CustomException)
def crawler_locked_handler(request, exc):
    data = {"error": "FATAL ERROR", "message": exc.error}
    return api_v1.create_response(request=request, data=data, status=500)

api_v1.add_router("",router)

and here is my test. If I instatiate the Ninja TestClient with api_v1.router, the CustomError gets raised but the exception handler does not catch it. TestClient(api_v1) raises a ninja ConfigError. Django.test Client() works as expected.

from api import router, api_v1
from ninja.testing import TestClient
from django.test import Client

class ThingPatch:

    def raise_custom_error(cls):
        raise CustomError

class TestApiEndpoints(unittest.TestCase):

    def setUp(self):
        self.ninja_client = TestClient(router) # Raises CustomError but does not catch in exception handler
        # self.ninja_client = TestClient(api_v1) # Raises ConfigError copied below
        self.django_client = Client() # Works as expected

    @patch('thing.do', ThingPatch.raise_custom_error)
    def test_custom_error_request(self):
        response = self.ninja_client.get(f'/endpoint1', content_type="application/json")
        self.assertEqual(response.status_code, 500)

ConfigError:

ninja.errors.ConfigError: Looks like you created multiple NinjaAPIs or TestClients
To let ninja distinguish them you need to set either unique version or urls_namespace
 - NinjaAPI(..., version='2.0.0')
 - NinjaAPI(..., urls_namespace='otherapi')
Already registered: ['api-1.0.0']
danyadanch commented 4 months ago

sup! i have very strange workaround for this, but got it working

# apps.common.api.errors
from django.core.exceptions import ValidationError
from ninja import NinjaAPI

def get_validation_error_handler(api: NinjaAPI):
    def handle(request, exc: ValidationError):
        error = "Validation Error"

        error_data = {"message": error, "errors": exc.error_list}

        return api.create_response(request, error_data, status=400)

    return (handle, ValidationError)

def get_exception_handler(api: NinjaAPI):
    def handle(request, exc: Exception):
        error_data = {"message": str(exc)}
        return api.create_response(request, error_data, status=500)

    return (handle, Exception)

EXCEPTION_HANDLERS = [get_validation_error_handler, get_exception_handler]

def add_exception_handlers(api: NinjaAPI):
    for handler in EXCEPTION_HANDLERS:
        handler_func, exc = handler(api)
        api.add_exception_handler(handler=handler_func, exc_class=exc)
# apps.common.test_client
'''
   This is mixin file for testing
''' 
from django.conf import settings
from ninja import NinjaAPI, Router
from ninja.testing import TestClient
from apps.common.api.errors import add_exception_handlers

class BaseNinjaTestCase(object):
    @classmethod
    def setup_client(cls, router: Router):
        settings.DEBUG = True
        settings.IN_TEST = True

        cls.api_client = TestClient(router)

       ''' 
          https://github.com/vitalik/django-ninja/blob/c6d44b62a180fcf8ddfd73d67e0274a77b9d30ae/ninja/testing/client.py#L94-L95
          when we in test we not having urls, so it recreates api instance without attached handlers
       '''
        _ = cls.api_client.urls

        # add handlers
        add_exception_handlers(cls.api_client.router_or_app.api)
# example test file
from apps.common.test_client import BaseNinjaTestCase
from apps.sheets.operations.api import router

class TestOperationsApi(TestCase, BaseNinjaTestCase):
    @classmethod
    def setUpTestData(cls):
        cls.setup_client(router)