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