django-ninja
django-ninja copied to clipboard
TestClient lacks of authentication
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?
Or maybe someone already found solution for that scenario in tests, any help would be appreciated
@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.
@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(...
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?
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.
I'm having the same issue as @renatounai here, is anyone else experiencing this?
Same here.
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
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}"},
)
Upon reviewing the code, I discovered that it is possible to specify the user with test client.
response = client.get("/quests", user=fake_user)
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}"},
)
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?
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