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

feat(testing): add test example with test user #181

Merged
merged 3 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ jobs:
tox -v
env:
DJANGO: ${{ matrix.django-version }}
DESCOPE_PROJECT_ID: P2ZRsmAQw8MKG78knGZ9GXWRqxM5
DESCOPE_MANAGEMENT_KEY: ${{ secrets.DESCOPE_MANAGEMENT_KEY }}
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ pip install django-descope

6. Start the development server and visit the newly created view

## Testing

See [test_admin.py](example_app/test_admin.py) for a rudimentary example of
how to utilize [Descope Test Users](https://docs.descope.com/manage/testusers/)
when testing your application with authenticated users.
You can use the helper [`django_descope.authentication.add_tokens_to_request`](django_descope/authentication.py) to add the tokens to the django session

> [!IMPORTANT]
> Remember you must create the relevant roles in [Descope Console](https://app.descope.com)
> so you can utilize them in your testing.

## Settings

The following settings are available to configure in your project `settings.py`
Expand Down
23 changes: 15 additions & 8 deletions django_descope/authentication.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any, Union

from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_TOKEN_NAME
from descope.exceptions import AuthException
Expand All @@ -13,13 +14,21 @@
logger = logging.getLogger(__name__)


def add_tokens_to_request(session: Any, session_token: str, refresh_token: str):
session[SESSION_COOKIE_NAME] = session_token
session[REFRESH_SESSION_COOKIE_NAME] = refresh_token
session.save()


class DescopeAuthentication(BaseBackend):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def authenticate(self, request: HttpRequest):
session_token = request.session.get(SESSION_COOKIE_NAME)
refresh_token = request.session.get(REFRESH_SESSION_COOKIE_NAME)
def authenticate(self, request: Union[HttpRequest, None], **kwargs):
if request is None:
return None
session_token = request.session.get(SESSION_COOKIE_NAME, "")
refresh_token = request.session.get(REFRESH_SESSION_COOKIE_NAME, "")

logger.debug("Validating (and refreshing) Descope session")
try:
Expand All @@ -42,13 +51,11 @@ def authenticate(self, request: HttpRequest):
if settings.DEBUG:
# Contains sensitive information, so only log in DEBUG mode
logger.debug(validated_session)
return self.get_user(request, validated_session, refresh_token)

def get_user(self, request: HttpRequest, validated_session, refresh_token):
if validated_session:
username = validated_session[SESSION_TOKEN_NAME]["sub"]
user, created = DescopeUser.objects.get_or_create(username=username)
user, _ = DescopeUser.objects.get_or_create(username=username)
user.sync(validated_session, refresh_token)
request.session[SESSION_COOKIE_NAME] = user.session_token["jwt"]
return user
return None
else:
return None
7 changes: 4 additions & 3 deletions django_descope/middleware.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import logging

from django.contrib.auth import login
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest
from django.utils.deprecation import MiddlewareMixin

from .authentication import DescopeAuthentication

logger = logging.getLogger(__name__)


class DescopeMiddleware:
class DescopeMiddleware(MiddlewareMixin):
_auth = DescopeAuthentication()

def __init__(self, get_response: HttpResponse = None):
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request: HttpRequest):
Expand Down
6 changes: 3 additions & 3 deletions django_descope/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from django.views import View
from django.views.decorators.cache import never_cache

# User = get_user_model()
from .authentication import add_tokens_to_request

logger = logging.getLogger(__name__)


Expand All @@ -19,8 +20,7 @@ def post(self, request: HttpRequest):
refresh = request.POST.get(REFRESH_SESSION_COOKIE_NAME)

if session and refresh:
request.session[SESSION_COOKIE_NAME] = session
request.session[REFRESH_SESSION_COOKIE_NAME] = refresh
add_tokens_to_request(request, session, refresh)
return JsonResponse({"success": True})

return HttpResponseBadRequest()
Empty file added example_app/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions example_app/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import json
import logging
import random
import string

import descope
from descope import REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME
from django.test import TestCase

from django_descope import descope_client
from django_descope.authentication import add_tokens_to_request

logger = logging.getLogger(__name__)


def random_string(N: int) -> str:
return "".join(random.choices(string.ascii_lowercase + string.digits, k=N))


class AdminLoginTestCase(TestCase):
delivery_method = descope.DeliveryMethod.EMAIL
login_id = f"test+{random_string(8)}@test.internal"
token: dict

def setUp(self) -> None:
descope_client.mgmt.user.create_test_user(
self.login_id, role_names=["is_staff", "is_superuser"], verified_email=True
)
resp = descope_client.mgmt.user.generate_otp_for_test_user(
self.delivery_method,
self.login_id,
)
self.token = descope_client.otp.verify_code(
self.delivery_method, self.login_id, resp.get("code")
)

session = self.client.session
add_tokens_to_request(
session,
self.token[SESSION_TOKEN_NAME]["jwt"],
self.token[REFRESH_SESSION_TOKEN_NAME]["jwt"],
)

def test_test_user_can_login_to_admin(self):
"""Test that if user has the right roles they can login to admin"""

res = self.client.get("/debug")
self.assertEqual(res.status_code, 200)

debug = json.loads(res.content)
self.assertEqual(debug["user"], self.token["userId"])
self.assertEqual(debug["email"], self.login_id)
self.assertTrue(debug["is_authenticated"])
self.assertTrue(debug["is_staff"])
self.assertTrue(debug["is_superuser"])

res = self.client.get("/admin/")
self.assertEqual(res.status_code, 200, res.headers)

def tearDown(self) -> None:
descope_client.mgmt.user.delete(self.login_id)
4 changes: 0 additions & 4 deletions example_app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@

from django_descope import descope_client
from django_descope.models import DescopeUser
from django.shortcuts import render

from django.urls import path


logger = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import os
from pathlib import Path
from typing import List

from dotenv import load_dotenv

Expand Down Expand Up @@ -103,8 +104,7 @@
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [] # With descope, there's no need for passwords!

AUTH_PASSWORD_VALIDATORS: List[str] = [] # With descope, there's no need for passwords!

# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/
Expand Down
4 changes: 3 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ commands =
python manage.py test
extras=
test
passenv=
DESCOPE_PROJECT_ID
DESCOPE_MANAGEMENT_KEY
setenv=
PYTHONDONTWRITEBYTECODE=1
DESCOPE_PROJECT_ID=test
deps=
dj32: Django>=3.2,<3.3
dj40: Django>=4.0,<4.1
Expand Down
Loading