<filename>tests/test_models.py
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.utils import IntegrityError
from django.test import override_settings, TestCase
from django.utils import timezone
from uniauth.models import Institution, InstitutionAccount, LinkedEmail, \
        UserProfile


class ModelSignalTests(TestCase):
    """
    Tests that the custom model signals fire and
    behave as expected
    """

    def test_create_user_profile_signal(self):
        """
        Ensure a UserProfile is automatically created
        whenever Users are
        """
        user = User.objects.create(username="new-user")
        self.assertTrue(UserProfile.objects.filter(
                user__username="new-user").exists())
        user2 = User.objects.create(username="<EMAIL>",
                email="<EMAIL>")
        self.assertTrue(UserProfile.objects.filter(
                user__email="<EMAIL>").exists())

    @override_settings(PASSWORD_RESET_TIMEOUT_DAYS=4)
    def test_clear_old_tmp_users_signal(self):
        """
        Ensure old temporary users are deleted whenever
        a new User is created
        """
        User.objects.all().delete()
        User.objects.create(username="not-temporary-user")
        for i in range(10):
            User.objects.create(username="tmp-%d-days-ago"%i)
        # We must update the date_joined in a different for loop,
        # because otherwise, the users could get deleted on the
        # create signal we're trying to test!
        for i in range(10):
            date_joined = timezone.now() - timedelta(days=i)
            user = User.objects.get(username="tmp-%d-days-ago"%i)
            user.date_joined = date_joined
            user.save()
        # Create another object to (hopefully) trigger the tmp
        # user deletion signal
        User.objects.create(username="another-user")

        expected_num_users = 10 - (settings.PASSWORD_RESET_TIMEOUT_DAYS + 1) + 2
        self.assertEqual(User.objects.count(), expected_num_users)
        self.assertTrue(User.objects.filter(username="not-temporary-user")\
                .exists())
        self.assertTrue(User.objects.filter(username="another-user").exists())
        for i in range(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1):
            self.assertTrue(User.objects.filter(username="tmp-%d-days-ago"%i)\
                    .exists())
        for i in range(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1, 10):
            self.assertFalse(User.objects.filter(username="tmp-%d-days-ago"%i)\
                    .exists())


class UserProfileModelTests(TestCase):
    """
    Tests the UserProfile model in models.py
    """

    def test_user_profile_model_basic(self):
        """
        Ensure the basic model methods + attributes work properly
        """
        user = User.objects.create(username="new-user")
        self.assertNotEqual(user.uniauth_profile, None)
        self.assertEqual(user.uniauth_profile.user, user)
        self.assertEqual(str(user.uniauth_profile), "new-user")
        user2 = User.objects.create(username="other-user",
                email="<EMAIL>")
        self.assertEqual(str(user2.uniauth_profile), "<EMAIL>")
        result = UserProfile.objects.get(user__username="new-user")
        self.assertEqual(result, user.uniauth_profile)
        with self.assertRaises(IntegrityError):
            UserProfile.objects.create()

    def test_user_profile_get_display_id(self):
        """
        Ensure the get_display_id method works properly
        """
        user = User.objects.create(username="new-user")
        self.assertEqual(user.uniauth_profile.get_display_id(), "new-user")
        user = User.objects.create(username="cas-example-inst-id123")
        self.assertEqual(user.uniauth_profile.get_display_id(), "id123")
        user = User.objects.create(username="<EMAIL>",
                email="<EMAIL>")
        self.assertEqual(user.uniauth_profile.get_display_id(), "john.doe")


