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