vitalik / django-ninja

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

TestClient instantiation throws different errors due to the values I pass to router_or_app argument #354

Open abdelrahmenAyman opened 2 years ago

abdelrahmenAyman commented 2 years ago

Hello there, I am still new to Ninja and I have had a hard time trying to figure out how to use the test clients provided in the testing package, and I couldn't find in the documentation any clue on how to use them, it would be much appreciated if anyone could help me with how to use the TestClient properly.

I have the following secondary_django_app/views/project_api.py:

project_router = Router()
class ProjectSchema(ModelSchema): ...
@project_router.post("/process-project", url_name="process-project")
async def process_project(request, project: ProjectSchema):
    return project

Then in the main django app main/api.py:

api = NinjaAPI()
api.add_router("/projects/", project_router)

main/urls.py:

from main.api import api
urlpatterns = [path("api/", api.urls)]

Then I am having a test function where I tried several ways to instantiate the TestAsyncClient where I try to send a request to the process-project endpoint:

from django.urls import reverse_lazy
from ninja.testing import TestAsyncClient

def test_process_project():
    c = TestAsyncClient(...)
    path = reverse_lazy("api-1.0.0:process-project")
    c.post(...)

I have tried importing the api from main/api.py and pass it to TestAsyncClient and tried passing NinjaAPI() instead but both generate the following error:

self = <ninja.main.NinjaAPI object at 0x7f40f20bb850>

    def _validate(self) -> None:
        from ninja.security import APIKeyCookie

        # 1) urls namespacing validation
        skip_registry = os.environ.get("NINJA_SKIP_REGISTRY", False)
        if (
            not skip_registry
            and self.urls_namespace in NinjaAPI._registry
            and not debug_server_url_reimport()
        ):
            msg = [
                "Looks like you created multiple NinjaAPIs",
                "To let ninja distinguish them you need to set either unique version or url_namespace",
                " - NinjaAPI(..., version='2.0.0')",
                " - NinjaAPI(..., urls_namespace='otherapi')",
                f"Already registered: {NinjaAPI._registry}",
            ]
>           raise ConfigError("\n".join(msg))
E           ninja.errors.ConfigError: Looks like you created multiple NinjaAPIs
E           To let ninja distinguish them you need to set either unique version or url_namespace
E            - NinjaAPI(..., version='2.0.0')
E            - NinjaAPI(..., urls_namespace='otherapi')
E           Already registered: ['api-1.0.0']

I also tried importing the project_router from project_api.py and pass it but then a different error is thrown:

self = <ninja.testing.client.TestAsyncClient object at 0x7f494c904cd0>, method = 'GET', path = '/api/projects/process-project', data = {}, request_params = {}

    def _resolve(
        self, method: str, path: str, data: Dict, request_params: Any
    ) -> Tuple[Callable, Mock, Dict]:
        url_path = path.split("?")[0].lstrip("/")
        for url in self.urls:
            match = url.resolve(url_path)
            if match:
                request = self._build_request(method, path, data, request_params)
                return match.func, request, match.kwargs
>       raise Exception(f'Cannot resolve "{path}"')
E       Exception: Cannot resolve "/api/projects/process-project"

/usr/local/lib/python3.9/site-packages/ninja/testing/client.py:89: Exception

I tried several other values, but I don't think there is a point in including everything I tried, I am sure there is something I am missing here, I can use Django's TestClient and use json.loads on the response content, but I would rather use the cleaner way which is using the TestClient provided by Ninja.

vitalik commented 2 years ago

@abdelrahmenAyman

when you use Ninja test client - you should not rely on django url resolver (url_name)

c = TestAsyncClient(api) # or TestAsyncClient(router)

c.get('/projects/process-project')
vitalik commented 2 years ago

DjangoNinja skips the middleware/urlsover layer...

if you need it in your logic - just use default Django Test Client

dvf commented 4 months ago

I'm having the same issue, but I'm not relying on any middleware or URL Resolver:

urls.py

from django.urls import path
from ninja import NinjaAPI

from foo.api import router
from project.auth import JWTAuth

api = NinjaAPI(
    version="1.0.0",
    title="My API",
    auth=JWTAuth(),
)

api.add_router("addresses/", router, tags=["Addresses"])

urlpatterns = [path("", api.urls)]

My test fails:

@pytest.mark.django_db
def test_get_address_happy():
    client = TestClient(api)
    res = client.get("/addresses/")

    assert res.status_code == 200
../../../../.venv/lib/python3.12/site-packages/ninja/testing/client.py:35: in get
    return self.request("GET", path, data, **request_params)
../../../../.venv/lib/python3.12/site-packages/ninja/testing/client.py:85: in request
    func, request, kwargs = self._resolve(method, path, data, request_params)
../../../../.venv/lib/python3.12/site-packages/ninja/testing/client.py:104: in _resolve
    for url in self.urls:
../../../../.venv/lib/python3.12/site-packages/ninja/testing/client.py:93: in urls
    self._urls_cache = self.router_or_app.urls[0]
../../../../.venv/lib/python3.12/site-packages/ninja/main.py:397: in urls
    self._validate()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <ninja.main.NinjaAPI object at 0x1126c0ad0>

        def _validate(self) -> None:
            # urls namespacing validation
            skip_registry = os.environ.get("NINJA_SKIP_REGISTRY", False)
            if (
                not skip_registry
                and self.urls_namespace in NinjaAPI._registry
                and not debug_server_url_reimport()
            ):
                msg = f"""
    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: {NinjaAPI._registry}
    """
>               raise ConfigError(msg.strip())
E               ninja.errors.ConfigError: Looks like you created multiple NinjaAPIs or TestClients
E               To let ninja distinguish them you need to set either unique version or urls_namespace
E                - NinjaAPI(..., version='2.0.0')
E                - NinjaAPI(..., urls_namespace='otherapi')
E               Already registered: ['api-1.0.0']
gsimore commented 3 months ago

I had the same issue and setting this environment variable (for tests only) fixed it for me NINJA_SKIP_REGISTRY=1