tests.py 13.6 KB
Newer Older
1 2 3 4
"""
This test file will test registration, login, activation, and session activity timeouts
"""
import time
5
import mock
6
import unittest
7
from ddt import ddt, data, unpack
8

9
from django.test import TestCase
10
from django.test.utils import override_settings
Diana Huang committed
11
from django.core.cache import cache
12
from django.conf import settings
13
from django.contrib.auth.models import User
14
from django.core.urlresolvers import reverse
15

16
from contentstore.models import PushNotificationConfig
17
from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient
18
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
19 20 21 22
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
import datetime
from pytz import UTC
23

24
from freezegun import freeze_time
Calen Pennington committed
25

26

27
class ContentStoreTestCase(ModuleStoreTestCase):
David Baumgold committed
28 29 30 31 32
    def _login(self, email, password):
        """
        Login.  View should always return 200.  The success/fail is in the
        returned json
        """
David Baumgold committed
33 34 35 36
        resp = self.client.post(
            reverse('login_post'),
            {'email': email, 'password': password}
        )
37
        self.assertEqual(resp.status_code, 200)
38
        return resp
39

David Baumgold committed
40
    def login(self, email, password):
41
        """Login, check that it worked."""
David Baumgold committed
42
        resp = self._login(email, password)
43
        data = parse_json(resp)
44 45
        self.assertTrue(data['success'])
        return resp
46

David Baumgold committed
47
    def _create_account(self, username, email, password):
48
        """Try to create an account.  No error checking"""
49
        resp = self.client.post('/create_account', {
50 51
            'username': username,
            'email': email,
David Baumgold committed
52
            'password': password,
53 54 55 56 57
            'location': 'home',
            'language': 'Franglish',
            'name': 'Fred Weasley',
            'terms_of_service': 'true',
            'honor_code': 'true',
Calen Pennington committed
58
        })
59 60
        return resp

David Baumgold committed
61
    def create_account(self, username, email, password):
62
        """Create the account and check that it worked"""
David Baumgold committed
63
        resp = self._create_account(username, email, password)
64 65 66
        self.assertEqual(resp.status_code, 200)
        data = parse_json(resp)
        self.assertEqual(data['success'], True)
67 68

        # Check both that the user is created, and inactive
69
        self.assertFalse(user(email).is_active)
70 71 72 73

        return resp

    def _activate_user(self, email):
74 75
        """Look up the activation key for the user, then hit the activate view.
        No error checking"""
76 77 78 79 80 81 82 83 84 85
        activation_key = registration(email).activation_key

        # and now we try to activate
        resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
        return resp

    def activate_user(self, email):
        resp = self._activate_user(email)
        self.assertEqual(resp.status_code, 200)
        # Now make sure that the user is now actually activated
86
        self.assertTrue(user(email).is_active)
87

88

89 90
class AuthTestCase(ContentStoreTestCase):
    """Check that various permissions-related things work"""
91

92
    CREATE_USER = False
93
    ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
94 95

    def setUp(self):
96
        super(AuthTestCase, self).setUp()
97

98 99 100
        self.email = 'a@b.com'
        self.pw = 'xyz'
        self.username = 'testuser'
101
        self.client = AjaxEnabledTestClient()
Diana Huang committed
102 103
        # clear the cache so ratelimiting won't affect these tests
        cache.clear()
104 105

    def check_page_get(self, url, expected):
106
        resp = self.client.get_html(url)
107
        self.assertEqual(resp.status_code, expected)
108
        return resp
109

110 111 112
    def test_public_pages_load(self):
        """Make sure pages that don't require login load without error."""
        pages = (
113 114 115
            reverse('login'),
            reverse('signup'),
        )
116
        for page in pages:
117
            print "Checking '{0}'".format(page)
118
            self.check_page_get(page, 200)
119

120 121 122
    def test_create_account_errors(self):
        # No post data -- should fail
        resp = self.client.post('/create_account', {})
123
        self.assertEqual(resp.status_code, 400)
124
        data = parse_json(resp)
