| 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
| 2 | # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS. |
| 3 | # |
| 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU Affero General Public License as published by |
| 6 | # the Free Software Foundation, either version 3 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU Affero General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU Affero General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | import urlparse |
| 18 | import datetime |
| 19 | |
| 20 | from nose.tools import assert_equal |
| 21 | |
| 22 | from mediagoblin.auth import lib as auth_lib |
| 23 | from mediagoblin.tests.tools import setup_fresh_app, fixture_add_user |
| 24 | from mediagoblin import mg_globals |
| 25 | from mediagoblin.tools import template, mail |
| 26 | |
| 27 | |
| 28 | ######################## |
| 29 | # Test bcrypt auth funcs |
| 30 | ######################## |
| 31 | |
| 32 | def test_bcrypt_check_password(): |
| 33 | # Check known 'lollerskates' password against check function |
| 34 | assert auth_lib.bcrypt_check_password( |
| 35 | 'lollerskates', |
| 36 | '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') |
| 37 | |
| 38 | assert not auth_lib.bcrypt_check_password( |
| 39 | 'notthepassword', |
| 40 | '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO') |
| 41 | |
| 42 | |
| 43 | # Same thing, but with extra fake salt. |
| 44 | assert not auth_lib.bcrypt_check_password( |
| 45 | 'notthepassword', |
| 46 | '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6', |
| 47 | '3><7R45417') |
| 48 | |
| 49 | |
| 50 | def test_bcrypt_gen_password_hash(): |
| 51 | pw = 'youwillneverguessthis' |
| 52 | |
| 53 | # Normal password hash generation, and check on that hash |
| 54 | hashed_pw = auth_lib.bcrypt_gen_password_hash(pw) |
| 55 | assert auth_lib.bcrypt_check_password( |
| 56 | pw, hashed_pw) |
| 57 | assert not auth_lib.bcrypt_check_password( |
| 58 | 'notthepassword', hashed_pw) |
| 59 | |
| 60 | |
| 61 | # Same thing, extra salt. |
| 62 | hashed_pw = auth_lib.bcrypt_gen_password_hash(pw, '3><7R45417') |
| 63 | assert auth_lib.bcrypt_check_password( |
| 64 | pw, hashed_pw, '3><7R45417') |
| 65 | assert not auth_lib.bcrypt_check_password( |
| 66 | 'notthepassword', hashed_pw, '3><7R45417') |
| 67 | |
| 68 | |
| 69 | @setup_fresh_app |
| 70 | def test_register_views(test_app): |
| 71 | """ |
| 72 | Massive test function that all our registration-related views all work. |
| 73 | """ |
| 74 | # Test doing a simple GET on the page |
| 75 | # ----------------------------------- |
| 76 | |
| 77 | test_app.get('/auth/register/') |
| 78 | # Make sure it rendered with the appropriate template |
| 79 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 80 | 'mediagoblin/auth/register.html') |
| 81 | |
| 82 | # Try to register without providing anything, should error |
| 83 | # -------------------------------------------------------- |
| 84 | |
| 85 | template.clear_test_template_context() |
| 86 | test_app.post( |
| 87 | '/auth/register/', {}) |
| 88 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] |
| 89 | form = context['register_form'] |
| 90 | assert form.username.errors == [u'This field is required.'] |
| 91 | assert form.password.errors == [u'This field is required.'] |
| 92 | assert form.confirm_password.errors == [u'This field is required.'] |
| 93 | assert form.email.errors == [u'This field is required.'] |
| 94 | |
| 95 | # Try to register with fields that are known to be invalid |
| 96 | # -------------------------------------------------------- |
| 97 | |
| 98 | ## too short |
| 99 | template.clear_test_template_context() |
| 100 | test_app.post( |
| 101 | '/auth/register/', { |
| 102 | 'username': 'l', |
| 103 | 'password': 'o', |
| 104 | 'confirm_password': 'o', |
| 105 | 'email': 'l'}) |
| 106 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] |
| 107 | form = context['register_form'] |
| 108 | |
| 109 | assert form.username.errors == [ |
| 110 | u'Field must be between 3 and 30 characters long.'] |
| 111 | assert form.password.errors == [ |
| 112 | u'Field must be between 6 and 30 characters long.'] |
| 113 | |
| 114 | ## bad form |
| 115 | template.clear_test_template_context() |
| 116 | test_app.post( |
| 117 | '/auth/register/', { |
| 118 | 'username': '@_@', |
| 119 | 'email': 'lollerskates'}) |
| 120 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] |
| 121 | form = context['register_form'] |
| 122 | |
| 123 | assert form.username.errors == [ |
| 124 | u'Invalid input.'] |
| 125 | assert form.email.errors == [ |
| 126 | u'Invalid email address.'] |
| 127 | |
| 128 | ## mismatching passwords |
| 129 | template.clear_test_template_context() |
| 130 | test_app.post( |
| 131 | '/auth/register/', { |
| 132 | 'password': 'herpderp', |
| 133 | 'confirm_password': 'derpherp'}) |
| 134 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] |
| 135 | form = context['register_form'] |
| 136 | |
| 137 | assert form.password.errors == [ |
| 138 | u'Passwords must match.'] |
| 139 | |
| 140 | ## At this point there should be no users in the database ;) |
| 141 | assert not mg_globals.database.User.find().count() |
| 142 | |
| 143 | # Successful register |
| 144 | # ------------------- |
| 145 | template.clear_test_template_context() |
| 146 | response = test_app.post( |
| 147 | '/auth/register/', { |
| 148 | 'username': 'happygirl', |
| 149 | 'password': 'iamsohappy', |
| 150 | 'confirm_password': 'iamsohappy', |
| 151 | 'email': 'happygrrl@example.org'}) |
| 152 | response.follow() |
| 153 | |
| 154 | ## Did we redirect to the proper page? Use the right template? |
| 155 | assert_equal( |
| 156 | urlparse.urlsplit(response.location)[2], |
| 157 | '/u/happygirl/') |
| 158 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 159 | 'mediagoblin/user_pages/user.html') |
| 160 | |
| 161 | ## Make sure user is in place |
| 162 | new_user = mg_globals.database.User.find_one( |
| 163 | {'username': 'happygirl'}) |
| 164 | assert new_user |
| 165 | assert new_user.status == u'needs_email_verification' |
| 166 | assert new_user.email_verified == False |
| 167 | |
| 168 | ## Make sure user is logged in |
| 169 | request = template.TEMPLATE_TEST_CONTEXT[ |
| 170 | 'mediagoblin/user_pages/user.html']['request'] |
| 171 | assert request.session['user_id'] == unicode(new_user._id) |
| 172 | |
| 173 | ## Make sure we get email confirmation, and try verifying |
| 174 | assert len(mail.EMAIL_TEST_INBOX) == 1 |
| 175 | message = mail.EMAIL_TEST_INBOX.pop() |
| 176 | assert message['To'] == 'happygrrl@example.org' |
| 177 | email_context = template.TEMPLATE_TEST_CONTEXT[ |
| 178 | 'mediagoblin/auth/verification_email.txt'] |
| 179 | assert email_context['verification_url'] in message.get_payload(decode=True) |
| 180 | |
| 181 | path = urlparse.urlsplit(email_context['verification_url'])[2] |
| 182 | get_params = urlparse.urlsplit(email_context['verification_url'])[3] |
| 183 | assert path == u'/auth/verify_email/' |
| 184 | parsed_get_params = urlparse.parse_qs(get_params) |
| 185 | |
| 186 | ### user should have these same parameters |
| 187 | assert parsed_get_params['userid'] == [ |
| 188 | unicode(new_user._id)] |
| 189 | assert parsed_get_params['token'] == [ |
| 190 | new_user.verification_key] |
| 191 | |
| 192 | ## Try verifying with bs verification key, shouldn't work |
| 193 | template.clear_test_template_context() |
| 194 | response = test_app.get( |
| 195 | "/auth/verify_email/?userid=%s&token=total_bs" % unicode( |
| 196 | new_user._id)) |
| 197 | response.follow() |
| 198 | context = template.TEMPLATE_TEST_CONTEXT[ |
| 199 | 'mediagoblin/user_pages/user.html'] |
| 200 | # assert context['verification_successful'] == True |
| 201 | # TODO: Would be good to test messages here when we can do so... |
| 202 | new_user = mg_globals.database.User.find_one( |
| 203 | {'username': 'happygirl'}) |
| 204 | assert new_user |
| 205 | assert new_user.status == u'needs_email_verification' |
| 206 | assert new_user.email_verified == False |
| 207 | |
| 208 | ## Verify the email activation works |
| 209 | template.clear_test_template_context() |
| 210 | response = test_app.get("%s?%s" % (path, get_params)) |
| 211 | response.follow() |
| 212 | context = template.TEMPLATE_TEST_CONTEXT[ |
| 213 | 'mediagoblin/user_pages/user.html'] |
| 214 | # assert context['verification_successful'] == True |
| 215 | # TODO: Would be good to test messages here when we can do so... |
| 216 | new_user = mg_globals.database.User.find_one( |
| 217 | {'username': 'happygirl'}) |
| 218 | assert new_user |
| 219 | assert new_user.status == u'active' |
| 220 | assert new_user.email_verified == True |
| 221 | |
| 222 | # Uniqueness checks |
| 223 | # ----------------- |
| 224 | ## We shouldn't be able to register with that user twice |
| 225 | template.clear_test_template_context() |
| 226 | response = test_app.post( |
| 227 | '/auth/register/', { |
| 228 | 'username': 'happygirl', |
| 229 | 'password': 'iamsohappy2', |
| 230 | 'confirm_password': 'iamsohappy2', |
| 231 | 'email': 'happygrrl2@example.org'}) |
| 232 | |
| 233 | context = template.TEMPLATE_TEST_CONTEXT[ |
| 234 | 'mediagoblin/auth/register.html'] |
| 235 | form = context['register_form'] |
| 236 | assert form.username.errors == [ |
| 237 | u'Sorry, a user with that name already exists.'] |
| 238 | |
| 239 | ## TODO: Also check for double instances of an email address? |
| 240 | |
| 241 | ### Oops, forgot the password |
| 242 | # ------------------- |
| 243 | template.clear_test_template_context() |
| 244 | response = test_app.post( |
| 245 | '/auth/forgot_password/', |
| 246 | {'username': 'happygirl'}) |
| 247 | response.follow() |
| 248 | |
| 249 | ## Did we redirect to the proper page? Use the right template? |
| 250 | assert_equal( |
| 251 | urlparse.urlsplit(response.location)[2], |
| 252 | '/auth/forgot_password/email_sent/') |
| 253 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 254 | 'mediagoblin/auth/fp_email_sent.html') |
| 255 | |
| 256 | ## Make sure link to change password is sent by email |
| 257 | assert len(mail.EMAIL_TEST_INBOX) == 1 |
| 258 | message = mail.EMAIL_TEST_INBOX.pop() |
| 259 | assert message['To'] == 'happygrrl@example.org' |
| 260 | email_context = template.TEMPLATE_TEST_CONTEXT[ |
| 261 | 'mediagoblin/auth/fp_verification_email.txt'] |
| 262 | #TODO - change the name of verification_url to something forgot-password-ish |
| 263 | assert email_context['verification_url'] in message.get_payload(decode=True) |
| 264 | |
| 265 | path = urlparse.urlsplit(email_context['verification_url'])[2] |
| 266 | get_params = urlparse.urlsplit(email_context['verification_url'])[3] |
| 267 | assert path == u'/auth/forgot_password/verify/' |
| 268 | parsed_get_params = urlparse.parse_qs(get_params) |
| 269 | |
| 270 | # user should have matching parameters |
| 271 | new_user = mg_globals.database.User.find_one({'username': 'happygirl'}) |
| 272 | assert parsed_get_params['userid'] == [unicode(new_user._id)] |
| 273 | assert parsed_get_params['token'] == [new_user.fp_verification_key] |
| 274 | |
| 275 | ### The forgotten password token should be set to expire in ~ 10 days |
| 276 | # A few ticks have expired so there are only 9 full days left... |
| 277 | assert (new_user.fp_token_expire - datetime.datetime.now()).days == 9 |
| 278 | |
| 279 | ## Try using a bs password-changing verification key, shouldn't work |
| 280 | template.clear_test_template_context() |
| 281 | response = test_app.get( |
| 282 | "/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode( |
| 283 | new_user._id), status=404) |
| 284 | assert_equal(response.status, '404 Not Found') |
| 285 | |
| 286 | ## Try using an expired token to change password, shouldn't work |
| 287 | template.clear_test_template_context() |
| 288 | real_token_expiration = new_user.fp_token_expire |
| 289 | new_user.fp_token_expire = datetime.datetime.now() |
| 290 | new_user.save() |
| 291 | response = test_app.get("%s?%s" % (path, get_params), status=404) |
| 292 | assert_equal(response.status, '404 Not Found') |
| 293 | new_user.fp_token_expire = real_token_expiration |
| 294 | new_user.save() |
| 295 | |
| 296 | ## Verify step 1 of password-change works -- can see form to change password |
| 297 | template.clear_test_template_context() |
| 298 | response = test_app.get("%s?%s" % (path, get_params)) |
| 299 | assert template.TEMPLATE_TEST_CONTEXT.has_key('mediagoblin/auth/change_fp.html') |
| 300 | |
| 301 | ## Verify step 2.1 of password-change works -- report success to user |
| 302 | template.clear_test_template_context() |
| 303 | response = test_app.post( |
| 304 | '/auth/forgot_password/verify/', { |
| 305 | 'userid': parsed_get_params['userid'], |
| 306 | 'password': 'iamveryveryhappy', |
| 307 | 'confirm_password': 'iamveryveryhappy', |
| 308 | 'token': parsed_get_params['token']}) |
| 309 | response.follow() |
| 310 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 311 | 'mediagoblin/auth/fp_changed_success.html') |
| 312 | |
| 313 | ## Verify step 2.2 of password-change works -- login w/ new password success |
| 314 | template.clear_test_template_context() |
| 315 | response = test_app.post( |
| 316 | '/auth/login/', { |
| 317 | 'username': u'happygirl', |
| 318 | 'password': 'iamveryveryhappy'}) |
| 319 | |
| 320 | # User should be redirected |
| 321 | response.follow() |
| 322 | assert_equal( |
| 323 | urlparse.urlsplit(response.location)[2], |
| 324 | '/') |
| 325 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 326 | 'mediagoblin/root.html') |
| 327 | |
| 328 | |
| 329 | @setup_fresh_app |
| 330 | def test_authentication_views(test_app): |
| 331 | """ |
| 332 | Test logging in and logging out |
| 333 | """ |
| 334 | # Make a new user |
| 335 | test_user = fixture_add_user(active_user=False) |
| 336 | |
| 337 | # Get login |
| 338 | # --------- |
| 339 | test_app.get('/auth/login/') |
| 340 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 341 | 'mediagoblin/auth/login.html') |
| 342 | |
| 343 | # Failed login - blank form |
| 344 | # ------------------------- |
| 345 | template.clear_test_template_context() |
| 346 | response = test_app.post('/auth/login/') |
| 347 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] |
| 348 | form = context['login_form'] |
| 349 | assert form.username.errors == [u'This field is required.'] |
| 350 | assert form.password.errors == [u'This field is required.'] |
| 351 | |
| 352 | # Failed login - blank user |
| 353 | # ------------------------- |
| 354 | template.clear_test_template_context() |
| 355 | response = test_app.post( |
| 356 | '/auth/login/', { |
| 357 | 'password': u'toast'}) |
| 358 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] |
| 359 | form = context['login_form'] |
| 360 | assert form.username.errors == [u'This field is required.'] |
| 361 | |
| 362 | # Failed login - blank password |
| 363 | # ----------------------------- |
| 364 | template.clear_test_template_context() |
| 365 | response = test_app.post( |
| 366 | '/auth/login/', { |
| 367 | 'username': u'chris'}) |
| 368 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] |
| 369 | form = context['login_form'] |
| 370 | assert form.password.errors == [u'This field is required.'] |
| 371 | |
| 372 | # Failed login - bad user |
| 373 | # ----------------------- |
| 374 | template.clear_test_template_context() |
| 375 | response = test_app.post( |
| 376 | '/auth/login/', { |
| 377 | 'username': u'steve', |
| 378 | 'password': 'toast'}) |
| 379 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] |
| 380 | assert context['login_failed'] |
| 381 | |
| 382 | # Failed login - bad password |
| 383 | # --------------------------- |
| 384 | template.clear_test_template_context() |
| 385 | response = test_app.post( |
| 386 | '/auth/login/', { |
| 387 | 'username': u'chris', |
| 388 | 'password': 'jam'}) |
| 389 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] |
| 390 | assert context['login_failed'] |
| 391 | |
| 392 | # Successful login |
| 393 | # ---------------- |
| 394 | template.clear_test_template_context() |
| 395 | response = test_app.post( |
| 396 | '/auth/login/', { |
| 397 | 'username': u'chris', |
| 398 | 'password': 'toast'}) |
| 399 | |
| 400 | # User should be redirected |
| 401 | response.follow() |
| 402 | assert_equal( |
| 403 | urlparse.urlsplit(response.location)[2], |
| 404 | '/') |
| 405 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 406 | 'mediagoblin/root.html') |
| 407 | |
| 408 | # Make sure user is in the session |
| 409 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] |
| 410 | session = context['request'].session |
| 411 | assert session['user_id'] == unicode(test_user._id) |
| 412 | |
| 413 | # Successful logout |
| 414 | # ----------------- |
| 415 | template.clear_test_template_context() |
| 416 | response = test_app.get('/auth/logout/') |
| 417 | |
| 418 | # Should be redirected to index page |
| 419 | response.follow() |
| 420 | assert_equal( |
| 421 | urlparse.urlsplit(response.location)[2], |
| 422 | '/') |
| 423 | assert template.TEMPLATE_TEST_CONTEXT.has_key( |
| 424 | 'mediagoblin/root.html') |
| 425 | |
| 426 | # Make sure the user is not in the session |
| 427 | context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] |
| 428 | session = context['request'].session |
| 429 | assert session.has_key('user_id') == False |
| 430 | |
| 431 | # User is redirected to custom URL if POST['next'] is set |
| 432 | # ------------------------------------------------------- |
| 433 | template.clear_test_template_context() |
| 434 | response = test_app.post( |
| 435 | '/auth/login/', { |
| 436 | 'username': u'chris', |
| 437 | 'password': 'toast', |
| 438 | 'next' : '/u/chris/'}) |
| 439 | assert_equal( |
| 440 | urlparse.urlsplit(response.location)[2], |
| 441 | '/u/chris/') |
| 442 | |