Skip to content

Commit

Permalink
finish user tests
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Oct 21, 2023
1 parent c0637c3 commit b90df7a
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 44 deletions.
79 changes: 73 additions & 6 deletions codeforlife/tests/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@
from unittest.mock import patch

from pyotp import TOTP
from django.urls import reverse
from django.utils import timezone
from django.db.models import Model
from rest_framework.test import APITestCase as _APITestCase
from rest_framework.test import APIClient as _APIClient
from rest_framework.serializers import BaseSerializer
from rest_framework.response import Response

from ..user.models import User, AuthFactor


AnySerializer = t.TypeVar("AnySerializer", bound=BaseSerializer)
AnyModel = t.TypeVar("AnyModel", bound=Model)


class APIClient(_APIClient):
StatusCodeAssertion = t.Optional[t.Union[int, t.Callable[[int], bool]]]

@staticmethod
def status_code_is_ok(status_code: int):
return 200 <= status_code < 300

def generic(
self,
method,
Expand All @@ -29,24 +41,22 @@ def generic(
# Use a custom kwarg to handle the common case of checking the
# response's status code.
if status_code_assertion is None:
status_code_assertion = (
lambda status_code: 200 <= status_code <= 299
)
status_code_assertion = self.status_code_is_ok
elif isinstance(status_code_assertion, int):
expected_status_code = status_code_assertion
status_code_assertion = (
lambda status_code: status_code == expected_status_code
)
assert status_code_assertion(
wsgi_response.status_code
), f"Response has error status code: {wsgi_response.status_code}"
), f"Unexpected status code: {wsgi_response.status_code}."

return wsgi_response

def login(self, **credentials):
assert super().login(
**credentials
), f"Failed to login with credentials: {credentials}"
), f"Failed to login with credentials: {credentials}."

user = User.objects.get(session=self.session.session_key)

Expand All @@ -58,12 +68,69 @@ def login(self, **credentials):
with patch.object(timezone, "now", return_value=now):
assert super().login(
otp=otp
), f'Failed to login with OTP "{otp}" at {now}'
), f'Failed to login with OTP "{otp}" at {now}.'

assert user.is_authenticated, "Failed to authenticate user."

return user

@staticmethod
def assert_data_equals_model(
data: t.Dict[str, t.Any],
model: AnyModel,
serializer_class: t.Type[AnySerializer],
):
assert (
data == serializer_class(model).data
), "Data does not equal serialized model."

def retrieve(
self,
basename: str,
model: AnyModel,
serializer_class: t.Type[AnySerializer],
status_code_assertion: StatusCodeAssertion = None,
**kwargs,
):
response: Response = self.get(
reverse(f"{basename}-detail", kwargs={"pk": model.pk}),
status_code_assertion=status_code_assertion,
**kwargs,
)

if self.status_code_is_ok(response.status_code):
self.assert_data_equals_model(
response.json(),
model,
serializer_class,
)

return response

def list(
self,
basename: str,
models: t.Iterable[AnyModel],
serializer_class: t.Type[AnySerializer],
status_code_assertion: StatusCodeAssertion = None,
**kwargs,
):
response: Response = self.get(
reverse(f"{basename}-list"),
status_code_assertion=status_code_assertion,
**kwargs,
)

if self.status_code_is_ok(response.status_code):
for data, model in zip(response.json()["data"], models):
self.assert_data_equals_model(
data,
model,
serializer_class,
)

return response


class APITestCase(_APITestCase):
client: APIClient
Expand Down
124 changes: 86 additions & 38 deletions codeforlife/user/tests/views/test_user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.urls import reverse
import typing as t

from django.db.models.query import QuerySet
from rest_framework import status

Expand All @@ -9,25 +10,11 @@

class TestUserViewSet(APITestCase):
"""
Naming convention:
test_{action}__{user_type}__{other_user_type}__{same_school}
Base naming convention:
test_{action}
action: This view set action.
https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
user_type: The type of user that is making the request. Options:
- teacher: A teacher.
- student: A student.
other_user_type: The type of user whose data is being requested. Options:
- self: User's own data.
- teacher: Another teacher's data.
- student: Another student's data.
same_school: A flag for if the other user is from the same school. Options:
- same_school: The other user is from the same school.
- not_same_school: The other user is not from the same school.
"""

# TODO: replace this setup with data fixtures.
Expand Down Expand Up @@ -91,19 +78,6 @@ def _login_student(self):
assert user.student.class_field.teacher.school
return user

