Skip to content

Commit

Permalink
Merge pull request #28 from SELab-2/test-authentication
Browse files Browse the repository at this point in the history
Authentication tests
  • Loading branch information
EwoutV authored Mar 3, 2024
2 parents 72e870f + 6c9a15d commit 1621f1e
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 19 deletions.
Binary file added backend/.coverage
Binary file not shown.
12 changes: 6 additions & 6 deletions backend/authentication/fixtures/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
email: [email protected]
first_name: John
last_name: Doe
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.690556+00:00
faculties:
- Wetenschappen
Expand All @@ -18,7 +18,7 @@
email: [email protected]
first_name: Tom
last_name: Boonen
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.686541+00:00
faculties:
- Psychologie_PedagogischeWetenschappen
Expand All @@ -30,7 +30,7 @@
email: [email protected]
first_name: Peter
last_name: Sagan
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.689543+00:00
faculties:
- Psychologie_PedagogischeWetenschappen
Expand All @@ -42,7 +42,7 @@
email: [email protected]
first_name: Bartje
last_name: Verhaege
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.691565+00:00
faculties:
- Geneeskunde_Gezondheidswetenschappen
Expand All @@ -54,7 +54,7 @@
email: [email protected]
first_name: Bart
last_name: Simpson
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.687541+00:00
faculties:
- Wetenschappen
Expand All @@ -66,7 +66,7 @@
email: [email protected]
first_name: Kim
last_name: Clijsters
last_enrolled: 1
last_enrolled: 2023
create_time: 2024-02-29 20:35:45.688545+00:00
faculties:
- Psychologie_PedagogischeWetenschappen
3 changes: 1 addition & 2 deletions backend/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,12 @@ class User(AbstractBaseUser):
)

create_time = DateTimeField(
auto_now=True
auto_now_add=True
)

"""Model settings"""
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = []


class Faculty(models.Model):
Expand Down
11 changes: 2 additions & 9 deletions backend/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,13 @@ def validate(self, data):
'refresh': str(RefreshToken.for_user(user))
}


class UserSerializer(ModelSerializer):
"""Serializer for the user model
This serializer validates the user fields for creation and updating.
"""
id = CharField()
username = CharField()
email = EmailField()
email = EmailField()

faculties = HyperlinkedRelatedField(
many=True,
Expand All @@ -90,13 +89,7 @@ class UserSerializer(ModelSerializer):

class Meta:
model = User
fields = [
'id', 'username', 'email',
'first_name', 'last_name',
'faculties',
'last_enrolled', 'last_login', 'create_time',
'notifications'
]
fields = '__all__'

def get_or_create(self, validated_data: dict) -> User:
"""Create or fetch the user based on the validated data."""
Expand Down
194 changes: 194 additions & 0 deletions backend/authentication/tests/test_authentication_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
from django.test import TestCase

from unittest.mock import patch, Mock

from rest_framework_simplejwt.tokens import RefreshToken

from authentication.cas.client import client
from authentication.serializers import CASTokenObtainSerializer, UserSerializer
from authentication.signals import user_created, user_login


TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6'
WRONG_TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5'

ID = '1234'
USERNAME = 'ddickwd'
EMAIL = '[email protected]'
FIRST_NAME = 'Dummy'
LAST_NAME = 'McDickwad'


class UserSerializerModelTests(TestCase):

def test_invalid_email_makes_user_serializer_invalid(self):
"""
The is_valid() method of a UserSerializer whose supplied User's email is not
formatted as an email address should return False.
"""
user = UserSerializer(data={
'id': ID,
'username': USERNAME,
'email': 'dummy',
'first_name': FIRST_NAME,
'last_name': LAST_NAME,
})
user2 = UserSerializer(data={
'id': ID,
'username': USERNAME,
'email': 'dummy@dummy',
'first_name': FIRST_NAME,
'last_name': LAST_NAME,
})
user3 = UserSerializer(data={
'id': ID,
'username': USERNAME,
'email': 21,
'first_name': FIRST_NAME,
'last_name': LAST_NAME,
})
self.assertFalse(user.is_valid())
self.assertFalse(user2.is_valid())
self.assertFalse(user3.is_valid())

def test_valid_email_makes_valid_serializer(self):
"""
When the serializer is provided with a valid email, the serializer becomes valid,
thus the is_valid() method returns True.
"""
user = UserSerializer(data={
'id': ID,
'username': USERNAME,
'email': EMAIL,
'first_name': FIRST_NAME,
'last_name': LAST_NAME,
})
self.assertTrue(user.is_valid())


def customize_data(ugent_id, uid, mail):

class Response:
__slots__ = ('error', 'data')

def __init__(self):
self.error = None
self.data = {}

def service_validate(
ticket=None,
service_url=None,
headers=None,):
response = Response()
if ticket != TICKET:
response.error = 'This is an error'
else:
response.data['attributes'] = {
'ugentID': ugent_id,
'uid': uid,
'mail': mail,
'givenname': FIRST_NAME,
'surname': LAST_NAME,
'faculty': 'Sciences',
'lastenrolled': '2023 - 2024',
'lastlogin': '',
'createtime': ''
}
return response

return service_validate


class SerializersTests(TestCase):
def test_wrong_length_ticket_generates_error(self):
"""
When the provided ticket has the wrong length, a ValidationError should be raised
when validating the serializer.
"""
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': 'ST'
})
self.assertFalse(serializer.is_valid())

