Skip to content

Commit

Permalink
Fix User.last_login bug with login endpoints. add unit test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarychen committed Jan 14, 2024
1 parent 1ae9ffd commit 8423c39
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 8 deletions.
16 changes: 10 additions & 6 deletions ninja_simple_jwt/auth/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,21 @@
web_auth_router = Router()


@mobile_auth_router.post("/sign-in", response=MobileSignInResponse)
@mobile_auth_router.post("/sign-in", response=MobileSignInResponse, url_name="mobile_signin")
def mobile_sign_in(request: HttpRequest, payload: SignInRequest) -> dict:
payload_data = payload.dict()
user = authenticate(username=payload_data["username"], password=payload_data["password"])
user_logged_in.send(sender=user.__class__, request=request, user=user)

if user is None:
raise AuthenticationError()

user_logged_in.send(sender=user.__class__, request=request, user=user)
refresh_token, _ = get_refresh_token_for_user(user)
access_token, _ = get_access_token_for_user(user)
return {"refresh": refresh_token, "access": access_token}


@mobile_auth_router.post("/token-refresh", response=MobileTokenRefreshResponse)
@mobile_auth_router.post("/token-refresh", response=MobileTokenRefreshResponse, url_name="mobile_token_refresh")
def mobile_token_refresh(request: HttpRequest, payload: MobileTokenRefreshRequest) -> dict:
payload_data = payload.dict()
try:
Expand All @@ -48,13 +50,15 @@ def mobile_token_refresh(request: HttpRequest, payload: MobileTokenRefreshReques
return {"access": access_token}


@web_auth_router.post("/sign-in", response=WebSignInResponse)
@web_auth_router.post("/sign-in", response=WebSignInResponse, url_name="web_signin")
def web_sign_in(request: HttpRequest, payload: SignInRequest, response: HttpResponse) -> dict:
payload_data = payload.dict()
user = authenticate(username=payload_data["username"], password=payload_data["password"])
user_logged_in.send(sender=user.__class__, request=request, user=user)

if user is None:
raise AuthenticationError()

user_logged_in.send(sender=user.__class__, request=request, user=user)
refresh_token, refresh_token_payload = get_refresh_token_for_user(user)
access_token, _ = get_access_token_for_user(user)
response.set_cookie(
Expand All @@ -69,7 +73,7 @@ def web_sign_in(request: HttpRequest, payload: SignInRequest, response: HttpResp
return {"access": access_token}


@web_auth_router.post("/token-refresh", response=WebSignInResponse)
@web_auth_router.post("/token-refresh", response=WebSignInResponse, url_name="web_token_refresh")
def web_token_refresh(request: HttpRequest) -> dict:
cookie = request.COOKIES.get(ninja_simple_jwt_settings.JWT_REFRESH_COOKIE_NAME)
if cookie is None:
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = django-ninja-simple-jwt
version = 0.4.0
version = 0.4.1
description = Simple JWT-based authentication using Django and Django-ninja
long_description = file: README.md
url = https://github.com/oscarychen/django-ninja-simple-jwt
Expand Down
Empty file added tests/test_auth/__init__.py
Empty file.
151 changes: 151 additions & 0 deletions tests/test_auth/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from datetime import datetime, timedelta
from typing import Any

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from freezegun import freeze_time

from ninja_simple_jwt.jwt.key_creation import make_and_save_key_pair
from ninja_simple_jwt.jwt.token_operations import get_refresh_token_for_user
from ninja_simple_jwt.settings import DEFAULTS


class TestAuthEndPoints(TestCase):
@staticmethod
def merge_settings(**kwargs: Any) -> dict:
return {**DEFAULTS, **kwargs}

def setUp(self) -> None:
make_and_save_key_pair()


class TestMobileSignIn(TestAuthEndPoints):
def test_user_can_sign_in(self) -> None:
username = "user"
password = "password"
user = get_user_model().objects.create_user(username=username, password=password)

response = self.client.post(
reverse("api-1.0.0:mobile_signin"),
data={"username": username, "password": password},
content_type="application/json",
)

self.assertEqual(200, response.status_code, "Correct status code.")
self.assertIn("refresh", response.json(), "Response data contains refresh token.")
self.assertIn("access", response.json(), "Response data contains access token.")

user.refresh_from_db()
self.assertIsNotNone(user.last_login, "User.last_login updated.")

def test_inactive_user_cannot_sign_in(self) -> None:
username = "user"
password = "password"
get_user_model().objects.create_user(username=username, password=password, is_active=False)

response = self.client.post(
reverse("api-1.0.0:mobile_signin"),
data={"username": username, "password": password},
content_type="application/json",
)

self.assertEqual(401, response.status_code, "Inactive user cannot sign in.")


class TestMobileRefresh(TestAuthEndPoints):
def test_user_can_refresh_token(self) -> None:
user = get_user_model().objects.create_user(username="user")

with self.settings(
NINJA_SIMPLE_JWT=self.merge_settings(
JWT_REFRESH_TOKEN_LIFETIME=timedelta(days=31),
JWT_ACCESS_TOKEN_LIFETIME=timedelta(minutes=5),
)
):
with freeze_time("2024-01-11 12:00:01"):
refresh_token, _ = get_refresh_token_for_user(user)
response = self.client.post(
reverse("api-1.0.0:mobile_token_refresh"),
data={"refresh": refresh_token},
content_type="application/json",
)

self.assertEqual(200, response.status_code, "Correct status code.")
self.assertIn("access", response.json(), "Response data contains access token.")
self.assertNotIn("refresh", response.json(), "Response data should not contain refresh token.")


class TestWebSignIn(TestAuthEndPoints):
def test_user_can_sign_in(self) -> None:
username = "user"
password = "password"
user = get_user_model().objects.create_user(username=username, password=password)

with self.settings(
NINJA_SIMPLE_JWT=self.merge_settings(
JWT_REFRESH_TOKEN_LIFETIME=timedelta(days=31),
JWT_ACCESS_TOKEN_LIFETIME=timedelta(minutes=5),
WEB_REFRESH_COOKIE_PATH="/tests/refresh_api_path",
)
):
with freeze_time("2024-01-11 12:00:01"):
response = self.client.post(
reverse("api-1.0.0:web_signin"),
data={"username": username, "password": password},
content_type="application/json",
)

self.assertEqual(200, response.status_code, "Correct status code.")
self.assertNotIn("refresh", response.json(), "Response body should not contain refresh token.")
self.assertIn("refresh", response.cookies, "Response header Set-Cookie has refresh token.")
self.assertIn("access", response.json(), "Response body contains access token.")

refresh_token_cookie = response.cookies.get("refresh")
self.assertTrue(refresh_token_cookie["httponly"], "Refresh token cookie is HttpOnly.")
self.assertEqual(
"/tests/refresh_api_path", refresh_token_cookie["path"], "Refresh token cookie has correct path."
)
cookie_expires = datetime.strptime(refresh_token_cookie["expires"], "%a, %d %b %Y %H:%M:%S %Z")
self.assertEqual(datetime(2024, 2, 11, 12, 0, 2), cookie_expires, "Refresh token cookies has correct expires.")

user.refresh_from_db()
self.assertIsNotNone(user.last_login, "User.last_login updated.")

def test_inactive_user_cannot_sign_in(self) -> None:
username = "user"
password = "password"
get_user_model().objects.create_user(username=username, password=password, is_active=False)

response = self.client.post(
reverse("api-1.0.0:web_signin"),
data={"username": username, "password": password},
content_type="application/json",
)

self.assertEqual(401, response.status_code, "Inactive user cannot sign in.")


class TestWebRefresh(TestAuthEndPoints):
def test_user_token_refresh(self) -> None:
user = get_user_model().objects.create_user(username="user")

with self.settings(
NINJA_SIMPLE_JWT=self.merge_settings(
JWT_REFRESH_TOKEN_LIFETIME=timedelta(days=31),
JWT_ACCESS_TOKEN_LIFETIME=timedelta(minutes=5),
JWT_REFRESH_COOKIE_NAME="refresh-token",
)
):
with freeze_time("2024-01-11 12:00:01"):
refresh_token, _ = get_refresh_token_for_user(user)

response = self.client.post(
reverse("api-1.0.0:web_token_refresh"),
content_type="application/json",
HTTP_COOKIE=f"refresh-token={refresh_token}",
)

self.assertEqual(200, response.status_code, "Correct status code.")
self.assertIn("access", response.json(), "Response body has access token.")
self.assertNotIn("refresh", response.json(), "Response body should not have refresh token.")
12 changes: 11 additions & 1 deletion tests/urls.py
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
urlpatterns = [] # type: ignore
from django.urls import path
from ninja import NinjaAPI

from ninja_simple_jwt.auth.views.api import mobile_auth_router, web_auth_router

api = NinjaAPI()
api.add_router("/auth/mobile/", mobile_auth_router)
api.add_router("/auth/web/", web_auth_router)


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

0 comments on commit 8423c39

Please sign in to comment.