Move db/util.py -> db/mongo/util.py
[mediagoblin.git] / mediagoblin / tests / test_auth.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
12a100e4 2# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
4b5f4e87
CAW
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
1972a888 17import urlparse
65a83047 18import datetime
4b5f4e87 19
1972a888 20from nose.tools import assert_equal
4b5f4e87 21
1972a888 22from mediagoblin.auth import lib as auth_lib
9754802d 23from mediagoblin.tests.tools import setup_fresh_app, fixture_add_user
6e7ce8d1 24from mediagoblin import mg_globals
152a3bfa 25from mediagoblin.tools import template, mail
460ce564 26
4b5f4e87
CAW
27
28########################
29# Test bcrypt auth funcs
30########################
31
32def 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
db780024
CAW
38 assert not auth_lib.bcrypt_check_password(
39 'notthepassword',
40 '$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
41
42
4b5f4e87 43 # Same thing, but with extra fake salt.
db780024
CAW
44 assert not auth_lib.bcrypt_check_password(
45 'notthepassword',
4b5f4e87
CAW
46 '$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6',
47 '3><7R45417')
48
49
50def 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)
db780024
CAW
57 assert not auth_lib.bcrypt_check_password(
58 'notthepassword', hashed_pw)
59
4b5f4e87
CAW
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')
db780024
CAW
65 assert not auth_lib.bcrypt_check_password(
66 'notthepassword', hashed_pw, '3><7R45417')
460ce564
CAW
67
68
3aa4c668
CAW
69@setup_fresh_app
70def test_register_views(test_app):
2fecc29d
CAW
71 """
72 Massive test function that all our registration-related views all work.
73 """
460ce564 74 # Test doing a simple GET on the page
651403f0
CAW
75 # -----------------------------------
76
460ce564
CAW
77 test_app.get('/auth/register/')
78 # Make sure it rendered with the appropriate template
ae3bc7fa 79 assert template.TEMPLATE_TEST_CONTEXT.has_key(
460ce564 80 'mediagoblin/auth/register.html')
757690cc 81
460ce564 82 # Try to register without providing anything, should error
651403f0
CAW
83 # --------------------------------------------------------
84
ae3bc7fa 85 template.clear_test_template_context()
460ce564
CAW
86 test_app.post(
87 '/auth/register/', {})
ae3bc7fa 88 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
460ce564
CAW
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.']
651403f0
CAW
94
95 # Try to register with fields that are known to be invalid
96 # --------------------------------------------------------
97
98 ## too short
ae3bc7fa 99 template.clear_test_template_context()
651403f0
CAW
100 test_app.post(
101 '/auth/register/', {
102 'username': 'l',
103 'password': 'o',
104 'confirm_password': 'o',
105 'email': 'l'})
ae3bc7fa 106 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
651403f0
CAW
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
ae3bc7fa 115 template.clear_test_template_context()
651403f0
CAW
116 test_app.post(
117 '/auth/register/', {
118 'username': '@_@',
119 'email': 'lollerskates'})
ae3bc7fa 120 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
651403f0
CAW
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
ae3bc7fa 129 template.clear_test_template_context()
651403f0
CAW
130 test_app.post(
131 '/auth/register/', {
132 'password': 'herpderp',
133 'confirm_password': 'derpherp'})
ae3bc7fa 134 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
651403f0
CAW
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 ;)
6e7ce8d1 141 assert not mg_globals.database.User.find().count()
651403f0
CAW
142
143 # Successful register
144 # -------------------
ae3bc7fa 145 template.clear_test_template_context()
1972a888
CAW
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
651403f0 154 ## Did we redirect to the proper page? Use the right template?
1972a888
CAW
155 assert_equal(
156 urlparse.urlsplit(response.location)[2],
0bc03620 157 '/u/happygirl/')
ae3bc7fa 158 assert template.TEMPLATE_TEST_CONTEXT.has_key(
0bc03620 159 'mediagoblin/user_pages/user.html')
1972a888 160
651403f0 161 ## Make sure user is in place
6e7ce8d1 162 new_user = mg_globals.database.User.find_one(
1972a888
CAW
163 {'username': 'happygirl'})
164 assert new_user
7a3d00ec 165 assert new_user.status == u'needs_email_verification'
4facc7a0 166 assert new_user.email_verified == False
1972a888 167
f73f4c4b 168 ## Make sure user is logged in
ae3bc7fa 169 request = template.TEMPLATE_TEST_CONTEXT[
f73f4c4b 170 'mediagoblin/user_pages/user.html']['request']
eabe6b67 171 assert request.session['user_id'] == unicode(new_user._id)
f73f4c4b 172
1972a888 173 ## Make sure we get email confirmation, and try verifying
152a3bfa
AW
174 assert len(mail.EMAIL_TEST_INBOX) == 1
175 message = mail.EMAIL_TEST_INBOX.pop()
1972a888 176 assert message['To'] == 'happygrrl@example.org'
ae3bc7fa 177 email_context = template.TEMPLATE_TEST_CONTEXT[
1972a888
CAW
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'] == [
eabe6b67 188 unicode(new_user._id)]
1972a888 189 assert parsed_get_params['token'] == [
00bb9550 190 new_user.verification_key]
757690cc 191
7b1e17ed 192 ## Try verifying with bs verification key, shouldn't work
ae3bc7fa 193 template.clear_test_template_context()
a656ccd5 194 response = test_app.get(
7b1e17ed 195 "/auth/verify_email/?userid=%s&token=total_bs" % unicode(
eabe6b67 196 new_user._id))
a656ccd5 197 response.follow()
ae3bc7fa 198 context = template.TEMPLATE_TEST_CONTEXT[
e054ae9b 199 'mediagoblin/user_pages/user.html']
a656ccd5
CAW
200 # assert context['verification_successful'] == True
201 # TODO: Would be good to test messages here when we can do so...
6e7ce8d1 202 new_user = mg_globals.database.User.find_one(
7b1e17ed
CAW
203 {'username': 'happygirl'})
204 assert new_user
7a3d00ec 205 assert new_user.status == u'needs_email_verification'
4facc7a0 206 assert new_user.email_verified == False
7b1e17ed
CAW
207
208 ## Verify the email activation works
ae3bc7fa 209 template.clear_test_template_context()
a656ccd5
CAW
210 response = test_app.get("%s?%s" % (path, get_params))
211 response.follow()
ae3bc7fa 212 context = template.TEMPLATE_TEST_CONTEXT[
e054ae9b 213 'mediagoblin/user_pages/user.html']
a656ccd5
CAW
214 # assert context['verification_successful'] == True
215 # TODO: Would be good to test messages here when we can do so...
6e7ce8d1 216 new_user = mg_globals.database.User.find_one(
7b1e17ed
CAW
217 {'username': 'happygirl'})
218 assert new_user
7a3d00ec 219 assert new_user.status == u'active'
4facc7a0 220 assert new_user.email_verified == True
1972a888 221
cb9bac0c
CAW
222 # Uniqueness checks
223 # -----------------
224 ## We shouldn't be able to register with that user twice
ae3bc7fa 225 template.clear_test_template_context()
8a869db8
CAW
226 response = test_app.post(
227 '/auth/register/', {
228 'username': 'happygirl',
229 'password': 'iamsohappy2',
230 'confirm_password': 'iamsohappy2',
231 'email': 'happygrrl2@example.org'})
757690cc 232
ae3bc7fa 233 context = template.TEMPLATE_TEST_CONTEXT[
8a869db8
CAW
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.']
651403f0 238
8a869db8 239 ## TODO: Also check for double instances of an email address?
757690cc 240
65a83047
CFD
241 ### Oops, forgot the password
242 # -------------------
ae3bc7fa 243 template.clear_test_template_context()
f03fef4e
CAW
244 response = test_app.post(
245 '/auth/forgot_password/',
246 {'username': 'happygirl'})
65a83047
CFD
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],
f03fef4e 252 '/auth/forgot_password/email_sent/')
ae3bc7fa 253 assert template.TEMPLATE_TEST_CONTEXT.has_key(
65a83047
CFD
254 'mediagoblin/auth/fp_email_sent.html')
255
256 ## Make sure link to change password is sent by email
152a3bfa
AW
257 assert len(mail.EMAIL_TEST_INBOX) == 1
258 message = mail.EMAIL_TEST_INBOX.pop()
65a83047 259 assert message['To'] == 'happygrrl@example.org'
ae3bc7fa 260 email_context = template.TEMPLATE_TEST_CONTEXT[
65a83047
CFD
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]
f03fef4e 267 assert path == u'/auth/forgot_password/verify/'
65a83047
CFD
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'})
eabe6b67 272 assert parsed_get_params['userid'] == [unicode(new_user._id)]
dc39e455 273 assert parsed_get_params['token'] == [new_user.fp_verification_key]
65a83047
CFD
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...
2d540fed 277 assert (new_user.fp_token_expire - datetime.datetime.now()).days == 9
65a83047
CFD
278
279 ## Try using a bs password-changing verification key, shouldn't work
ae3bc7fa 280 template.clear_test_template_context()
65a83047 281 response = test_app.get(
f03fef4e 282 "/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode(
93e46224
E
283 new_user._id), status=404)
284 assert_equal(response.status, '404 Not Found')
65a83047 285
4bcaf9f3 286 ## Try using an expired token to change password, shouldn't work
ae3bc7fa 287 template.clear_test_template_context()
2d540fed
E
288 real_token_expiration = new_user.fp_token_expire
289 new_user.fp_token_expire = datetime.datetime.now()
4bcaf9f3 290 new_user.save()
93e46224
E
291 response = test_app.get("%s?%s" % (path, get_params), status=404)
292 assert_equal(response.status, '404 Not Found')
2d540fed 293 new_user.fp_token_expire = real_token_expiration
4bcaf9f3
CFD
294 new_user.save()
295
65a83047 296 ## Verify step 1 of password-change works -- can see form to change password
ae3bc7fa 297 template.clear_test_template_context()
65a83047 298 response = test_app.get("%s?%s" % (path, get_params))
ae3bc7fa 299 assert template.TEMPLATE_TEST_CONTEXT.has_key('mediagoblin/auth/change_fp.html')
65a83047
CFD
300
301 ## Verify step 2.1 of password-change works -- report success to user
ae3bc7fa 302 template.clear_test_template_context()
65a83047 303 response = test_app.post(
f03fef4e 304 '/auth/forgot_password/verify/', {
65a83047
CFD
305 'userid': parsed_get_params['userid'],
306 'password': 'iamveryveryhappy',
307 'confirm_password': 'iamveryveryhappy',
308 'token': parsed_get_params['token']})
309 response.follow()
ae3bc7fa 310 assert template.TEMPLATE_TEST_CONTEXT.has_key(
f03fef4e 311 'mediagoblin/auth/fp_changed_success.html')
65a83047
CFD
312
313 ## Verify step 2.2 of password-change works -- login w/ new password success
ae3bc7fa 314 template.clear_test_template_context()
65a83047
CFD
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 '/')
ae3bc7fa 325 assert template.TEMPLATE_TEST_CONTEXT.has_key(
65a83047
CFD
326 'mediagoblin/root.html')
327
757690cc
CM
328
329@setup_fresh_app
330def test_authentication_views(test_app):
331 """
332 Test logging in and logging out
333 """
334 # Make a new user
9754802d 335 test_user = fixture_add_user(active_user=False)
757690cc
CM
336
337 # Get login
0a4cecdc 338 # ---------
757690cc 339 test_app.get('/auth/login/')
ae3bc7fa 340 assert template.TEMPLATE_TEST_CONTEXT.has_key(
757690cc
CM
341 'mediagoblin/auth/login.html')
342
0a4cecdc
CM
343 # Failed login - blank form
344 # -------------------------
ae3bc7fa 345 template.clear_test_template_context()
0a4cecdc 346 response = test_app.post('/auth/login/')
ae3bc7fa 347 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
0a4cecdc
CM
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 # -------------------------
ae3bc7fa 354 template.clear_test_template_context()
0a4cecdc
CM
355 response = test_app.post(
356 '/auth/login/', {
357 'password': u'toast'})
ae3bc7fa 358 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
0a4cecdc
CM
359 form = context['login_form']
360 assert form.username.errors == [u'This field is required.']
361
362 # Failed login - blank password
363 # -----------------------------
ae3bc7fa 364 template.clear_test_template_context()
0a4cecdc
CM
365 response = test_app.post(
366 '/auth/login/', {
367 'username': u'chris'})
ae3bc7fa 368 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
0a4cecdc
CM
369 form = context['login_form']
370 assert form.password.errors == [u'This field is required.']
371
372 # Failed login - bad user
373 # -----------------------
ae3bc7fa 374 template.clear_test_template_context()
0a4cecdc
CM
375 response = test_app.post(
376 '/auth/login/', {
377 'username': u'steve',
378 'password': 'toast'})
ae3bc7fa 379 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
0a4cecdc
CM
380 assert context['login_failed']
381
382 # Failed login - bad password
383 # ---------------------------
ae3bc7fa 384 template.clear_test_template_context()
0a4cecdc
CM
385 response = test_app.post(
386 '/auth/login/', {
387 'username': u'chris',
388 'password': 'jam'})
ae3bc7fa 389 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
0a4cecdc
CM
390 assert context['login_failed']
391
392 # Successful login
393 # ----------------
ae3bc7fa 394 template.clear_test_template_context()
757690cc
CM
395 response = test_app.post(
396 '/auth/login/', {
397 'username': u'chris',
398 'password': 'toast'})
0a4cecdc
CM
399
400 # User should be redirected
757690cc
CM
401 response.follow()
402 assert_equal(
403 urlparse.urlsplit(response.location)[2],
404 '/')
ae3bc7fa 405 assert template.TEMPLATE_TEST_CONTEXT.has_key(
757690cc
CM
406 'mediagoblin/root.html')
407
0a4cecdc 408 # Make sure user is in the session
ae3bc7fa 409 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
0a4cecdc 410 session = context['request'].session
eabe6b67 411 assert session['user_id'] == unicode(test_user._id)
757690cc 412
0a4cecdc
CM
413 # Successful logout
414 # -----------------
ae3bc7fa 415 template.clear_test_template_context()
0a4cecdc
CM
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 '/')
ae3bc7fa 423 assert template.TEMPLATE_TEST_CONTEXT.has_key(
0a4cecdc
CM
424 'mediagoblin/root.html')
425
426 # Make sure the user is not in the session
ae3bc7fa 427 context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
0a4cecdc
CM
428 session = context['request'].session
429 assert session.has_key('user_id') == False
757690cc 430
12c231c8
CM
431 # User is redirected to custom URL if POST['next'] is set
432 # -------------------------------------------------------
ae3bc7fa 433 template.clear_test_template_context()
12c231c8
CM
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