125 126 127 128 129
        self.assertEqual(data['success'], False)

    def test_create_account(self):
        self.create_account(self.username, self.email, self.pw)
        self.activate_user(self.email)
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
    def test_create_account_username_already_exists(self):
        User.objects.create_user(self.username, self.email, self.pw)
        resp = self._create_account(self.username, "abc@def.com", "password")
        # we have a constraint on unique usernames, so this should fail
        self.assertEqual(resp.status_code, 400)

    def test_create_account_pw_already_exists(self):
        User.objects.create_user(self.username, self.email, self.pw)
        resp = self._create_account("abcdef", "abc@def.com", self.pw)
        # we can have two users with the same password, so this should succeed
        self.assertEqual(resp.status_code, 200)

    @unittest.skipUnless(settings.SOUTH_TESTS_MIGRATE, "South migrations required")
    def test_create_account_email_already_exists(self):
        User.objects.create_user(self.username, self.email, self.pw)
        resp = self._create_account("abcdef", self.email, "password")
        # This is tricky. Django's user model doesn't have a constraint on
        # unique email addresses, but we *add* that constraint during the
        # migration process:
        # see common/djangoapps/student/migrations/0004_add_email_index.py
        #
        # The behavior we *want* is for this account creation request
        # to fail, due to this uniqueness constraint, but the request will
        # succeed if the migrations have not run.
        self.assertEqual(resp.status_code, 400)

157 158 159 160 161 162 163
    def test_login(self):
        self.create_account(self.username, self.email, self.pw)

        # Not activated yet.  Login should fail.
        resp = self._login(self.email, self.pw)
        data = parse_json(resp)
        self.assertFalse(data['success'])
Calen Pennington committed
164

165 166 167 168 169
        self.activate_user(self.email)

        # Now login should work
        self.login(self.email, self.pw)

Diana Huang committed
170 171 172 173 174 175 176 177 178 179 180 181
    def test_login_ratelimited(self):
        # try logging in 30 times, the default limit in the number of failed
        # login attempts in one 5 minute period before the rate gets limited
        for i in xrange(30):
            resp = self._login(self.email, 'wrong_password{0}'.format(i))
            self.assertEqual(resp.status_code, 200)
        resp = self._login(self.email, 'wrong_password')
        self.assertEqual(resp.status_code, 200)
        data = parse_json(resp)
        self.assertFalse(data['success'])
        self.assertIn('Too many failed login attempts.', data['value'])

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    @override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=3)
    @override_settings(MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS=2)
    def test_excessive_login_failures(self):
        # try logging in 3 times, the account should get locked for 3 seconds
        # note we want to keep the lockout time short, so we don't slow down the tests

        with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}):
            self.create_account(self.username, self.email, self.pw)
            self.activate_user(self.email)

            for i in xrange(3):
                resp = self._login(self.email, 'wrong_password{0}'.format(i))
                self.assertEqual(resp.status_code, 200)
                data = parse_json(resp)
                self.assertFalse(data['success'])
                self.assertIn(
                    'Email or password is incorrect.',
                    data['value']
                )

            # now the account should be locked

            resp = self._login(self.email, 'wrong_password')
            self.assertEqual(resp.status_code, 200)
            data = parse_json(resp)
            self.assertFalse(data['success'])
            self.assertIn(
                'This account has been temporarily locked due to excessive login failures. Try again later.',
                data['value']
            )

            with freeze_time('2100-01-01'):
                self.login(self.email, self.pw)

            # make sure the failed attempt counter gets reset on successful login
            resp = self._login(self.email, 'wrong_password')
            self.assertEqual(resp.status_code, 200)
            data = parse_json(resp)
            self.assertFalse(data['success'])

            # account should not be locked out after just one attempt
            self.login(self.email, self.pw)

            # do one more login when there is no bad login counter row at all in the database to
            # test the "ObjectNotFound" case
            self.login(self.email, self.pw)

229 230 231 232 233 234 235 236 237
    def test_login_link_on_activation_age(self):
        self.create_account(self.username, self.email, self.pw)
        # we want to test the rendering of the activation page when the user isn't logged in
        self.client.logout()
        resp = self._activate_user(self.email)
        self.assertEqual(resp.status_code, 200)

        # check the the HTML has links to the right login page. Note that this is merely a content
        # check and thus could be fragile should the wording change on this page
238
        expected = 'You can now <a href="' + reverse('login') + '">sign in</a>.'
239
        self.assertIn(expected, resp.content.decode('utf-8'))
240

