tests.py 12.8 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.utils import override_settings
Diana Huang committed
10
from django.core.cache import cache
11
from django.conf import settings
12
from django.contrib.auth.models import User
13
from django.core.urlresolvers import reverse
14

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

22
from freezegun import freeze_time
Calen Pennington committed
23

24

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

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

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

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

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

        return resp

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

86

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

    def setUp(self):
        self.email = 'a@b.com'
        self.pw = 'xyz'
        self.username = 'testuser'
94
        self.client = AjaxEnabledTestClient()
Diana Huang committed
95 96
        # clear the cache so ratelimiting won't affect these tests
        cache.clear()
97 98

    def check_page_get(self, url, expected):
99
        resp = self.client.get_html(url)
100
        self.assertEqual(resp.status_code, expected)
101
        return resp
102

103 104 105
    def test_public_pages_load(self):
        """Make sure pages that don't require login load without error."""
        pages = (
106 107 108
            reverse('login'),
            reverse('signup'),
        )
109
        for page in pages:
David Baumgold committed
110
            print("Checking '{0}'".format(page))
111
            self.check_page_get(page, 200)
112

113 114 115
    def test_create_account_errors(self):
        # No post data -- should fail
        resp = self.client.post('/create_account', {})
116
        self.assertEqual(resp.status_code, 400)
117
        data = parse_json(resp)
118 119 120 121 122
        self.assertEqual(data['success'], False)

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

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
    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)

150 151 152 153 154 155 156
    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
157

158 159 160 161 162
        self.activate_user(self.email)

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

Diana Huang committed
163 164 165 166 167 168 169 170 171 172 173 174
    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'])

175 176 177 178 179 180 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
    @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)

222 223 224 225 226 227 228 229 230 231 232 233
    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
        expected = 'You can now <a href="' + reverse('login') + '">login</a>.'
        self.assertIn(expected, resp.content)

234 235 236
    def test_private_pages_auth(self):
        """Make sure pages that do require login work."""
        auth_pages = (
237
            '/home/',
238
        )
239 240 241 242

        # These are pages that should just load when the user is logged in
        # (no data needed)
        simple_auth_pages = (
243
            '/home/',
244
        )
245 246 247 248

        # need an activated user
        self.test_create_account()

Calen Pennington committed
249
        # Create a new session
250
        self.client = AjaxEnabledTestClient()
Calen Pennington committed
251

252
        # Not logged in.  Should redirect to login.
David Baumgold committed
253
        print('Not logged in')
254
        for page in auth_pages:
David Baumgold committed
255
            print("Checking '{0}'".format(page))
256 257 258 259 260
            self.check_page_get(page, expected=302)

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

David Baumgold committed
261
        print('Logged in')
262
        for page in simple_auth_pages:
David Baumgold committed
263
            print("Checking '{0}'".format(page))
264 265 266 267 268
            self.check_page_get(page, expected=200)

    def test_index_auth(self):

        # not logged in.  Should return a redirect.
269
        resp = self.client.get_html('/home/')
270 271 272
        self.assertEqual(resp.status_code, 302)

        # Logged in should work.
273

274 275 276 277 278 279 280 281 282 283 284 285
    @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
286
        course_url = '/home/'
287
        resp = self.client.get_html(course_url)
288 289 290 291 292
        self.assertEquals(resp.status_code, 200)

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

293
        resp = self.client.get_html(course_url)
294 295

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

298 299 300 301 302 303 304 305 306

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
307 308 309 310 311
        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]
312
        self.assertTrue(self.course.forum_posts_allowed)
David Baumgold committed
313 314 315 316 317
        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]
318
        self.assertFalse(self.course.forum_posts_allowed)
319 320 321 322 323 324 325 326 327 328 329


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

330
    @data(('edX/test_course_key/Test_Course', 200), ('garbage:edX+test_course_key+Test_Course', 404))
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
    @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)

        url = '/import_status/{course_key}/{filename}'.format(
            course_key=course_key,
            filename='xyz.tar.gz'
        )
        resp = self.client.get_html(url)
        self.assertEqual(resp.status_code, status_code)