feat(backend): added auth, reviews, users modules
also provided tests
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
from apps.users.auth.jwt import create_access_token
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.services import user_create
|
||||
|
||||
|
||||
def _make_user(
|
||||
username="testuser",
|
||||
email="test@lotty.local",
|
||||
password="testpass123", # noqa: S107
|
||||
role=UserRole.VIEWER,
|
||||
**kwargs,
|
||||
) -> User:
|
||||
return user_create(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
role=role,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _auth_header(user) -> str:
|
||||
token: str = create_access_token(user.pk, user.role)
|
||||
return f"Bearer {token}"
|
||||
@@ -0,0 +1,116 @@
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from apps.users.auth.bearer import JWTBearer
|
||||
from apps.users.auth.jwt import (
|
||||
TokenError,
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
create_token_pair,
|
||||
decode_access_token,
|
||||
decode_refresh_token,
|
||||
decode_token,
|
||||
)
|
||||
from apps.users.models import User, UserRole
|
||||
|
||||
from ._helpers import _make_user
|
||||
|
||||
|
||||
class JWTCreateTest(TestCase):
|
||||
def test_create_access_token(self) -> None:
|
||||
token: str = create_access_token(uuid.uuid4(), "admin")
|
||||
self.assertIsInstance(token, str)
|
||||
self.assertTrue(len(token) > 0)
|
||||
|
||||
def test_create_refresh_token(self) -> None:
|
||||
token: str = create_refresh_token(uuid.uuid4())
|
||||
self.assertIsInstance(token, str)
|
||||
|
||||
def test_create_token_pair(self) -> None:
|
||||
pair: dict[str, str] = create_token_pair(uuid.uuid4(), "viewer")
|
||||
self.assertIn("access", pair)
|
||||
self.assertIn("refresh", pair)
|
||||
|
||||
|
||||
class JWTDecodeTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.uid: uuid.UUID = uuid.uuid4()
|
||||
|
||||
def test_decode_access_token(self) -> None:
|
||||
token: str = create_access_token(self.uid, "experimenter")
|
||||
payload: dict[str, Any] = decode_access_token(token)
|
||||
self.assertEqual(payload["sub"], str(self.uid))
|
||||
self.assertEqual(payload["role"], "experimenter")
|
||||
self.assertEqual(payload["type"], "access")
|
||||
|
||||
def test_decode_refresh_token(self) -> None:
|
||||
token: str = create_refresh_token(self.uid)
|
||||
payload: dict[str, Any] = decode_refresh_token(token)
|
||||
self.assertEqual(payload["sub"], str(self.uid))
|
||||
self.assertEqual(payload["type"], "refresh")
|
||||
|
||||
def test_decode_wrong_type_raises(self) -> None:
|
||||
token: str = create_refresh_token(self.uid)
|
||||
with self.assertRaises(TokenError):
|
||||
decode_access_token(token)
|
||||
|
||||
def test_decode_expired_token_raises(self) -> None:
|
||||
token: str = create_access_token(
|
||||
self.uid, "admin", lifetime=timedelta(seconds=-1)
|
||||
)
|
||||
with self.assertRaises(TokenError):
|
||||
decode_access_token(token)
|
||||
|
||||
def test_decode_invalid_token_raises(self) -> None:
|
||||
with self.assertRaises(TokenError):
|
||||
decode_token("not.a.jwt")
|
||||
|
||||
def test_extra_claims(self) -> None:
|
||||
token: str = create_access_token(
|
||||
self.uid, "admin", extra_claims={"org": "lotty"}
|
||||
)
|
||||
payload: dict[str, Any] = decode_access_token(token)
|
||||
self.assertEqual(payload["org"], "lotty")
|
||||
|
||||
|
||||
class JWTBearerTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.bearer = JWTBearer()
|
||||
self.user: User = _make_user(
|
||||
username="bearer_user",
|
||||
email="bearer@x.com",
|
||||
role=UserRole.ADMIN,
|
||||
)
|
||||
|
||||
def test_valid_token_returns_user(self) -> None:
|
||||
token: str = create_access_token(self.user.pk, self.user.role)
|
||||
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
result: User | None = self.bearer.authenticate(request, token)
|
||||
self.assertEqual(result, self.user)
|
||||
|
||||
def test_invalid_token_returns_none(self) -> None:
|
||||
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
result: User | None = self.bearer.authenticate(request, "garbage")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_nonexistent_user_returns_none(self) -> None:
|
||||
token: str = create_access_token(uuid.uuid4(), "admin")
|
||||
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
result: User | None = self.bearer.authenticate(request, token)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_inactive_user_returns_none(self) -> None:
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
token: str = create_access_token(self.user.pk, self.user.role)
|
||||
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
result: User | None = self.bearer.authenticate(request, token)
|
||||
self.assertIsNone(result)
|
||||
@@ -0,0 +1,56 @@
|
||||
import uuid
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
|
||||
from ._helpers import _make_user
|
||||
|
||||
|
||||
class UserRoleChoicesTest(TestCase):
|
||||
def test_choices_count(self) -> None:
|
||||
self.assertEqual(len(UserRole.choices), 4)
|
||||
|
||||
def test_choice_values(self) -> None:
|
||||
values = {c[0] for c in UserRole.choices}
|
||||
self.assertEqual(
|
||||
values, {"admin", "experimenter", "approver", "viewer"}
|
||||
)
|
||||
|
||||
|
||||
class UserModelTest(TestCase):
|
||||
def test_default_role_is_viewer(self) -> None:
|
||||
user: User = _make_user()
|
||||
self.assertEqual(user.role, UserRole.VIEWER)
|
||||
|
||||
def test_role_properties(self) -> None:
|
||||
admin: User = _make_user(
|
||||
username="a", email="a@x.com", role=UserRole.ADMIN
|
||||
)
|
||||
self.assertTrue(admin.is_admin_role)
|
||||
self.assertFalse(admin.is_experimenter)
|
||||
self.assertFalse(admin.is_approver)
|
||||
self.assertFalse(admin.is_viewer)
|
||||
|
||||
exp: User = _make_user(
|
||||
username="e", email="e@x.com", role=UserRole.EXPERIMENTER
|
||||
)
|
||||
self.assertTrue(exp.is_experimenter)
|
||||
|
||||
appr: User = _make_user(
|
||||
username="ap", email="ap@x.com", role=UserRole.APPROVER
|
||||
)
|
||||
self.assertTrue(appr.is_approver)
|
||||
|
||||
viewer: User = _make_user(
|
||||
username="v", email="v@x.com", role=UserRole.VIEWER
|
||||
)
|
||||
self.assertTrue(viewer.is_viewer)
|
||||
|
||||
def test_uuid_primary_key(self) -> None:
|
||||
user: User = _make_user()
|
||||
self.assertIsInstance(user.pk, uuid.UUID)
|
||||
|
||||
def test_str_representation(self) -> None:
|
||||
user: User = _make_user(username="hello")
|
||||
self.assertEqual(str(user), "hello")
|
||||
@@ -0,0 +1,44 @@
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from apps.users.auth.bearer import require_admin, require_roles
|
||||
from apps.users.models import User, UserRole
|
||||
from config.errors import ForbiddenError
|
||||
|
||||
from ._helpers import _make_user
|
||||
|
||||
|
||||
class RequireRolesTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.admin: User = _make_user(
|
||||
username="rr_admin", email="rr_admin@x.com", role=UserRole.ADMIN
|
||||
)
|
||||
self.viewer: User = _make_user(
|
||||
username="rr_viewer", email="rr_viewer@x.com", role=UserRole.VIEWER
|
||||
)
|
||||
|
||||
def _make_request(self, user) -> WSGIRequest:
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
request.auth = user
|
||||
return request
|
||||
|
||||
def test_require_admin_passes(self) -> None:
|
||||
request: WSGIRequest = self._make_request(self.admin)
|
||||
result = require_admin(request)
|
||||
self.assertEqual(result, self.admin)
|
||||
|
||||
def test_require_admin_denies_viewer(self) -> None:
|
||||
request: WSGIRequest = self._make_request(self.viewer)
|
||||
with self.assertRaises(ForbiddenError):
|
||||
require_admin(request)
|
||||
|
||||
def test_require_roles_multiple(self) -> None:
|
||||
checker = require_roles("admin", "viewer")
|
||||
request: WSGIRequest = self._make_request(self.viewer)
|
||||
result = checker(request)
|
||||
self.assertEqual(result, self.viewer)
|
||||
|
||||
def test_require_roles_no_auth_raises(self) -> None:
|
||||
request: WSGIRequest = RequestFactory().get("/")
|
||||
with self.assertRaises(ForbiddenError):
|
||||
require_admin(request)
|
||||
@@ -0,0 +1,123 @@
|
||||
import uuid
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.selectors import (
|
||||
user_exists_with_email,
|
||||
user_exists_with_username,
|
||||
user_get_by_email,
|
||||
user_get_by_id,
|
||||
user_get_by_username,
|
||||
user_list,
|
||||
user_list_admins,
|
||||
user_list_approvers,
|
||||
user_list_by_role,
|
||||
user_list_experimenters,
|
||||
user_list_viewers,
|
||||
)
|
||||
|
||||
from ._helpers import _make_user
|
||||
|
||||
|
||||
class UserSelectorsTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.admin: User = _make_user(
|
||||
username="sel_admin",
|
||||
email="sel_admin@x.com",
|
||||
role=UserRole.ADMIN,
|
||||
)
|
||||
self.exp: User = _make_user(
|
||||
username="sel_exp",
|
||||
email="sel_exp@x.com",
|
||||
role=UserRole.EXPERIMENTER,
|
||||
)
|
||||
self.appr: User = _make_user(
|
||||
username="sel_appr",
|
||||
email="sel_appr@x.com",
|
||||
role=UserRole.APPROVER,
|
||||
)
|
||||
self.viewer: User = _make_user(
|
||||
username="sel_viewer",
|
||||
email="sel_viewer@x.com",
|
||||
role=UserRole.VIEWER,
|
||||
)
|
||||
|
||||
def test_get_by_id(self) -> None:
|
||||
found: User | None = user_get_by_id(str(self.admin.pk))
|
||||
self.assertEqual(found, self.admin)
|
||||
|
||||
def test_get_by_id_invalid_uuid(self) -> None:
|
||||
self.assertIsNone(user_get_by_id("not-a-uuid"))
|
||||
|
||||
def test_get_by_id_nonexistent(self) -> None:
|
||||
self.assertIsNone(user_get_by_id(str(uuid.uuid4())))
|
||||
|
||||
def test_get_by_username(self) -> None:
|
||||
found: User | None = user_get_by_username("sel_exp")
|
||||
self.assertEqual(found, self.exp)
|
||||
|
||||
def test_get_by_username_nonexistent(self) -> None:
|
||||
self.assertIsNone(user_get_by_username("ghost"))
|
||||
|
||||
def test_get_by_email(self) -> None:
|
||||
found: User | None = user_get_by_email("sel_appr@x.com")
|
||||
self.assertEqual(found, self.appr)
|
||||
|
||||
def test_list_all(self) -> None:
|
||||
qs: QuerySet[User] = user_list()
|
||||
self.assertEqual(qs.count(), 4)
|
||||
|
||||
def test_list_filter_by_role(self) -> None:
|
||||
qs: QuerySet[User] = user_list(role=UserRole.ADMIN)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
self.assertEqual(qs.first(), self.admin)
|
||||
|
||||
def test_list_filter_by_is_active(self) -> None:
|
||||
self.viewer.is_active = False
|
||||
self.viewer.save()
|
||||
qs: QuerySet[User] = user_list(is_active=True)
|
||||
self.assertEqual(qs.count(), 3)
|
||||
|
||||
def test_list_filter_by_search(self) -> None:
|
||||
qs: QuerySet[User] = user_list(search="sel_exp")
|
||||
self.assertEqual(qs.count(), 1)
|
||||
|
||||
def test_list_by_role(self) -> None:
|
||||
qs: QuerySet[User] = user_list_by_role(UserRole.APPROVER)
|
||||
self.assertEqual(qs.count(), 1)
|
||||
|
||||
def test_list_admins(self) -> None:
|
||||
self.assertEqual(user_list_admins().count(), 1)
|
||||
|
||||
def test_list_experimenters(self) -> None:
|
||||
self.assertEqual(user_list_experimenters().count(), 1)
|
||||
|
||||
def test_list_approvers(self) -> None:
|
||||
self.assertEqual(user_list_approvers().count(), 1)
|
||||
|
||||
def test_list_viewers(self) -> None:
|
||||
self.assertEqual(user_list_viewers().count(), 1)
|
||||
|
||||
def test_exists_with_username(self) -> None:
|
||||
self.assertTrue(user_exists_with_username("sel_admin"))
|
||||
self.assertFalse(user_exists_with_username("ghost"))
|
||||
|
||||
def test_exists_with_username_exclude(self) -> None:
|
||||
self.assertFalse(
|
||||
user_exists_with_username(
|
||||
"sel_admin", exclude_id=str(self.admin.pk)
|
||||
)
|
||||
)
|
||||
|
||||
def test_exists_with_email(self) -> None:
|
||||
self.assertTrue(user_exists_with_email("sel_viewer@x.com"))
|
||||
self.assertFalse(user_exists_with_email("ghost@x.com"))
|
||||
|
||||
def test_exists_with_email_exclude(self) -> None:
|
||||
self.assertFalse(
|
||||
user_exists_with_email(
|
||||
"sel_viewer@x.com", exclude_id=str(self.viewer.pk)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,128 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.users.models import User, UserRole
|
||||
from apps.users.services import (
|
||||
user_activate,
|
||||
user_assign_role,
|
||||
user_create,
|
||||
user_deactivate,
|
||||
user_delete,
|
||||
user_update,
|
||||
)
|
||||
|
||||
from ._helpers import _make_user
|
||||
|
||||
|
||||
class UserCreateServiceTest(TestCase):
|
||||
def test_create_with_defaults(self) -> None:
|
||||
user: User = user_create(
|
||||
username="svc_user",
|
||||
email="svc@lotty.local",
|
||||
password="pass1234",
|
||||
)
|
||||
self.assertEqual(user.role, UserRole.VIEWER)
|
||||
self.assertTrue(user.check_password("pass1234"))
|
||||
self.assertTrue(user.is_active)
|
||||
|
||||
def test_create_with_role(self) -> None:
|
||||
user: User = user_create(
|
||||
username="admin_svc",
|
||||
email="admin_svc@lotty.local",
|
||||
password="pass1234",
|
||||
role=UserRole.ADMIN,
|
||||
)
|
||||
self.assertEqual(user.role, UserRole.ADMIN)
|
||||
|
||||
def test_create_without_password(self) -> None:
|
||||
user: User = user_create(
|
||||
username="nopw",
|
||||
email="nopw@lotty.local",
|
||||
)
|
||||
self.assertFalse(user.has_usable_password())
|
||||
|
||||
def test_create_with_extra_fields(self) -> None:
|
||||
user: User = user_create(
|
||||
username="extra",
|
||||
email="extra@lotty.local",
|
||||
password="pass1234",
|
||||
first_name="First",
|
||||
last_name="Last",
|
||||
)
|
||||
self.assertEqual(user.first_name, "First")
|
||||
self.assertEqual(user.last_name, "Last")
|
||||
|
||||
|
||||
class UserUpdateServiceTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user: User = _make_user()
|
||||
|
||||
def test_update_username(self) -> None:
|
||||
updated: User = user_update(user=self.user, username="newname")
|
||||
self.assertEqual(updated.username, "newname")
|
||||
|
||||
def test_update_email(self) -> None:
|
||||
updated: User = user_update(user=self.user, email="new@lotty.local")
|
||||
self.assertEqual(updated.email, "new@lotty.local")
|
||||
|
||||
def test_update_role(self) -> None:
|
||||
updated: User = user_update(user=self.user, role=UserRole.ADMIN)
|
||||
self.assertEqual(updated.role, UserRole.ADMIN)
|
||||
|
||||
def test_update_password(self) -> None:
|
||||
updated: User = user_update(user=self.user, password="newpass99")
|
||||
self.assertTrue(updated.check_password("newpass99"))
|
||||
|
||||
def test_update_is_active(self) -> None:
|
||||
updated: User = user_update(user=self.user, is_active=False)
|
||||
self.assertFalse(updated.is_active)
|
||||
|
||||
def test_partial_update_leaves_other_fields(self) -> None:
|
||||
original_email = self.user.email
|
||||
updated: User = user_update(user=self.user, username="changed")
|
||||
self.assertEqual(updated.email, original_email)
|
||||
|
||||
def test_update_names(self) -> None:
|
||||
updated: User = user_update(
|
||||
user=self.user, first_name="Jane", last_name="Doe"
|
||||
)
|
||||
self.assertEqual(updated.first_name, "Jane")
|
||||
self.assertEqual(updated.last_name, "Doe")
|
||||
|
||||
|
||||
class UserAssignRoleServiceTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user: User = _make_user()
|
||||
|
||||
def test_assign_valid_role(self) -> None:
|
||||
updated: User = user_assign_role(
|
||||
user=self.user, role=UserRole.EXPERIMENTER
|
||||
)
|
||||
self.assertEqual(updated.role, UserRole.EXPERIMENTER)
|
||||
|
||||
def test_assign_invalid_role_raises(self) -> None:
|
||||
with self.assertRaises(ValidationError):
|
||||
user_assign_role(user=self.user, role="superadmin")
|
||||
|
||||
|
||||
class UserDeleteServiceTest(TestCase):
|
||||
def test_hard_delete(self) -> None:
|
||||
user: User = _make_user()
|
||||
pk = user.pk
|
||||
user_delete(user=user)
|
||||
self.assertFalse(User.objects.filter(pk=pk).exists())
|
||||
|
||||
|
||||
class UserActivateDeactivateServiceTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user: User = _make_user()
|
||||
|
||||
def test_deactivate(self) -> None:
|
||||
updated: User = user_deactivate(user=self.user)
|
||||
self.assertFalse(updated.is_active)
|
||||
|
||||
def test_activate(self) -> None:
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
updated: User = user_activate(user=self.user)
|
||||
self.assertTrue(updated.is_active)
|
||||
Reference in New Issue
Block a user