# Run from /backend with: python manage.py test users
from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from users.models import User
from users.services import Conflict, register_user
[docs]
class UserModelTests(TestCase):
[docs]
def test_create_user(self):
"""Verifies user creation"""
user = User.objects.create(
email="test_user@ucla.edu",
password="password12345678",
role="MENTEE"
)
self.assertEqual(user.email, "test_user@ucla.edu")
self.assertEqual(user.role, "MENTEE")
[docs]
def test_user_requires_email(self):
"""Ensures email is required"""
user = User(email="", password="password12345678")
with self.assertRaises(ValidationError):
user.full_clean()
user.save()
[docs]
def test_user_requires_valid_ucla_email(self):
"""Validates UCLA email domain"""
user1 = User(email="invalid@gmail.edu", password="password12345678")
with self.assertRaises(ValidationError):
user1.full_clean()
user1.save()
user2 = User(email="invalid@ucla.com", password="password12345678")
with self.assertRaises(ValidationError):
user2.full_clean()
user2.save()
[docs]
class UserMatchingLogicTests(TestCase):
[docs]
def setUp(self):
self.mentor = User.objects.create_user(email="mentor@ucla.edu", password="pw", role="MENTOR")
self.mentee1 = User.objects.create_user(email="mentee1@ucla.edu", password="pw", role="MENTEE")
self.mentee2 = User.objects.create_user(email="mentee2@ucla.edu", password="pw", role="MENTEE")
self.mentee3 = User.objects.create_user(email="mentee3@ucla.edu", password="pw", role="MENTEE")
[docs]
def test_max_mentee_limit(self):
"""Ensures a mentor cannot exceed the MAX_MENTEES limit"""
# Add first two mentees (should succeed)
self.assertTrue(self.mentor.add_mentee(self.mentee1.email))
self.assertTrue(self.mentor.add_mentee(self.mentee2.email))
# Attempt to add a third mentee (should fail)
self.assertFalse(self.mentor.add_mentee(self.mentee3.email))
# Verify mentor status automatically updated to 'isMatched' (full)
self.assertTrue(self.mentor.isMatched)
[docs]
def test_blacklisted_mentor_rejection(self):
"""Ensures a mentor cannot add a mentee who blacklisted them"""
self.mentee1.blacklist_mentor(self.mentor.email)
# Attempt to add the mentee (should fail due to blacklist)
self.assertFalse(self.mentor.add_mentee(self.mentee1.email))
[docs]
class UserProfileSecurityTests(TestCase):
[docs]
def setUp(self):
self.user = User.objects.create_user(email="student@ucla.edu", password="pw", role="MENTEE")
[docs]
def test_update_profile_ignores_protected_fields(self):
"""Ensures users cannot overwrite protected fields like isMatched via profile updates"""
malicious_data = {
"firstName": "Hacker", # Allowed field
"role": "ADMIN", # Protected field
"isMatched": True # Protected field
}
self.user.update_profile(**malicious_data)
# Verify allowed field changed
self.assertEqual(self.user.firstName, "Hacker")
# Verify protected fields were completely ignored
self.assertEqual(self.user.role, "MENTEE")
self.assertFalse(self.user.isMatched)
[docs]
def test_complete_survey_has_same_protections(self):
"""complete_survey() should have same security as update_profile()"""
self.user.complete_survey({
"firstName": "Hacker",
"role": "ADMIN",
"isMatched": True
})
self.assertEqual(self.user.firstName, "Hacker")
self.assertEqual(self.user.role, "MENTEE")
self.assertFalse(self.user.isMatched)
[docs]
def test_update_profile_allows_mentor_mentee_role_change(self):
"""Users can switch between MENTOR and MENTEE"""
self.user.update_profile(role="MENTOR")
self.assertEqual(self.user.role, "MENTOR")
self.user.update_profile(role="MENTEE")
self.assertEqual(self.user.role, "MENTEE")
[docs]
def test_change_role_blocks_same_role(self):
"""change_role() prevents no-op changes"""
with self.assertRaises(ValueError) as context:
self.user.change_role("MENTEE") # already MENTEE
self.assertIn("already have this role", str(context.exception))
[docs]
class AdminCreationTests(TestCase):
"""Ensure ADMIN users can only be created via safe methods"""
[docs]
def test_admin_creation_via_create_superuser(self):
"""Admins should be created via create_superuser"""
admin = User.objects.create_superuser(
email="admin@ucla.edu",
password="adminpass12345678"
)
self.assertEqual(admin.role, "ADMIN")
self.assertTrue(admin.is_staff)
self.assertTrue(admin.is_superuser)
[docs]
def test_cannot_become_admin_via_profile_update(self):
"""Check whether there is a loophole to become ADMIN"""
user = User.objects.create_user(
email="not_admin@ucla.edu",
password="password12345678",
role="MENTEE"
)
# try to become an admin
user.update_profile(role="ADMIN")
self.assertNotEqual(user.role, "ADMIN")
user.complete_survey({"role": "ADMIN"})
self.assertNotEqual(user.role, "ADMIN")
user.role = "ADMIN"
user.save()
[docs]
class UserServiceTests(TestCase):
[docs]
def test_register_user_service_success(self):
"""Verifies the service creates a user and hashes the password"""
user = register_user(email="new_student@ucla.edu", password="securepassword", role="MENTEE")
self.assertEqual(user.email, "new_student@ucla.edu")
# Ensure password was hashed, not stored in plain text
self.assertNotEqual(user.password, "securepassword")
self.assertTrue(user.check_password("securepassword"))
[docs]
def test_register_duplicate_email_raises_conflict(self):
"""Ensures registering an existing email throws the correct Conflict error"""
register_user(email="existing@ucla.edu", password="pw1", role="MENTEE")
with self.assertRaises(Conflict):
register_user(email="existing@ucla.edu", password="pw2", role="MENTOR")
[docs]
class RegistrationTests(TestCase):
[docs]
def test_register_while_in_grace_period(self):
"""Test that you can't register during grace period"""
# register and delete account
user = register_user('grace@ucla.edu', 'password12345678', 'MENTEE')
user.request_deletion()
# try to register during grace period: should fail
with self.assertRaises(Conflict):
register_user('grace@ucla.edu', 'newpassword12345', 'MENTOR')
[docs]
def test_register_past_grace_period(self):
"""Test that you can re-register after account expiration"""
# register user
user = register_user('expired@ucla.edu', 'password12345678', 'MENTEE')
user_id = user.pk
# request deletion
user.request_deletion()
# manually expire the account by subtracting 2 hours
user.permanent_deletion_date = timezone.now() - timedelta(hours=2)
user.save()
new_user = register_user('expired@ucla.edu', 'newpassword12345', 'MENTOR')
self.assertIsNotNone(new_user)
self.assertEqual(new_user.email, 'expired@ucla.edu')
self.assertEqual(new_user.role, 'MENTOR')
self.assertNotEqual(new_user.pk, user_id) # different user object
self.assertTrue(new_user.is_verified) # auto-verified in test mode
# old user should be deleted
with self.assertRaises(User.DoesNotExist):
User.objects.get(pk=user_id)
[docs]
class UserAccountDeletionTests(TestCase):
[docs]
def setUp(self):
self.user = User.objects.create_user(email="delete_me@ucla.edu", password="pw", role="MENTEE")
[docs]
def test_request_deletion_flags_account(self):
"""Ensures requesting deletion sets flags but doesn't hard-delete immediately"""
self.user.request_deletion()
# Verify the soft-delete flags were triggered
self.assertTrue(self.user.is_deleted)
self.assertIsNotNone(self.user.permanent_deletion_date)
# Verify the user record still physically exists in the database
self.assertEqual(User.objects.count(), 1)
[docs]
def test_cancel_deletion_restores_account(self):
"""Ensures a user can cancel their deletion within the grace period"""
self.user.request_deletion()
# Cancel the deletion
self.assertTrue(self.user.cancel_deletion())
# Verify the flags were completely removed
self.assertFalse(self.user.is_deleted)
self.assertIsNone(self.user.permanent_deletion_date)
[docs]
class UserAPIClientTests(TestCase):
[docs]
def setUp(self):
self.user = User.objects.create_user(email="api_tester@ucla.edu", password="secure_pw", role="MENTEE")
# Ensure the user is verified so they can log in
self.user.is_verified = True
self.user.save()
[docs]
def test_unauthenticated_user_access_denied(self):
"""Ensures an unauthenticated browser cannot access the profile endpoint"""
# Attempt to access the /me/ endpoint WITHOUT logging in
url = reverse('current_user')
response = self.client.get(url)
# Django should reject this with a 401 Unauthorized or 403 Forbidden
self.assertIn(response.status_code, [401, 403])
[docs]
def test_authenticated_user_access_granted(self):
"""Ensures a logged-in user can access their own profile data"""
# 1. Log the test client in
login_success = self.client.login(email="api_tester@ucla.edu", password="secure_pw")
self.assertTrue(login_success)
# 2. Access the /me/ endpoint
url = reverse('current_user')
response = self.client.get(url)
# 3. Verify the server accepted the request (200 OK)
self.assertEqual(response.status_code, 200)
[docs]
class UserRoleChangeTests(TestCase):
[docs]
def setUp(self):
self.mentor = User.objects.create_user(email="mentor_switch@ucla.edu", password="pw", role="MENTOR")
self.mentee = User.objects.create_user(email="mentee_switch@ucla.edu", password="pw", role="MENTEE")
# Match them up
self.mentor.add_mentee(self.mentee.email)
self.mentee.matchedMentorEmail = self.mentor.email
self.mentee.save()
[docs]
def test_change_role_wipes_matches(self):
"""Ensures that changing a role completely cleans up existing relationships"""
# Change mentee to mentor
self.mentee.change_role('MENTOR')
# Refresh the original mentor from the database
self.mentor.refresh_from_db()
# The original mentor should no longer have the mentee in their list
self.assertNotIn(self.mentee.email, self.mentor.current_mentees)
# The mentor should no longer be marked as 'isMatched' (full)
self.assertFalse(self.mentor.isMatched)
[docs]
class UserTokenExpirationTests(TestCase):
[docs]
def setUp(self):
self.user = User.objects.create_user(email="token_test@ucla.edu", password="pw", role="MENTEE")
[docs]
def test_expired_verification_token_is_rejected(self):
"""Ensures a token past its expiration time fails verification"""
token = self.user.generate_verification_token()
# Artificially age the token's creation time to 2 hours ago (past the 1-hour limit)
self.user.email_verification_sent_at = timezone.now() - timedelta(hours=settings.EMAIL_VERIFICATION_TOKEN_EXPIRATION + 1)
self.user.save()
# Attempt to verify
is_valid = self.user.verify_email(token)
self.assertFalse(is_valid)
self.assertFalse(self.user.is_verified)
[docs]
class HardDeletionTests(TestCase):
[docs]
def setUp(self):
# User 1: Expired grace period
self.expired_user = User.objects.create_user(email="expired@ucla.edu", password="pw")
self.expired_user.request_deletion()
self.expired_user.permanent_deletion_date = timezone.now() - timedelta(days=1)
self.expired_user.save()
# User 2: Still in grace period
self.safe_user = User.objects.create_user(email="safe@ucla.edu", password="pw")
self.safe_user.request_deletion()
self.safe_user.permanent_deletion_date = timezone.now() + timedelta(days=1)
self.safe_user.save()
[docs]
def test_only_expired_accounts_are_permanently_deleted(self):
"""Ensures the hard-delete function respects the grace period"""
deleted_count = User.permanently_delete_expired_accounts()
self.assertEqual(deleted_count, 1)
# Verify expired user is completely gone
with self.assertRaises(User.DoesNotExist):
User.objects.get(email="expired@ucla.edu")
# Verify safe user still exists
self.assertTrue(User.objects.filter(email="safe@ucla.edu").exists())