tests.py 15 KB
Newer Older
1 2 3
"""
This test file will test registration, login, activation, and session activity timeouts
"""
4
import datetime
5
import time
6
import unittest
7

8 9
import mock
from ddt import data, ddt, unpack
10
from django.conf import settings
11
from django.contrib.auth.models import User
12
from django.core.cache import cache
13
from django.core.urlresolvers import reverse
14 15 16 17
from django.test import TestCase
from django.test.utils import override_settings
from freezegun import freeze_time
from pytz import UTC
18

19
from contentstore.models import PushNotificationConfig
20
from contentstore.tests.test_course_settings import CourseTestCase
21
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json, registration, user
22
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
23
from xmodule.modulestore.tests.factories import CourseFactory
Calen Pennington committed
24

25

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

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

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

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

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

        return resp

    def _activate_user(self, email):
73 74
        """Look up the activation key for the user, then hit the activate view.
        No error checking"""
75 76 77 78 79 80 81 82 83 84
        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
85
        self.assertTrue(user(email).is_active)
86

87

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

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

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

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

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

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

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

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

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
    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)

156 157 158 159 160 161 162
    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
163

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

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

Diana Huang committed
169 170 171 172 173 174 175 176 177 178 179 180
    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'])

181 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
    @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)

228 229 230 231 232 233 234 235 236
    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
237
        expected = 'You can now <a href="' + reverse('login') + '">sign in</a>.'
238
        self.assertIn(expected, resp.content.decode('utf-8'))
239

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

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

        # need an activated user
        self.test_create_account()

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

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

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

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

    def test_index_auth(self):

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

        # Logged in should work.
279

280 281 282 283 284 285 286 287 288 289 290 291
    @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
292
        course_url = '/home/'
293
        resp = self.client.get_html(course_url)
294 295 296 297 298
        self.assertEquals(resp.status_code, 200)

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

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

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

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
    @mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
    def test_signup_button_index_page(self):
        """
        Navigate to the home page and check the Sign Up button is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
        is turned off
        """
        response = self.client.get(reverse('homepage'))
        self.assertNotIn('<a class="action action-signup" href="/signup">Sign Up</a>', response.content)

    @mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
    def test_signup_button_login_page(self):
        """
        Navigate to the login page and check the Sign Up button is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
        is turned off
        """
        response = self.client.get(reverse('login'))
        self.assertNotIn('<a class="action action-signup" href="/signup">Sign Up</a>', response.content)

    @mock.patch.dict(settings.FEATURES, {"ALLOW_PUBLIC_ACCOUNT_CREATION": False})
    def test_signup_link_login_page(self):
        """
        Navigate to the login page and check the Sign Up link is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag
        is turned off
        """
        response = self.client.get(reverse('login'))
        self.assertNotIn('<a href="/signup" class="action action-signin">Don&#39;t have a Studio Account? Sign up!</a>',
                         response.content)

332 333 334 335 336 337 338 339 340

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
341 342 343 344 345
        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]
346
        self.assertTrue(self.course.forum_posts_allowed)
David Baumgold committed
347 348 349 350 351
        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]
352
        self.assertFalse(self.course.forum_posts_allowed)
353

354 355 356 357
        # 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)

358 359 360 361 362 363 364 365 366 367

@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')

368
    @data(('edX/test_course_key/Test_Course', 200), ('garbage:edX+test_course_key+Test_Course', 404))
369 370 371 372 373 374 375 376 377
    @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)

378
        url = '/import_status/{course_key}/{filename}'.format(
379 380 381 382 383
            course_key=course_key,
            filename='xyz.tar.gz'
        )
        resp = self.client.get_html(url)
        self.assertEqual(resp.status_code, status_code)
384 385 386 387 388 389 390 391 392 393 394 395


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())