@patch.object(client,
'perform_service_validate',
customize_data(ID, USERNAME, EMAIL))
def test_wrong_ticket_generates_error(self):
"""
When the wrong ticket is provided, a ValidationError should be raised when trying to validate
the serializer.
"""
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': WRONG_TICKET
})
self.assertFalse(serializer.is_valid())

@patch.object(client,
'perform_service_validate',
customize_data(ID, USERNAME, "dummy@dummy"))
def test_wrong_user_arguments_generate_error(self):
"""
If the user arguments returned by CAS are not valid, then a ValidationError
should be raised when validating the serializer.
"""
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': TICKET
})
self.assertFalse(serializer.is_valid())

@patch.object(client,
'perform_service_validate',
customize_data(ID, USERNAME, EMAIL))
def test_new_user_activates_user_created_signal(self):
"""
If the authenticated user is new to the app, then the user_created signal should
be sent when trying to validate the token."""

mock = Mock()
user_created.connect(mock, dispatch_uid="STDsAllAround")
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': TICKET
})
# this next line triggers the retrieval of User information and logs in the user
serializer.is_valid()
self.assertEquals(mock.call_count, 1)

@patch.object(client,
'perform_service_validate',
customize_data(ID, USERNAME, EMAIL))
def test_old_user_does_not_activate_user_created_signal(self):
"""
If the authenticated user is new to the app, then the user_created signal should
be sent when trying to validate the token."""

mock = Mock()
user_created.connect(mock, dispatch_uid="STDsAllAround")
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': TICKET
})
# this next line triggers the retrieval of User information and logs in the user
serializer.is_valid()
self.assertEquals(mock.call_count, 0)

@patch.object(client,
'perform_service_validate',
customize_data(ID, USERNAME, EMAIL))
def test_login_signal(self):
"""
When the token is correct and all user data is correct, while trying to validate
the token, then the user_login signal should be sent.
"""
mock = Mock()
user_login.connect(mock, dispatch_uid="STDsAllAround")
serializer = CASTokenObtainSerializer(data={
'token': RefreshToken(),
'ticket': TICKET
})
# this next line triggers the retrieval of User information and logs in the user
serializer.is_valid()
self.assertEquals(mock.call_count, 1)
100 changes: 100 additions & 0 deletions backend/authentication/tests/test_authentication_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from rest_framework_simplejwt.tokens import AccessToken
from authentication.models import User
from ypovoli import settings


class TestWhomAmIView(APITestCase):
def setUp(self):
"""Create a user and generate a token for that user"""
user_data = {
'id': '1234',
'username': 'ddickwd',
'email': '[email protected]',
'first_name': 'dummy',
'last_name': 'McDickwad',
}
self.user = User.objects.create(**user_data)
access_token = AccessToken().for_user(self.user)
self.token = f'Bearer {access_token}'

def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self):
"""
WhoAmIView should return the User info when requested if User
exists in database and token is supplied.
"""
self.client.credentials(HTTP_AUTHORIZATION=self.token)

response = self.client.get(reverse('auth.whoami'))
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(content['id'], self.user.id)

def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(self):
"""
WhoAmIView should return that the user was not found if
authenticated user was deleted from the database.
"""
self.user.delete()
self.client.credentials(HTTP_AUTHORIZATION=self.token)

response = self.client.get(reverse('auth.whoami'))
self.assertEqual(response.status_code, 405)

def test_who_am_i_view_returns_401_when_not_authenticated(self):
"""WhoAmIView should return a 401 status code when the user is not authenticated"""
response = self.client.get(reverse('auth.whoami'))
self.assertEqual(response.status_code, 401)


class TestLogoutView(APITestCase):
def setUp(self):
user_data = {
'id': '1234',
'username': 'ddickwd',
'email': '[email protected]',
'first_name': 'dummy',
'last_name': 'McDickwad',
}
self.user = User.objects.create(**user_data)

def test_logout_view_authenticated_logout_url(self):
"""LogoutView should return a logout url redirect if authenticated user sends a post request."""
access_token = AccessToken().for_user(self.user)
self.token = f'Bearer {access_token}'
self.client.credentials(HTTP_AUTHORIZATION=self.token)
response = self.client.post(reverse('auth.logout'))
self.assertEqual(response.status_code, 302)
url = '{server_url}/logout?service={service_url}'.format(
server_url=settings.CAS_ENDPOINT,
service_url=settings.API_ENDPOINT
)
self.assertEqual(response['Location'], url)

def test_logout_view_not_authenticated_logout_url(self):
"""LogoutView should return a 401 error when trying to access it while not authenticated."""
response = self.client.post(reverse('auth.logout'))
self.assertEqual(response.status_code, 401)


class TestLoginView(APITestCase):
def test_login_view_returns_login_url(self):
"""LoginView should return a login url redirect if a post request is sent."""
response = self.client.get(reverse('auth.login'))
self.assertEqual(response.status_code, 302)
url = '{server_url}/login?service={service_url}'.format(
server_url=settings.CAS_ENDPOINT,
service_url=settings.CAS_RESPONSE
)
self.assertEqual(response['Location'], url)


class TestTokenEchoView(APITestCase):
def test_token_echo_echoes_token(self):
"""TokenEchoView should echo the User's current token"""
ticket = 'This is a ticket.'
response = self.client.get(reverse('auth.echo'), data={'ticket': ticket})
content = response.rendered_content.decode('utf-8').strip('"')
self.assertEqual(content, ticket)
Loading

0 comments on commit 1621f1e

Please sign in to comment.