class LinkedEmailModelTests(TestCase):
    """
    Tests the LinkedEmail model in models.py
    """

    def test_linked_email_model_basic(self):
        """
        Ensure the basic model methods + attributes work properly
        """
        user = User.objects.create(username="new-user")
        linked_email = LinkedEmail.objects.create(profile=user.uniauth_profile,
                address="<EMAIL>", is_verified=True)
        self.assertEqual(linked_email.profile, user.uniauth_profile)
        self.assertEqual(linked_email.address, "<EMAIL>")
        self.assertTrue(linked_email.is_verified)
        self.assertTrue("new-user" in str(linked_email))
        self.assertTrue("<EMAIL>" in str(linked_email))
        result = LinkedEmail.objects.get(profile=user.uniauth_profile,
                address="<EMAIL>")
        self.assertEqual(result, linked_email)

    def test_linked_email_model_clean(self):
        """
        Ensure the model prevents saving invalid states
        """
        user1 = User.objects.create(username="new-user-1")
        user2 = User.objects.create(username="new-user-2")
        user3 = User.objects.create(username="new-user-3")

        # Don't allow shared emails if setting is False
        with self.settings(UNIAUTH_ALLOW_SHARED_EMAILS=False):
            LinkedEmail.objects.create(profile=user1.uniauth_profile,
                    address="<EMAIL>", is_verified=True)
            try:
                email = LinkedEmail(profile=user2.uniauth_profile,
                        address="<EMAIL>", is_verified=True)
                email.clean()
                email.save()
                self.fail("Adding shared email address succeeded when "
                        "UNIAUTH_ALLOW_SHARED_EMAILS was False.")
            except ValidationError:
                pass
            self.assertFalse(LinkedEmail.objects.filter(
                    profile=user2.uniauth_profile,
                    address="<EMAIL>").exists())

        # Don't allow more than the max number of linked emails per profile
        with self.settings(UNIAUTH_MAX_LINKED_EMAILS=5):
            for i in range(settings.UNIAUTH_MAX_LINKED_EMAILS):
                LinkedEmail.objects.create(profile=user3.uniauth_profile,
                        address="<EMAIL>"%i, is_verified=(i%2==0))
            try:
                email = LinkedEmail(profile=user3.uniauth_profile,
                        address="<EMAIL>")
                email.clean()
                email.save()
                self.fail("Able to link more than UNIAUTH_MAX_LINKED_EMAILS")
            except ValidationError:
                pass
            self.assertFalse(LinkedEmail.objects.filter(profile=user3.uniauth_profile,
                    address="<EMAIL>").exists())

        # Allow any number of linked emails if setting is <= 0
        with self.settings(UNIAUTH_MAX_LINKED_EMAILS=-1):
            for i in range(100):
                email, _ = LinkedEmail.objects.get_or_create(
                        profile=user3.uniauth_profile,
                        address="<EMAIL>"%i, is_verified=(i%2==0))
                email.clean()
                email.save()
            self.assertEqual(LinkedEmail.objects.filter(
                    profile=user3.uniauth_profile).count(), 100)


class InstitutionModelTests(TestCase):
    """
    Tests the Institution model in models.py
    """

    def test_institution_model_basic(self):
        """
        Ensure the basic model methods + attributes work properly
        """
        institution = Institution.objects.create(name="Test Inst",
                slug="test-inst", cas_server_url="https://fed.example.edu/")
        self.assertEqual(institution.name, "Test Inst")
        self.assertEqual(institution.slug, "test-inst")
        self.assertEqual(institution.cas_server_url, "https://fed.example.edu/")
        self.assertEqual(str(institution), "test-inst")
        result = Institution.objects.get(slug="test-inst")
        self.assertEqual(result, institution)
        with self.assertRaises(IntegrityError):
            Institution.objects.create(name="Test-inst", slug="test-inst",
                    cas_server_url="https://www.different.edu/")


class InstitutionAccountModelTests(TestCase):
    """
    Tests the InstitutionAccount model in models.py
    """

    def test_institution_account_model_basic(self):
        """
        Ensure the basic model methods + attributes work properly
        """
        user = User.objects.create(username="new-user")
        institution = Institution.objects.create(name="Test Inst",
                slug="test-inst", cas_server_url="https://fed.example.edu/")
        account = InstitutionAccount.objects.create(
                profile=user.uniauth_profile,
                institution=institution, cas_id="netid123")
        self.assertEqual(account.profile, user.uniauth_profile)
        self.assertEqual(account.institution, institution)
        self.assertEqual(account.cas_id, "netid123")
        self.assertTrue("test-inst" in str(account))
        self.assertTrue("new-user" in str(account))
        result = InstitutionAccount.objects.get(institution=institution,
                cas_id="netid123")
        self.assertEqual(result, account)

