tests.py 15.5 KB
Newer Older
1 2 3
"""
This test file will test registration, login, activation, and session activity timeouts
"""
bmedx committed
4
from __future__ import print_function
5
import datetime
6 7
import time

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

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

27

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

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

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

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

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

        return resp

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

89

bmedx committed
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
@pytest.mark.django_db
def test_create_account_email_already_exists(django_db_use_migrations):
    """
    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.

    django_db_use_migration is a pytest fixture that tells us if
    migrations have been run. Since pytest fixtures don't play nice
    with TestCase objects this is a function and doesn't get to use
    assertRaises.
    """
    if django_db_use_migrations:
        email = 'a@b.com'
        pw = 'xyz'
        username = 'testuser'
        User.objects.create_user(username, email, pw)

        # Hack to use the _create_account shortcut
        case = ContentStoreTestCase()
        resp = case._create_account("abcdef", email, "password")  # pylint: disable=protected-access

        assert resp.status_code == 400, 'Migrations are run, but creating an account with duplicate email succeeded!'


120 121
class AuthTestCase(ContentStoreTestCase):
    """Check that various permissions-related things work"""
122

123
    CREATE_USER = False
124
    ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
125 126

    def setUp(self):
127
        super(AuthTestCase, self).setUp()
128

129 130 131
        self.email = 'a@b.com'
        self.pw = 'xyz'
        self.username = 'testuser'
132
        self.client = AjaxEnabledTestClient()
Diana Huang committed
133 134
        # clear the cache so ratelimiting won't affect these tests
        cache.clear()
135 136

    def check_page_get(self, url, expected):
137
        resp = self.client.get_html(url)
138
        self.assertEqual(resp.status_code, expected)
139
        return resp
140

141 142 143
    def test_public_pages_load(self):
        """Make sure pages that don't require login load without error."""
        pages = (
144 145 146
            reverse('login'),
            reverse('signup'),
        )
147
        for page in pages:
bmedx committed
148
            print("Checking '{0}'".format(page))
149
            self.check_page_get(page, 200)
150

151 152 153
    def test_create_account_errors(self):
        # No post data -- should fail
        resp = self.client.post('/create_account', {})
154
        self.assertEqual(resp.status_code, 400)
155
        data = parse_json(resp)
156 157 158 159 160
        self.assertEqual(data['success'], False)

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

162 163 164 165 166 167 168 169 170 171 172 173
    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)

174 175 176 177 178 179 180
    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
181

182 183 184 185 186
        self.activate_user(self.email)

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

Diana Huang committed
187 188 189 190 191 192 193 194 195 196 197 198
    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'])

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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    @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)

246 247 248 249 250 251 252 253 254
    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
255
        expected = 'You can now <a href="' + reverse('login') + '">sign in</a>.'
256
        self.assertIn(expected, resp.content.decode('utf-8'))
257

258 259 260
    def test_private_pages_auth(self):
        """Make sure pages that do require login work."""
        auth_pages = (
261
            '/home/',
262
        )
263 264 265 266

        # These are pages that should just load when the user is logged in
        # (no data needed)
        simple_auth_pages = (
267
            '/home/',
268
        )
269 270 271 272

        # need an activated user
        self.test_create_account()

Calen Pennington committed
273
        # Create a new session
274
        self.client = AjaxEnabledTestClient()
Calen Pennington committed
275

276
        # Not logged in.  Should redirect to login.
bmedx committed
277
        print('Not logged in')
278
        for page in auth_pages:
bmedx committed
279
            print("Checking '{0}'".format(page))
280 281 282 283 284
            self.check_page_get(page, expected=302)

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

bmedx committed
285
        print('Logged in')
286
        for page in simple_auth_pages:
bmedx committed
287
            print("Checking '{0}'".format(page))
288 289 290 291 292
            self.check_page_get(page, expected=200)

    def test_index_auth(self):

        # not logged in.  Should return a redirect.
293
        resp = self.client.get_html('/home/')
294 295 296
        self.assertEqual(resp.status_code, 302)

        # Logged in should work.
297

298 299 300 301 302 303 304 305 306 307 308 309
    @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
310
        course_url = '/home/'
311
        resp = self.client.get_html(course_url)
312 313 314 315 316
        self.assertEquals(resp.status_code, 200)

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

317
        resp = self.client.get_html(course_url)
318 319

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

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    @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)

350 351 352 353 354 355 356 357 358

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
359 360 361 362 363
        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]
364
        self.assertTrue(self.course.forum_posts_allowed)
David Baumgold committed
365 366 367 368 369
        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]
370
        self.assertFalse(self.course.forum_posts_allowed)
371

372 373 374 375
        # 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)

376 377 378 379 380 381 382 383 384 385

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

386
    @data(('edX/test_course_key/Test_Course', 200), ('garbage:edX+test_course_key+Test_Course', 404))
387 388 389 390 391 392 393 394 395
    @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)

396
        url = '/import_status/{course_key}/{filename}'.format(
397 398 399 400 401
            course_key=course_key,
            filename='xyz.tar.gz'
        )
        resp = self.client.get_html(url)
        self.assertEqual(resp.status_code, status_code)
402 403 404 405 406 407 408 409 410 411 412 413


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