def _retrieve_user(
self,
user: User,
status_code_assertion: APIClient.StatusCodeAssertion = None,
):
response = self.client.get(
reverse("user-detail", kwargs={"pk": user.id}),
status_code_assertion=status_code_assertion,
)
if 200 <= response.status_code < 300:
assert response.json() == UserSerializer(user).data
return response

def _get_other_user(
self,
user: User,
Expand Down Expand Up @@ -136,6 +110,36 @@ def _get_other_user(

return other_user

"""
Retrieve naming convention:
test_retrieve__{user_type}__{other_user_type}__{same_school}
user_type: The type of user that is making the request. Options:
- teacher: A teacher.
- student: A student.
other_user_type: The type of user whose data is being requested. Options:
- self: User's own data.
- teacher: Another teacher's data.
- student: Another student's data.
same_school: A flag for if the other user is from the same school. Options:
- same_school: The other user is from the same school.
- not_same_school: The other user is not from the same school.
"""

def _retrieve_user(
self,
user: User,
status_code_assertion: APIClient.StatusCodeAssertion = None,
):
return self.client.retrieve(
"user",
user,
UserSerializer,
status_code_assertion,
)

def test_retrieve__teacher__self(self):
"""
Teacher can retrieve their own user data.
Expand All @@ -152,7 +156,7 @@ def test_retrieve__student__self(self):
user = self._login_student()
self._retrieve_user(user)

def test_retrieve__teacher__not_self__same_school__teacher(self):
def test_retrieve__teacher__teacher__same_school(self):
"""
Teacher can retrieve another teacher from the same school.
"""
Expand All @@ -170,7 +174,7 @@ def test_retrieve__teacher__not_self__same_school__teacher(self):

self._retrieve_user(other_user)

def test_retrieve__teacher__not_self__same_school__student(self):
def test_retrieve__teacher__student__same_school(self):
"""
Teacher can retrieve a student from the same school.
"""
Expand All @@ -188,7 +192,7 @@ def test_retrieve__teacher__not_self__same_school__student(self):

self._retrieve_user(other_user)

def test_retrieve__student__not_self__same_school__teacher(self):
def test_retrieve__student__teacher__same_school(self):
"""
Student can not retrieve a teacher from the same school.
"""
Expand All @@ -209,7 +213,7 @@ def test_retrieve__student__not_self__same_school__teacher(self):
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

def test_retrieve__student__not_self__same_school__student(self):
def test_retrieve__student__student__same_school(self):
"""
Student can not retrieve another student from the same school.
"""
Expand All @@ -230,7 +234,7 @@ def test_retrieve__student__not_self__same_school__student(self):
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

def test_retrieve__teacher__not_self__not_same_school__teacher(self):
def test_retrieve__teacher__teacher__not_same_school(self):
"""
Teacher can not retrieve another teacher from another school.
"""
Expand All @@ -251,7 +255,7 @@ def test_retrieve__teacher__not_self__not_same_school__teacher(self):
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

def test_retrieve__teacher__not_self__not_same_school__student(self):
def test_retrieve__teacher__student__not_same_school(self):
"""
Teacher can not retrieve a student from another school.
"""
Expand All @@ -272,7 +276,7 @@ def test_retrieve__teacher__not_self__not_same_school__student(self):
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

def test_retrieve__student__not_self__not_same_school__teacher(self):
def test_retrieve__student__teacher__not_same_school(self):
"""
Student can not retrieve a teacher from another school.
"""
Expand All @@ -293,7 +297,7 @@ def test_retrieve__student__not_self__not_same_school__teacher(self):
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

def test_retrieve__student__not_self__not_same_school__student(self):
def test_retrieve__student__student__not_same_school(self):
"""
Student can not retrieve another student from another school.
"""
Expand All @@ -313,3 +317,47 @@ def test_retrieve__student__not_self__not_same_school__student(self):
other_user,
status_code_assertion=status.HTTP_404_NOT_FOUND,
)

"""
List naming convention:
test_list__{user_type}
user_type: The type of user that is making the request. Options:
- teacher: A teacher.
- student: A student.
"""

def _list_users(
self,
users: t.Iterable[User],
status_code_assertion: APIClient.StatusCodeAssertion = None,
):
return self.client.list(
"user",
users,
UserSerializer,
status_code_assertion,
)

def test_list__teacher(self):
"""
Teacher can list all the users in the same school.
"""

user = self._login_teacher()

self._list_users(
User.objects.filter(new_teacher__school=user.teacher.school)
| User.objects.filter(
new_student__class_field__teacher__school=user.teacher.school
)
)

def test_list__student(self):
"""
Student can list only themselves.
"""

user = self._login_student()

self._list_users([user])

0 comments on commit b90df7a

Please sign in to comment.