Skip to content

Commit

Permalink
test: adds testing to backend for availability (#116)
Browse files Browse the repository at this point in the history
* chore: cleans up linting

* fix: puts back deleted comment

* refact: adds folders for testing structure

* feat: sets up testing database

* chore: notes needed documentation

* chore: cleans up linting

* fix: removes unnecessary test database

* refact: changes view name to be more descriptive

* test: converts view test file to end to end test for availability endpoint

* test: adds availability model unit test

* chore: adds init files to run tests correctly

* test: adds test coverage and more comments

* chore: moves file to correct folder and skips tests

* chore: moves to models unit test folder

* chore: cleans up linting

* test: adds start after end time check

* chore: cleans up linting

* test: create init to run view tests

* test: adds view unit tests

* feat: adds validation for availability start and end times

* test: all tests passing

* chore: resolves merge conflicts

* chore: resolving merge conflict

* docs: adds basic documentation for testing

* chore: cleans up linting

* fix: puts back deleted comment

* refact: adds folders for testing structure

* feat: sets up testing database

* chore: notes needed documentation

* chore: cleans up linting

* fix: removes unnecessary test database

* refact: changes view name to be more descriptive

* test: converts view test file to end to end test for availability endpoint

* test: adds availability model unit test

* chore: adds init files to run tests correctly

* test: adds test coverage and more comments

* chore: moves file to correct folder and skips tests

* chore: moves to models unit test folder

* chore: cleans up linting

* test: adds start after end time check

* chore: cleans up linting

* test: create init to run view tests

* test: adds view unit tests

* feat: adds validation for availability start and end times

* test: all tests passing

* docs: adds basic documentation for testing

* refact: reverting this to a try/catch exception setup

* chore: cleans up merge

* chore: cleans up linting

* fix: puts back deleted comment

* refact: adds folders for testing structure

* feat: sets up testing database

* chore: notes needed documentation

* chore: cleans up linting

* fix: removes unnecessary test database

* test: converts view test file to end to end test for availability endpoint

* test: adds availability model unit test

* test: adds test coverage and more comments

* chore: moves file to correct folder and skips tests

* chore: moves to models unit test folder

* chore: cleans up linting

* test: adds view unit tests

* feat: adds validation for availability start and end times

* test: all tests passing

* chore: resolves merge conflicts

* chore: resolving merge conflict

* docs: adds basic documentation for testing

* chore: cleans up merge

* fix: merge conflicts

* fix: remove duplicate function

* chore: cleans up linting

* build: adds coverage.py and related files

* fix: reverts to try/exception

* chore: adds todo comments

* docs: adds test coverage information

* chore: cleans up linting

* test: adds check for end time validation message

* test: add str test for availability

* fix: remove exemption details

---------

Co-authored-by: GitLukeW <[email protected]>
  • Loading branch information
esparr and GitLukeW authored Jul 25, 2023
1 parent 451b248 commit 8b97923
Show file tree
Hide file tree
Showing 17 changed files with 626 additions and 153 deletions.
28 changes: 28 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[run]
# Measure coverage for the 'team_production_system' directory only
source = team_production_system

# Omit test files and certain configuration files from coverage
omit =
team_production_system/tests/*
team_production_system/apps.py
team_production_system/manage.py
team_production_system/__init__.py
team_production_system/asgi.py
team_production_system/wsgi.py
team_production_system/admin.py
team_production_system/urls.py
team_production_system/migrations/*

[report]
# Exclude the same files from the coverage report
omit =
team_production_system/tests/*
team_production_system/apps.py
team_production_system/manage.py
team_production_system/__init__.py
team_production_system/asgi.py
team_production_system/wsgi.py
team_production_system/admin.py
team_production_system/urls.py
team_production_system/migrations/*
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ boto3 = "*"
sentry-sdk = {extras = ["django"], version = "*"}
pytz = "*"
flake8 = "*"
coverage = "*"

[dev-packages]
autopep8 = "*"
Expand Down
276 changes: 166 additions & 110 deletions Pipfile.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ [email protected]

3. Save the .env file.

# Testing

For testing this app, we are using [Django Test Case](https://docs.djangoproject.com/en/4.2/topics/testing/overview/) and [Django REST Framework API Test Case](https://www.django-rest-framework.org/api-guide/testing/#api-test-cases) along with [coverage.py](https://coverage.readthedocs.io/en/7.2.7/index.html) for test coverage reporting.

To run tests:
```python manage.py test```

To skip a test that isn't finished, add the following before the test class:
```@unittest.skip("Test file is not ready yet")```

To run coverage for test:
```coverage run manage.py test```

After you run tests you can get the report in command-line by running:
```coverage report```

For an interactive html report, run:
```coverage html```

Then in the `htmlcov` folder of the project, open the file `index.html` in a browser. Here you can see an indepth analysis of coverage and what lines need testing. Click available links to view specific file coverage data.

Here is some helpful information on testing in Django and Django REST Framework: https://www.rootstrap.com/blog/testing-in-django-django-rest-basics-useful-tools-good-practices


# API Reference

API URL - https://team-production-system.onrender.com
Expand Down
4 changes: 3 additions & 1 deletion team_production_system/custom_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

class IsMentorMentee(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.pk == obj.mentor.pk or request.user.pk == obj.mentee.pk
is_mentor = request.user.pk == obj.mentor.pk
is_mentee = request.user.pk == obj.mentee.pk
return is_mentor or is_mentee


class NotificationSettingsPermission(BasePermission):
Expand Down
31 changes: 27 additions & 4 deletions team_production_system/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class AvailabilitySerializer(serializers.ModelSerializer):
class Meta:
model = Availability
fields = ('pk', 'mentor', 'start_time', 'end_time',)
read_only_fields = ('mentor',)
read_only_fields = ('mentor', 'pk',)

def create(self, validated_data):
mentor = Mentor.objects.select_related('user').get(
Expand All @@ -56,6 +56,29 @@ def create(self, validated_data):
raise serializers.ValidationError(
"Input overlaps with existing availability.")

def validate(self, data):
"""
Check that the start_time is before the end_time.
"""
start_time = data['start_time']
end_time = data['end_time']
if start_time >= end_time:
raise serializers.ValidationError(
'End time must be after start time.')
return data

def validate_end_time(self, value):
"""
Check that the end_time is in the future.
"""
if value <= timezone.now():
raise serializers.ValidationError(
'End time must be in the future.'
)
return value

# TODO: Add validation for start times


# Serializer for the mentor profile
class MentorProfileSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -142,9 +165,9 @@ class SessionSerializer(serializers.ModelSerializer):
class Meta:
model = Session
fields = ('pk', 'mentor_first_name', 'mentor_last_name',
'mentor_availability', 'mentee', 'mentee_first_name',
'mentee_last_name', 'start_time', 'end_time', 'status',
'session_length')
'mentor_availability', 'mentee', 'mentee_first_name',
'mentee_last_name', 'start_time', 'end_time', 'status',
'session_length',)
read_only_fields = ('mentor', 'mentor_first_name', 'mentor_last_name',
'mentee', 'mentee_first_name', 'mentee_last_name')

Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from ...models import Availability, Mentor, CustomUser
from ...serializers import AvailabilitySerializer


class AvailabilityListCreateTestCase(APITestCase):
def setUp(self):
# Create a Mentor object
self.user = CustomUser.objects.create_user(
username='mentor',
email='[email protected]',
password='password'
)
self.mentor = Mentor.objects.create(user=self.user)

# Create two Availability objects associated with the Mentor
self.availability1 = Availability.objects.create(
mentor=self.mentor,
start_time=timezone.now(),
end_time=timezone.now() + timezone.timedelta(hours=1)
)
self.availability2 = Availability.objects.create(
mentor=self.mentor,
start_time=timezone.now() + timezone.timedelta(days=1),
end_time=timezone.now() + timezone.timedelta(days=1, hours=1)
)
# Create a Client
self.client = APIClient()

def test_get_availability_list_without_authentication(self):
"""
Test that a GET request to retrieve the list of Availabilities without
authentication returns a status code of 401 UNAUTHORIZED.
"""
# Send a GET request to retrieve the list of Availabilities
url = reverse('availability')
response = self.client.get(url, format='json')

# Check that the response has a status code of 401 UNAUTHORIZED
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_get_availability_list(self):
"""
Test that a GET request to retrieve the list of Availabilities returns
a status code of 200 OK and the correct serialized data.
"""

# Authenticate as the Mentor
self.client.force_authenticate(user=self.user)

# Send a GET request to retrieve the list of Availabilities
url = reverse('availability')
response = self.client.get(url, format='json')

# Check that the response has a status code of 200 OK
self.assertEqual(response.status_code, status.HTTP_200_OK)

# Check that the response data matches the serialized Availability obj
availabilities = Availability.objects.filter(
mentor=self.mentor,
end_time__gte=timezone.now()
).select_related('mentor__user')
serializer = AvailabilitySerializer(availabilities, many=True)
self.assertEqual(response.data, serializer.data)
self.assertEqual(len(response.data), 2)

def test_create_availability(self):
"""
Test that a POST request to create a new Availability with valid data
returns a status code of 201 CREATED.
"""

# Authenticate as the Mentor
self.client.force_authenticate(user=self.user)

# Send a POST request to create a new Availability
data = {
'mentor': self.mentor.pk,
'start_time': timezone.now() + timezone.timedelta(days=2),
'end_time': timezone.now() + timezone.timedelta(days=2, hours=1)
}
url = reverse('availability')
response = self.client.post(url, data, format='json')

# Check that the response has a status code of 201 CREATED
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

# Check that the created Availability object has the correct attributes
availability = Availability.objects.last()
self.assertEqual(availability.start_time, data['start_time'])
self.assertEqual(availability.end_time, data['end_time'])
self.assertEqual(availability.mentor, self.mentor)

def test_create_availability_with_duplicate_start_time(self):
"""
Test that a POST request to create a new Availability with a start time
that has already been used returns a status code of 400 BAD REQUEST
and an error message.
"""
# Authenticate as the Mentor
self.client.force_authenticate(user=self.user)

'''Send a POST request to create a new Availability with
a start time that has already been used'''
data = {
'mentor': self.mentor.pk,
'start_time': self.availability1.start_time,
'end_time': timezone.now() + timezone.timedelta(hours=2)
}
url = reverse('availability')
response = self.client.post(url, data, format='json')

# Check that the response has a status code of 400 BAD REQUEST
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

# Check that the response data contains an error message
self.assertEqual(response.data[0],
'Input overlaps with existing availability.')

def test_create_availability_with_duplicate_start_and_end_times(self):
"""
Test that a POST request to create a new Availability with a start time
and end time that have already been used returns a status code of
400 BAD REQUEST and an error message.
"""
# Authenticate as the Mentor
self.client.force_authenticate(user=self.user)

# Send a POST request to create a new Availability with duplicate
# start and end times
data = {
'mentor': self.mentor.pk,
'start_time': self.availability1.start_time,
'end_time': self.availability1.end_time
}
url = reverse('availability')
response = self.client.post(url, data, format='json')

# Check that the response has a status code of 400 BAD REQUEST
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

# Check that the response data contains an error message
self.assertEqual(response.data[0],
'Input overlaps with existing availability.')

def test_create_availability_with_end_time_before_start_time(self):
"""
Test that a POST request to create a new Availability with a start time
after the end time returns a status code of 400 BAD REQUEST.
"""
# Authenticate as the Mentor
self.client.force_authenticate(user=self.user)

availability_data = {
'start_time': '2022-01-01T14:00:00Z',
'end_time': '2022-01-01T12:00:00Z',
'mentor': self.mentor.pk
}
response = self.client.post('/availability/',
availability_data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(str(response.data['end_time'][0]),
'End time must be in the future.')
self.assertEqual(Availability.objects.count(), 2)
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from django.test import TestCase
from django.utils import timezone
from datetime import datetime, timedelta
from .models import CustomUser, Mentor, Mentee, Availability
from datetime import timedelta
from ..models import CustomUser, Mentor, Availability
import unittest


@unittest.skip("Test file is not ready yet")
class AvailabilityTestCase(TestCase):
def setUp(self):
# Set up a mentor and an availability for testing
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.test import TestCase
from datetime import datetime, timedelta, timezone
from ....models import Availability, Mentor, CustomUser


class AvailabilityTestCase(TestCase):
def setUp(self):
# Create a User and Mentor object
self.user = CustomUser.objects.create_user(
username='mentor',
email='[email protected]',
password='password'
)
self.mentor = Mentor.objects.create(user=self.user)

def test_create_availability(self):
# Set start and end times
start_time = datetime.now(timezone.utc)
end_time = start_time + timedelta(hours=1)

# Create an Availability object
availability = Availability(
mentor=self.mentor,
start_time=start_time,
end_time=end_time)

# Save an Availability object associated with the Mentor
availability.save()

# Retrieve the saved Availability object
saved_availability = Availability.objects.first()

# Check that the object was saved correctly
self.assertEqual(saved_availability.start_time, start_time)
self.assertEqual(saved_availability.end_time, end_time)
self.assertEqual(saved_availability.mentor, self.mentor)
self.assertEqual(str(saved_availability), f"{self.mentor} is available from {start_time} to {end_time}.")

# TODO: Add test for self reference
Empty file.
Loading

0 comments on commit 8b97923

Please sign in to comment.