Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TestClient lacks of authentication #3

Closed
VityasZV opened this issue Jan 4, 2022 · 7 comments
Closed

TestClient lacks of authentication #3

VityasZV opened this issue Jan 4, 2022 · 7 comments

Comments

@VityasZV
Copy link

VityasZV commented Jan 4, 2022

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?

@eadwinCode
Copy link
Owner

Yes, I don't have force_authenticate feature implemented. I never thought it's that useful.

The TestClient inherits from django-ninja TestClient and it has different ways you can Authenticate a user during testing.

What type of authentication are you using in your project?

@VityasZV
Copy link
Author

VityasZV commented Jan 4, 2022

Hi! I am using JWTAuthAsync with default settings for now (AccessToken), here is piece of code:

`
class JWTAuthAsync(JWTAuth):
@sync_to_async
def authenticate(self, request: HttpRequest, token: str) -> Any:
logger.debug("AUTHENTICATE")
return self.jwt_authenticate(request, token)

class NinjaJWTAsync(NinjaJWTDefaultController):
@sync_to_async
@http_post("/verify", response={200: Schema}, url_name="token_verify")
def verify_token(self, token: schema.TokenVerifySerializer):
return {}

  @sync_to_async
  @http_post("/pair", response=schema.TokenObtainPairOutput, url_name="token_obtain_pair")
  def obtain_token(self, user_token: schema.TokenObtainPairSerializer):
      return user_token.output_schema()

  @sync_to_async
  @http_post("/refresh", response=schema.TokenRefreshSerializer, url_name="token_refresh")
  def refresh_token(self, refresh_token: schema.TokenRefreshSchema):
      refresh = schema.TokenRefreshSerializer(**refresh_token.dict())
      return refresh

class IsJWTAuthenticatedAsync:
"""
Allows access only to authenticated users. Manually written for async support
"""

  @staticmethod
  def raise_exception_if_no_permission(fn):
      async def wrapper(*args, **kwargs):
          logger.debug(f"{args=}  {kwargs=}")
          res = await fn(*args, **kwargs)
          logger.debug(f"{res=}")
          if not res:
              raise HttpError(403, "You don't have permission to perform this action. Wrong JWT Auth")
      return wrapper

  @raise_exception_if_no_permission
  @staticmethod
  async def is_authenticated(request: HttpRequest) -> bool:  # NOQA
      logger.debug(f"{request.auth=}")
      auth = await request.auth
      logger.debug(f"{auth=}")
      user = request.user or auth  # type: ignore
      logger.debug(f"{user=}")
      return bool(user and user.is_authenticated)
      
  @raise_exception_if_no_permission
  @staticmethod
  async def is_profile_authenticated(request: HttpRequest) -> bool:
      pre_res = await IsJWTAuthenticatedAsync.is_authenticated(request=request)
      user = request.user
      logger.debug(f"{user=}")
      username_to_profiles = await sync_to_async(UsernameHashToProfiles.objects.select_related('user', 'customer', 'creator', 'gallery').get)(user=user)
      return bool(username_to_profiles.customer or username_to_profiles.creator or username_to_profiles.gallery)

`

Here is view example:
`@api_controller(prefix_or_class="/profile")
class ProfileController:

@http_get(path="", auth=JWTAuthAsync(), response=List[GetProfilesOutSchema])
async def get_profiles(self, request, search_str: str = None):
    await IsJWTAuthenticatedAsync.is_profile_authenticated(request=request)
    search_args = search_str.split(sep=" ") if search_str else []
    if search_args:
        return await get_profiles_by_search_args(search_args=search_args)
    else:
        return []

`

My testing scenario is following:

`import pytest
from ninja_extra.testing import TestAsyncClient

from profile.api import ProfileController

prefix = "/profile"

@pytest.mark.parametrize(
"search_str, expected_data",
[
(
"",
[],
),
( "H",
[]
)
],
)
@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_profile(search_str, expected_data):
result = await TestAsyncClient(ProfileController).get(path=f"{prefix}", query=dict(search_str=search_str))
# TODO - huuuuge problems with JWT Auth testing, Test Client does not appear to have force_authenticate method or smth like that, so i'll get 401 unauthorized on each request to api
assert result.json() == expected_data
# result = await profile_client_jwt_authentication.get(path=f"{prefix}", query=dict(search_str=search_str))
# assert result.json() == expected_data
`
What can you advise for user authorization by JWT token in tests? I would not like to mock JWTAuth in tests :(

@VityasZV
Copy link
Author

VityasZV commented Jan 4, 2022

huge sorry for wrong indentations in comment, hope you’ll understand what i’m talking about

@eadwinCode
Copy link
Owner

This is what I could come up with. You need generate an auth token and pass it as headers param.
Follow the example below. Let me know if it solves your problem.

Also use the right token for your project. Here I used, SlidingToken

from ninja_jwt.tokens import SlidingToken


def get_authentication_header(user, header_key="Authorization"):
    token = SlidingToken.for_user(user)
    user_credential = {header_key: "Bearer " + str(token)}
    return user_credential

prefix = "/profile"

@pytest.mark.parametrize(
"search_str, expected_data",
  [
     ("", []), 
     ( "H", [])
  ]
)
@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_profile(search_str, expected_data):
    result = await profile_client_jwt_authentication.get(
        path=f"{prefix}", query=dict(search_str=search_str),
        headers=get_authentication_header(user=your_test_user)
    )
    assert result.json() == expected_data   

@VityasZV
Copy link
Author

VityasZV commented Jan 4, 2022

I want to hug you friend !!! Not without perversion with my asynchronous JWT, but with your prompt, it all worked out!

@eadwinCode
Copy link
Owner

Glad it worked out.

@FabianClemenz
Copy link

FabianClemenz commented Mar 17, 2023

Hi :)

i'm using something whats more like a real life scenario - calling the wanted APIs, so i can test the workflow too in one step. Also i'm using the plain old django test client :) - testing is done with pytest and factories build with factory-boy

from django.test import Client
import pytest

@pytest.fixture
def api_client():
    return Client()

@pytest.fixture
def user_and_access_token(api_client):
    user = UserFactory(active=True)
    user.set_password("secretpass")
    user.save()
    token_response = api_client.post(
        "/api/token/pair",
        data={
            "username": user.username,
            "password": "secretpass"
        },
        content_type="application/json",
    )
    access_token = token_response.json()["access"]

    return user, access_token

# use it in tests like this
def test_something(api_client, user_and_access_token):
    user, access_token = user_and_access_token
    # set access token in header
    header = {"HTTP_AUTHORIZATION": f"bearer {access_token}"}

    # WHEN
    response = api_client.get(url, **header)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants