bfa66bd233d630d1316d10fea2138246629a1482
[mediagoblin.git] / mediagoblin / tests / test_auth.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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
24 from mediagoblin import mg_globals
25 from mediagoblin import util
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 util.TEMPLATE_TEST_CONTEXT.has_key(
80 'mediagoblin/auth/register.html')
81
82 # Try to register without providing anything, should error
83 # --------------------------------------------------------
84
85 util.clear_test_template_context()
86 test_app.post(
87 '/auth/register/', {})
88 context = util.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 util.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 = util.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 util.clear_test_template_context()
116 test_app.post(
117 '/auth/register/', {
118 'username': '@_@',
119 'email': 'lollerskates'})
120 context = util.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 util.clear_test_template_context()
130 test_app.post(
131 '/auth/register/', {
132 'password': 'herpderp',
133 'confirm_password': 'derpherp'})
134 context = util.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 util.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 util.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 = util.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(util.EMAIL_TEST_INBOX) == 1
175 message = util.EMAIL_TEST_INBOX.pop()
176 assert message['To'] == 'happygrrl@example.org'
177 email_context = util.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 util.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 = util.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 util.clear_test_template_context()
210 response = test_app.get("%s?%s" % (path, get_params))
211 response.follow()
212 context = util.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 util.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 = util.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 util.clear_test_template_context()
244 response = test_app.post('/auth/forgotpass/', {'username': 'happygirl'})
245 response.follow()
246
247 ## Did we redirect to the proper page? Use the right template?
248 assert_equal(
249 urlparse.urlsplit(response.location)[2],
250 '/auth/fp_email_sent/')
251 assert util.TEMPLATE_TEST_CONTEXT.has_key(
252 'mediagoblin/auth/fp_email_sent.html')
253
254 ## Make sure link to change password is sent by email
255 assert len(util.EMAIL_TEST_INBOX) == 1
256 message = util.EMAIL_TEST_INBOX.pop()
257 assert message['To'] == 'happygrrl@example.org'
258 email_context = util.TEMPLATE_TEST_CONTEXT[
259 'mediagoblin/auth/fp_verification_email.txt']
260 #TODO - change the name of verification_url to something forgot-password-ish
261 assert email_context['verification_url'] in message.get_payload(decode=True)
262
263 path = urlparse.urlsplit(email_context['verification_url'])[2]
264 get_params = urlparse.urlsplit(email_context['verification_url'])[3]
265 assert path == u'/auth/verifyforgotpass/'
266 parsed_get_params = urlparse.parse_qs(get_params)
267
268 # user should have matching parameters
269 new_user = mg_globals.database.User.find_one({'username': 'happygirl'})
270 assert parsed_get_params['userid'] == [unicode(new_user['_id'])]
271 assert parsed_get_params['token'] == [new_user['fp_verification_key']]
272
273 ### The forgotten password token should be set to expire in ~ 10 days
274 # A few ticks have expired so there are only 9 full days left...
275 assert (new_user['fp_token_expire'] - datetime.datetime.now()).days == 9
276
277 ## Try using a bs password-changing verification key, shouldn't work
278 util.clear_test_template_context()
279 response = test_app.get(
280 "/auth/verifyforgotpass/?userid=%s&token=total_bs" % unicode(
281 new_user['_id']), status=400)
282 assert response.status == '400 Bad Request'
283
284 ## Try using an expired token to change password, shouldn't work
285 util.clear_test_template_context()
286 real_token_expiration = new_user['fp_token_expire']
287 new_user['fp_token_expire'] = datetime.datetime.now()
288 new_user.save()
289 response = test_app.get("%s?%s" % (path, get_params), status=400)
290 assert response.status == '400 Bad Request'
291 new_user['fp_token_expire'] = real_token_expiration
292 new_user.save()
293
294 ## Verify step 1 of password-change works -- can see form to change password
295 util.clear_test_template_context()
296 response = test_app.get("%s?%s" % (path, get_params))
297 assert util.TEMPLATE_TEST_CONTEXT.has_key('mediagoblin/auth/change_fp.html')
298
299 ## Verify step 2.1 of password-change works -- report success to user
300 util.clear_test_template_context()
301 response = test_app.post(
302 '/auth/verifyforgotpass/', {
303 'userid': parsed_get_params['userid'],
304 'password': 'iamveryveryhappy',
305 'confirm_password': 'iamveryveryhappy',
306 'token': parsed_get_params['token']})
307 response.follow()
308 assert util.TEMPLATE_TEST_CONTEXT.has_key(
309 'mediagoblin/auth/fp_changed_success.html')
310
311 ## Verify step 2.2 of password-change works -- login w/ new password success
312 util.clear_test_template_context()
313 response = test_app.post(
314 '/auth/login/', {
315 'username': u'happygirl',
316 'password': 'iamveryveryhappy'})
317
318 # User should be redirected
319 response.follow()
320 assert_equal(
321 urlparse.urlsplit(response.location)[2],
322 '/')
323 assert util.TEMPLATE_TEST_CONTEXT.has_key(
324 'mediagoblin/root.html')
325
326
327 @setup_fresh_app
328 def test_authentication_views(test_app):
329 """
330 Test logging in and logging out
331 """
332 # Make a new user
333 test_user = mg_globals.database.User()
334 test_user['username'] = u'chris'
335 test_user['email'] = u'chris@example.com'
336 test_user['pw_hash'] = auth_lib.bcrypt_gen_password_hash('toast')
337 test_user.save()
338
339 # Get login
340 # ---------
341 test_app.get('/auth/login/')
342 assert util.TEMPLATE_TEST_CONTEXT.has_key(
343 'mediagoblin/auth/login.html')
344
345 # Failed login - blank form
346 # -------------------------
347 util.clear_test_template_context()
348 response = test_app.post('/auth/login/')
349 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
350 form = context['login_form']
351 assert form.username.errors == [u'This field is required.']
352 assert form.password.errors == [u'This field is required.']
353
354 # Failed login - blank user
355 # -------------------------
356 util.clear_test_template_context()
357 response = test_app.post(
358 '/auth/login/', {
359 'password': u'toast'})
360 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
361 form = context['login_form']
362 assert form.username.errors == [u'This field is required.']
363
364 # Failed login - blank password
365 # -----------------------------
366 util.clear_test_template_context()
367 response = test_app.post(
368 '/auth/login/', {
369 'username': u'chris'})
370 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
371 form = context['login_form']
372 assert form.password.errors == [u'This field is required.']
373
374 # Failed login - bad user
375 # -----------------------
376 util.clear_test_template_context()
377 response = test_app.post(
378 '/auth/login/', {
379 'username': u'steve',
380 'password': 'toast'})
381 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
382 assert context['login_failed']
383
384 # Failed login - bad password
385 # ---------------------------
386 util.clear_test_template_context()
387 response = test_app.post(
388 '/auth/login/', {
389 'username': u'chris',
390 'password': 'jam'})
391 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
392 assert context['login_failed']
393
394 # Successful login
395 # ----------------
396 util.clear_test_template_context()
397 response = test_app.post(
398 '/auth/login/', {
399 'username': u'chris',
400 'password': 'toast'})
401
402 # User should be redirected
403 response.follow()
404 assert_equal(
405 urlparse.urlsplit(response.location)[2],
406 '/')
407 assert util.TEMPLATE_TEST_CONTEXT.has_key(
408 'mediagoblin/root.html')
409
410 # Make sure user is in the session
411 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
412 session = context['request'].session
413 assert session['user_id'] == unicode(test_user['_id'])
414
415 # Successful logout
416 # -----------------
417 util.clear_test_template_context()
418 response = test_app.get('/auth/logout/')
419
420 # Should be redirected to index page
421 response.follow()
422 assert_equal(
423 urlparse.urlsplit(response.location)[2],
424 '/')
425 assert util.TEMPLATE_TEST_CONTEXT.has_key(
426 'mediagoblin/root.html')
427
428 # Make sure the user is not in the session
429 context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
430 session = context['request'].session
431 assert session.has_key('user_id') == False
432
433 # User is redirected to custom URL if POST['next'] is set
434 # -------------------------------------------------------
435 util.clear_test_template_context()
436 response = test_app.post(
437 '/auth/login/', {
438 'username': u'chris',
439 'password': 'toast',
440 'next' : '/u/chris/'})
441 assert_equal(
442 urlparse.urlsplit(response.location)[2],
443 '/u/chris/')
444