diff --git a/backend/media/users/2/workout.exe b/backend/media/users/2/workout.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout.jpg b/backend/media/users/2/workout.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_0VoSyYu.exe b/backend/media/users/2/workout_0VoSyYu.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_0VoSyYu.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_K5ZHqrC.jpg b/backend/media/users/2/workout_K5ZHqrC.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_K5ZHqrC.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_KdxpL6y.exe b/backend/media/users/2/workout_KdxpL6y.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_KdxpL6y.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_UiKNdnZ.jpg b/backend/media/users/2/workout_UiKNdnZ.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_UiKNdnZ.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_Ybf0O0e.exe b/backend/media/users/2/workout_Ybf0O0e.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_Ybf0O0e.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_erGV1l4.jpg b/backend/media/users/2/workout_erGV1l4.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_erGV1l4.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_oIN0AMU.jpg b/backend/media/users/2/workout_oIN0AMU.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_oIN0AMU.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_tlB5n5j.exe b/backend/media/users/2/workout_tlB5n5j.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_tlB5n5j.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_wTd9rjn.exe b/backend/media/users/2/workout_wTd9rjn.exe new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_wTd9rjn.exe @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/media/users/2/workout_zuR6pI5.jpg b/backend/media/users/2/workout_zuR6pI5.jpg new file mode 100644 index 0000000..48ad80d --- /dev/null +++ b/backend/media/users/2/workout_zuR6pI5.jpg @@ -0,0 +1 @@ +file_content \ No newline at end of file diff --git a/backend/secfit/settings.py b/backend/secfit/settings.py index 6d29dd4..416399d 100644 --- a/backend/secfit/settings.py +++ b/backend/secfit/settings.py @@ -137,7 +137,12 @@ ] PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.Argon2PasswordHasher', + 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', + 'django.contrib.auth.hashers.ScryptPasswordHasher', + #'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', ] diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py new file mode 100644 index 0000000..45e3d78 --- /dev/null +++ b/backend/tests/test_models.py @@ -0,0 +1,241 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model +from django.urls import reverse +from workouts.models import Exercise, Workout +from users.models import AthleteFile +from datetime import datetime +from django.utils.timezone import make_aware +from django.core.files.uploadedfile import SimpleUploadedFile +import json + +User = get_user_model() + +# Test Case 001: Focuses on workout creation functionality +# Verifies that workouts can be created with valid inputs and proper relationships +class WorkoutCreationTest(TestCase): + """TC_001: Equivalence class testing for workout creation""" + def setUp(self): + # Setup test athlete user and authentication token + self.client = APIClient() + self.athlete = User.objects.create_user( + username='athlete', + password='athletepass123', + email='athlete@example.com', + isCoach=False + ) + # Get JWT token for athlete + response = self.client.post( + reverse('token_obtain_pair'), + {'username': 'athlete', 'password': 'athletepass123'}, + format='json' + ) + self.token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}') + + # Create test exercise to use in workout + self.exercise = Exercise.objects.create( + name="Squats", + description="Bodyweight squats", + unit="reps" + ) + + # Tests successful workout creation with all required valid fields + def test_valid_workout_creation(self): + """Verify workout creation with valid inputs""" + data = { + 'name': 'Morning Run', + 'date': make_aware(datetime(2025, 4, 1)).isoformat(), + 'exercise_instances': [{ + 'exercise': f'http://testserver/api/exercises/{self.exercise.id}/', + 'sets': 3, + 'number': 1, # Added required 'number' field + 'units': 'reps' + }], + 'notes': 'Felt strong today', + 'visibility': 'PU', + 'owner': f'http://testserver/api/users/{self.athlete.id}/' + } + + response = self.client.post( + reverse('workout-list'), + data=data, + format='json' + ) + + self.assertEqual(response.status_code, 201, response.data) + self.assertTrue(Workout.objects.filter(name='Morning Run').exists()) + +# Test Case 002: Focuses on user registration validation +# Verifies password rules and proper user type assignment +class UserRegistrationTest(TestCase): + """TC_002: Boundary value testing for password/user type validation""" + def setUp(self): + # Setup API client and registration endpoint + self.client = APIClient() + self.register_url = reverse('user-list') + + # Tests successful user registration with valid credentials + def test_valid_registration(self): + """Verify system enforces password rules and user type""" + data = { + 'username': 'TestUser', + 'password': 'SecurePass123!', + 'password1': 'SecurePass123!', + 'email': 'test@example.com', + 'first_name': 'Test', + 'last_name': 'User', + 'isCoach': False, + 'athletes': [], + 'workouts': [], + 'coach_files': [], + 'athlete_files': [] + } + + response = self.client.post( + self.register_url, + data=data, + format='json' + ) + + self.assertEqual(response.status_code, 201, response.data) + user = User.objects.get(username='TestUser') + self.assertEqual(user.email, 'test@example.com') + self.assertFalse(user.isCoach) + +# Test Case 003: Focuses on workout visibility permissions +# Verifies access control for private workouts +class WorkoutVisibilityTest(TestCase): + """TC_003: Robust boundary value testing for private workouts""" + def setUp(self): + # Setup test athlete and private workout + self.client = APIClient() + self.athlete = User.objects.create_user( + username='athlete', + password='athletepass123', + isCoach=False + ) + self.workout = Workout.objects.create( + name='Private Workout', + owner=self.athlete, + visibility='PR', + date=make_aware(datetime(2025, 4, 1)), + notes='Test workout' + ) + + # Tests that unauthenticated users cannot access private workouts + def test_visitor_access_private_workout(self): + """Verify visitors can't access private workouts""" + # Clear any authentication + self.client.credentials() + response = self.client.get( + reverse('workout-detail', kwargs={'pk': self.workout.id}) + ) + self.assertEqual(response.status_code, 401, + "Visitors should get 401 Unauthorized") + +# Test Case 004: Focuses on file upload functionality +# Verifies file type validation and coach-athlete relationship +class FileUploadTest(TestCase): + """TC_004: Special value testing for file uploads""" + def setUp(self): + # Setup coach and athlete users with authentication + self.client = APIClient() + self.coach = User.objects.create_user( + username='coach', + password='coachpass123', + isCoach=True + ) + self.athlete = User.objects.create_user( + username='athlete', + password='athletepass123', + isCoach=False + ) + # Get JWT token for coach + response = self.client.post( + reverse('token_obtain_pair'), + {'username': 'coach', 'password': 'coachpass123'}, + format='json' + ) + self.token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}') + + # Tests file upload with different file types + def test_file_upload_validation(self): + """Verify file upload works with different file types""" + # First, ensure coach is assigned to athlete + self.athlete.coach = self.coach + self.athlete.save() + + # Test with valid JPEG file (should succeed) + jpg_file = SimpleUploadedFile( + "workout.jpg", + b"file_content", + content_type="image/jpeg" + ) + response = self.client.post( + reverse('athlete-file-list'), + { + 'file': jpg_file, + 'athlete': f'http://testserver/api/users/{self.athlete.id}/', + 'owner': f'http://testserver/api/users/{self.coach.id}/' + }, + format='multipart' + ) + self.assertEqual(response.status_code, 201, + f"Expected 201, got {response.status_code}. Response: {response.data}") + + # Test with potentially dangerous EXE file (reveals security issue) + exe_file = SimpleUploadedFile( + "workout.exe", + b"file_content", + content_type="application/octet-stream" + ) + response = self.client.post( + reverse('athlete-file-list'), + { + 'file': exe_file, + 'athlete': f'http://testserver/api/users/{self.athlete.id}/', + 'owner': f'http://testserver/api/users/{self.coach.id}/' + }, + format='multipart' + ) + self.assertEqual(response.status_code, 201, + "Currently accepts all file types since validator isn't properly configured") + +class ExerciseValidationTest(TestCase): + """TC_005: Testing exercise creation""" + def setUp(self): + self.client = APIClient() + self.coach = User.objects.create_user( + username='coach', + password='coachpass123', + isCoach=True + ) + # Get JWT token for coach + response = self.client.post( + reverse('token_obtain_pair'), + {'username': 'coach', 'password': 'coachpass123'}, + format='json' + ) + self.token = response.data['access'] + self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}') + + def test_exercise_creation_with_any_unit(self): + """Verify system currently accepts any unit value""" + data = { + 'name': 'Custom Exercise', + 'description': 'This tests unit validation', + 'unit': 'unexpected_unit' # Should pass since validation isn't enforced + } + + response = self.client.post( + reverse('exercise-list'), + data=data, + format='json' + ) + + self.assertEqual(response.status_code, 201, + "Currently accepts any unit value") + self.assertTrue(Exercise.objects.filter(name='Custom Exercise').exists(), + "Exercise should be created regardless of unit value") \ No newline at end of file diff --git a/backend/tests/test_template.py b/backend/tests/test_template.py deleted file mode 100644 index 4a3dda4..0000000 --- a/backend/tests/test_template.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.test import TestCase -from workouts.models import Exercise - - -class User(TestCase): - def setUp(self): - Exercise.objects.create(name="Pushup", description="Pushup", unit="reps") - Exercise.objects.create(name="Running", description="Running", unit="minutes") - Exercise.objects.create(name="Dumbbell curl", description="Lifting dumbbells", unit="reps") - - def test_user_has_coach(self): - all_exercises = Exercise.objects.all() - num_reps = Exercise.objects.filter(unit="reps") - - self.assertEqual(len(all_exercises), 3) - self.assertEqual(len(num_reps), 2) diff --git a/backend/users/migrations/0004_alter_athletefile_file.py b/backend/users/migrations/0004_alter_athletefile_file.py new file mode 100644 index 0000000..45ea429 --- /dev/null +++ b/backend/users/migrations/0004_alter_athletefile_file.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2 on 2025-04-03 20:28 + +import users.models +import users.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_user_specialism'), + ] + + operations = [ + migrations.AlterField( + model_name='athletefile', + name='file', + field=models.FileField(upload_to=users.models.athlete_directory_path, validators=[users.validators.FileValidator(allowed_extensions='', allowed_mimetypes='', max_size=5242880)]), + ), + ]