241 242 243
    def test_private_pages_auth(self):
        """Make sure pages that do require login work."""
        auth_pages = (
244
            '/home/',
245
        )
246 247 248 249

        # These are pages that should just load when the user is logged in
        # (no data needed)
        simple_auth_pages = (
250
            '/home/',
251
        )
252 253 254 255

        # need an activated user
        self.test_create_account()

Calen Pennington committed
256
        # Create a new session
257
        self.client = AjaxEnabledTestClient()
Calen Pennington committed
258

259
        # Not logged in.  Should redirect to login.
260
        print 'Not logged in'
261
        for page in auth_pages:
262
            print "Checking '{0}'".format(page)
263 264 265 266 267
            self.check_page_get(page, expected=302)

        # Logged in should work.
        self.login(self.email, self.pw)

268
        print 'Logged in'
269
        for page in simple_auth_pages:
270
            print "Checking '{0}'".format(page)
271 272 273 274 275
            self.check_page_get(page, expected=200)

    def test_index_auth(self):

        # not logged in.  Should return a redirect.
276
        resp = self.client.get_html('/home/')
277 278 279
        self.assertEqual(resp.status_code, 302)

        # Logged in should work.
280

281 282 283 284 285 286 287 288 289 290 291 292
    @override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1)
    def test_inactive_session_timeout(self):
        """
        Verify that an inactive session times out and redirects to the
        login page
        """
        self.create_account(self.username, self.email, self.pw)
        self.activate_user(self.email)

        self.login(self.email, self.pw)

        # make sure we can access courseware immediately
293
        course_url = '/home/'
294
        resp = self.client.get_html(course_url)
295 296 297 298 299
        self.assertEquals(resp.status_code, 200)

        # then wait a bit and see if we get timed out
        time.sleep(2)

300
        resp = self.client.get_html(course_url)
301 302

        # re-request, and we should get a redirect to login page
303
        self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=/home/')
304

305 306 307 308 309 310 311 312 313

class ForumTestCase(CourseTestCase):
    def setUp(self):
        """ Creates the test course. """
        super(ForumTestCase, self).setUp()
        self.course = CourseFactory.create(org='testX', number='727', display_name='Forum Course')

    def test_blackouts(self):
        now = datetime.datetime.now(UTC)
David Baumgold committed
314 315 316 317 318
        times1 = [
            (now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)),
            (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))
        ]
        self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times1]
319
        self.assertTrue(self.course.forum_posts_allowed)
David Baumgold committed
320 321 322 323 324
        times2 = [
            (now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)),
            (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))
        ]
        self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times2]
325
        self.assertFalse(self.course.forum_posts_allowed)
326

327 328 329 330
        # test if user gives empty blackout date it should return true for forum_posts_allowed
        self.course.discussion_blackouts = [[]]
        self.assertTrue(self.course.forum_posts_allowed)

331 332 333 334 335 336 337 338 339 340

@ddt
class CourseKeyVerificationTestCase(CourseTestCase):
    def setUp(self):
        """
        Create test course.
        """
        super(CourseKeyVerificationTestCase, self).setUp()
        self.course = CourseFactory.create(org='edX', number='test_course_key', display_name='Test Course')

341
    @data(('edX/test_course_key/Test_Course', 200), ('garbage:edX+test_course_key+Test_Course', 404))
342 343 344 345 346 347 348 349 350
    @unpack
    def test_course_key_decorator(self, course_key, status_code):
        """
        Tests for the ensure_valid_course_key decorator.
        """
        url = '/import/{course_key}'.format(course_key=course_key)
        resp = self.client.get_html(url)
        self.assertEqual(resp.status_code, status_code)

351
        url = '/import_status/{course_key}/{filename}'.format(
352 353 354 355 356
            course_key=course_key,
            filename='xyz.tar.gz'
        )
        resp = self.client.get_html(url)
        self.assertEqual(resp.status_code, status_code)
357 358 359 360 361 362 363 364 365 366 367 368


class PushNotificationConfigTestCase(TestCase):
    """
    Tests PushNotificationConfig.
    """
    def test_notifications_defaults(self):
        self.assertFalse(PushNotificationConfig.is_enabled())

    def test_notifications_enabled(self):
        PushNotificationConfig(enabled=True).save()
        self.assertTrue(PushNotificationConfig.is_enabled())