django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

TestClient lacks of authentication

Open VityasZV opened this issue 3 years ago • 13 comments
trafficstars

Comparing to DRF API Client, which has force_authenticate method, here I can’t authenticate user in tests. Are there any plans for fixing that?

VityasZV avatar Jan 04 '22 10:01 VityasZV

Or maybe someone already found solution for that scenario in tests, any help would be appreciated

VityasZV avatar Jan 04 '22 11:01 VityasZV

@VityasZV When I worked on my small project before, I used HttpBasicAuth to implement the user authentication which is explained in here. For the authentication test, I made custom function that returns token encoded in base64. As the token is made based on username and password, it was easy to encode using base64 module in Python.

After that, I set the headers manually and applied it to the TestClient.

c = TestClient()
headers = {"Authorization": get_user_token("username", "djninja")}
res = c.get(url, headers=headers}
assert res.status_code == 200

Although it is vulnerable for security, but I think it can be a simple example for testing user authentication.

kmlee78 avatar Jan 10 '22 15:01 kmlee78

@VityasZV

I think at this moment you can just override the request method to pass your token/session/key/etc

class AuthenticatedClient(TestClient):

  def request(self, method, path, data = {}, json = None, **request_params: Any):
        headers = {"Authorization": get_user_token("user", "password")}
        request_params["headers"] = headers
        return super().request(method, path, data, json, **request_params)


client = AuthenticatedClient()
client.get(...

vitalik avatar Jan 10 '22 15:01 vitalik

I'm having a really weird inssue where my auth class is not being called at all during unity tests, but it works if I run my app normally and call the endpoint with postman. I'm running the tests with manager.py test. I'm sure the class is not being used because I've tried the debug with pycharm and even some print statements.

My auth class, simplified returning True always and it doesn't work even like that:

class AuthBearer(HttpBearer):
    def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
        return True

My api:

api = NinjaAPI(version="v1", auth=AuthBearer())`
api.add_router("/eventos", eventos_router)

The endpoint:

@router.get("/", response={HTTPStatus.OK: List[EventoOut] , HTTPStatus.NO_CONTENT: None})
def find_all(_):
    return Evento.objects.all()

The test, always returns 401:

        response = self.client.get("/api/eventos/")
        eventos = response.json()
        self.assertEqual(response.status_code, 200)

Any suggestions?

renatounai avatar May 24 '22 12:05 renatounai

I've ended up just disabling auth in tests

auth = None if settings.TESTING else AuthBearer()
api = NinjaAPI(auth=auth)

Not the best solution, but it will have to do for now.

renatounai avatar Jun 07 '22 11:06 renatounai

I'm having the same issue as @renatounai here, is anyone else experiencing this?

GlenWise avatar Jul 17 '22 23:07 GlenWise

Same here.

ssandr1kka avatar Jul 23 '22 11:07 ssandr1kka

Turns out the authenticate method is only called if the Authorization header is present in the request. See the code of the HttpBearer class:

class HttpBearer(HttpAuthBase, ABC):
    openapi_scheme: str = "bearer"
    header: str = "Authorization"

    def __call__(self, request: HttpRequest) -> Optional[Any]:
        headers = get_headers(request)
        auth_value = headers.get(self.header)
        if not auth_value:
            return None
        parts = auth_value.split(" ")

        if parts[0].lower() != self.openapi_scheme:
            if settings.DEBUG:
                logger.error(f"Unexpected auth - '{auth_value}'")
            return None
        token = " ".join(parts[1:])
        return self.authenticate(request, token)

    @abstractmethod
    def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
        pass  # pragma: no cover

So, in my code, I can use something like this in the setup method:

self.client.defaults["HTTP_AUTHORIZATION"] = "Bearer 123"

Or this in the request itself:

response = self.client.post("/api/events/", event_in, content_type=APPLICATION_JSON, HTTP_AUTHORIZATION="Bearer 123")

Changing Bearer 123 for your token of course.

@GlenWise , @ssandr1kka , see if this can help you

renatounai avatar Jul 27 '22 12:07 renatounai

Confirming this issue:

I'm having a really weird issue where my auth class is not being called at all during unit tests, but it works if I run my app normally and call the endpoint with postman.

It turns out that ninja's TestClient has a slight difference about setting request headers. You have to set them with a headers keyword argument. Example:

ninja_client = TestClient(api.default_router)
r = ninja_client.get(
    "event/",
    content_type="application/json",
    headers={"AUTHORIZATION": f"Bearer {user.api_token}"},
)

benjaoming avatar Apr 25 '23 21:04 benjaoming

Upon reviewing the code, I discovered that it is possible to specify the user with test client.

response = client.get("/quests", user=fake_user)

lucemia avatar May 15 '23 12:05 lucemia

It turns out that ninja's TestClient has a slight difference about setting request headers. You have to set them with a headers keyword argument. Example:

It works for Django v3+. For Django v2 you need to add HTTP_AUTHORIZATION instead of headers

from ninja.testing import TestClient

def test(client: TestClient):
  r = client.get(
      "event/",
      content_type="application/json",
      HTTP_AUTHORIZATION=f"Bearer {user.api_token}"},
  )

vladyslavushakov avatar Jul 25 '23 22:07 vladyslavushakov

Sorry you had to go through this @vladyslavushakov :/

I've been wanting to write some documentation as a how-to style section on writing test cases with django-ninja for a while.

Would people like that?

benjaoming avatar Jul 26 '23 12:07 benjaoming

I'm having issues adding authentication to the TestClient Based on the TesClient source code, I would expect that passing the user as a request_param should be enough, but it looks like it is not. Because when the client calls the operation, this one checks for the Authorization header, and even if you pass it, it tries to use it then to authenticate as far as I have debugged.

If this is the case, assigning the user when building the request in the TestClient doesn't have sense at all, since it's going to be ignored.

Also, passing the header with a token when running a test shouldn't be an option. It's a security risk Also, might be a good idea to be able to assign the user during the client construction

In any case, at least what I managed to do to fix it from my side was to redefined the HttpBearer.__call__ in my custom class with a small change

class MyHttpBearer(HttpBearer):
    def __call__(self, request: HttpRequest) -> Optional[Any]:
        if request.user:
            return request.user
        return super().__call__(request)

    def authenticate(self, request, token):
        ...

I also tried another solution before, and it was to defined an authenticator for Testing, that basically during my test environment, will authenticate with a dummy user depending on some info send in the request, but this was looked simpler

For fixing this in Django Ninja, we could do the same, but inOperation._run_authentication, though for a better testing, it would have sense to have an authenticator for this, or someway we could simmulate the real authentication process

aryan-curiel avatar Apr 22 '24 10:04 aryan-curiel