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