1 # Copyright (C) 2011-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """REST user tests."""
31 from mailman
.app
.lifecycle
import create_list
32 from mailman
.config
import config
33 from mailman
.database
.transaction
import transaction
34 from mailman
.interfaces
.usermanager
import IUserManager
35 from mailman
.testing
.helpers
import call_api
, configuration
36 from mailman
.testing
.layers
import RESTLayer
37 from urllib
.error
import HTTPError
38 from zope
.component
import getUtility
39 from mailman
.model
.preferences
import Preferences
43 class TestUsers(unittest
.TestCase
):
48 self
._mlist
= create_list('test@example.com')
50 def test_get_missing_user_by_id(self
):
51 # You can't GET a missing user by user id.
52 with self
.assertRaises(HTTPError
) as cm
:
53 call_api('http://localhost:9001/3.0/users/99')
54 self
.assertEqual(cm
.exception
.code
, 404)
56 def test_get_missing_user_by_address(self
):
57 # You can't GET a missing user by address.
58 with self
.assertRaises(HTTPError
) as cm
:
59 call_api('http://localhost:9001/3.0/users/missing@example.org')
60 self
.assertEqual(cm
.exception
.code
, 404)
62 def test_patch_missing_user_by_id(self
):
63 # You can't PATCH a missing user by user id.
64 with self
.assertRaises(HTTPError
) as cm
:
65 call_api('http://localhost:9001/3.0/users/99', {
66 'display_name': 'Bob Dobbs',
68 self
.assertEqual(cm
.exception
.code
, 404)
70 def test_patch_missing_user_by_address(self
):
71 # You can't PATCH a missing user by user address.
72 with self
.assertRaises(HTTPError
) as cm
:
73 call_api('http://localhost:9001/3.0/users/bob@example.org', {
74 'display_name': 'Bob Dobbs',
76 self
.assertEqual(cm
.exception
.code
, 404)
78 def test_put_missing_user_by_id(self
):
79 # You can't PUT a missing user by user id.
80 with self
.assertRaises(HTTPError
) as cm
:
81 call_api('http://localhost:9001/3.0/users/99', {
82 'display_name': 'Bob Dobbs',
83 'cleartext_password': 'abc123',
85 self
.assertEqual(cm
.exception
.code
, 404)
87 def test_put_missing_user_by_address(self
):
88 # You can't PUT a missing user by user address.
89 with self
.assertRaises(HTTPError
) as cm
:
90 call_api('http://localhost:9001/3.0/users/bob@example.org', {
91 'display_name': 'Bob Dobbs',
92 'cleartext_password': 'abc123',
94 self
.assertEqual(cm
.exception
.code
, 404)
96 def test_delete_missing_user_by_id(self
):
97 # You can't DELETE a missing user by user id.
98 with self
.assertRaises(HTTPError
) as cm
:
99 call_api('http://localhost:9001/3.0/users/99', method
='DELETE')
100 self
.assertEqual(cm
.exception
.code
, 404)
102 def test_delete_missing_user_by_address(self
):
103 # You can't DELETE a missing user by user address.
104 with self
.assertRaises(HTTPError
) as cm
:
105 call_api('http://localhost:9001/3.0/users/bob@example.com',
107 self
.assertEqual(cm
.exception
.code
, 404)
109 def test_delete_user_twice(self
):
110 # You cannot DELETE a user twice, either by address or user id.
112 anne
= getUtility(IUserManager
).create_user(
113 'anne@example.com', 'Anne Person')
114 user_id
= anne
.user_id
115 content
, response
= call_api(
116 'http://localhost:9001/3.0/users/anne@example.com',
118 self
.assertEqual(response
.status
, 204)
119 with self
.assertRaises(HTTPError
) as cm
:
120 call_api('http://localhost:9001/3.0/users/anne@example.com',
122 self
.assertEqual(cm
.exception
.code
, 404)
123 with self
.assertRaises(HTTPError
) as cm
:
124 call_api('http://localhost:9001/3.0/users/{}'.format(user_id
),
126 self
.assertEqual(cm
.exception
.code
, 404)
128 def test_get_after_delete(self
):
129 # You cannot GET a user record after deleting them.
131 anne
= getUtility(IUserManager
).create_user(
132 'anne@example.com', 'Anne Person')
133 user_id
= anne
.user_id
134 # You can still GET the user record.
135 content
, response
= call_api(
136 'http://localhost:9001/3.0/users/anne@example.com')
137 self
.assertEqual(response
.status
, 200)
139 content
, response
= call_api(
140 'http://localhost:9001/3.0/users/anne@example.com',
142 self
.assertEqual(response
.status
, 204)
143 # The user record can no longer be retrieved.
144 with self
.assertRaises(HTTPError
) as cm
:
145 call_api('http://localhost:9001/3.0/users/anne@example.com')
146 self
.assertEqual(cm
.exception
.code
, 404)
147 with self
.assertRaises(HTTPError
) as cm
:
148 call_api('http://localhost:9001/3.0/users/{}'.format(user_id
))
149 self
.assertEqual(cm
.exception
.code
, 404)
151 def test_existing_user_error(self
):
152 # Creating a user twice results in an error.
153 call_api('http://localhost:9001/3.0/users', {
154 'email': 'anne@example.com',
156 # The second try returns an error.
157 with self
.assertRaises(HTTPError
) as cm
:
158 call_api('http://localhost:9001/3.0/users', {
159 'email': 'anne@example.com',
161 self
.assertEqual(cm
.exception
.code
, 400)
162 self
.assertEqual(cm
.exception
.reason
,
163 b
'Address already exists: anne@example.com')
165 def test_addresses_of_missing_user_id(self
):
166 # Trying to get the /addresses of a missing user id results in error.
167 with self
.assertRaises(HTTPError
) as cm
:
168 call_api('http://localhost:9001/3.0/users/801/addresses')
169 self
.assertEqual(cm
.exception
.code
, 404)
171 def test_addresses_of_missing_user_address(self
):
172 # Trying to get the /addresses of a missing user id results in error.
173 with self
.assertRaises(HTTPError
) as cm
:
174 call_api('http://localhost:9001/3.0/users/z@example.net/addresses')
175 self
.assertEqual(cm
.exception
.code
, 404)
177 def test_login_missing_user_by_id(self
):
178 # Verify a password for a non-existing user, by id.
179 with self
.assertRaises(HTTPError
) as cm
:
180 call_api('http://localhost:9001/3.0/users/99/login', {
181 'cleartext_password': 'wrong',
183 self
.assertEqual(cm
.exception
.code
, 404)
185 def test_login_missing_user_by_address(self
):
186 # Verify a password for a non-existing user, by address.
187 with self
.assertRaises(HTTPError
) as cm
:
188 call_api('http://localhost:9001/3.0/users/z@example.org/login', {
189 'cleartext_password': 'wrong',
191 self
.assertEqual(cm
.exception
.code
, 404)
193 def test_create_user_twice(self
):
194 # LP: #1418280. No additional users should be created when an address
195 # that already exists is given.
196 content
, response
= call_api('http://localhost:9001/3.0/users')
197 self
.assertEqual(content
['total_size'], 0)
199 call_api('http://localhost:9001/3.0/users', dict(
200 email
='anne@example.com'))
201 # There is now one user.
202 content
, response
= call_api('http://localhost:9001/3.0/users')
203 self
.assertEqual(content
['total_size'], 1)
204 # Trying to create the user with the same address results in an error.
205 with self
.assertRaises(HTTPError
) as cm
:
206 call_api('http://localhost:9001/3.0/users', dict(
207 email
='anne@example.com'))
208 self
.assertEqual(cm
.exception
.code
, 400)
209 self
.assertEqual(cm
.exception
.reason
,
210 b
'Address already exists: anne@example.com')
211 # But at least no new users was created.
212 content
, response
= call_api('http://localhost:9001/3.0/users')
213 self
.assertEqual(content
['total_size'], 1)
215 def test_create_server_owner_false(self
):
216 # Issue #136: Creating a user with is_server_owner=no should create
217 # user who is not a server owner.
218 content
, response
= call_api('http://localhost:9001/3.0/users', dict(
219 email
='anne@example.com',
220 is_server_owner
='no'))
221 anne
= getUtility(IUserManager
).get_user('anne@example.com')
222 self
.assertFalse(anne
.is_server_owner
)
224 def test_create_server_owner_true(self
):
225 # Issue #136: Creating a user with is_server_owner=yes should create a
226 # new server owner user.
227 content
, response
= call_api('http://localhost:9001/3.0/users', dict(
228 email
='anne@example.com',
229 is_server_owner
='yes'))
230 anne
= getUtility(IUserManager
).get_user('anne@example.com')
231 self
.assertTrue(anne
.is_server_owner
)
233 def test_create_server_owner_bogus(self
):
234 # Issue #136: Creating a user with is_server_owner=bogus should throw
236 with self
.assertRaises(HTTPError
) as cm
:
237 call_api('http://localhost:9001/3.0/users', dict(
238 email
='anne@example.com',
239 is_server_owner
='bogus'))
240 self
.assertEqual(cm
.exception
.code
, 400)
242 def test_preferences_deletion_on_user_deletion(self
):
243 # LP: #1418276 - deleting a user did not delete their preferences.
245 anne
= getUtility(IUserManager
).create_user(
246 'anne@example.com', 'Anne Person')
247 # Anne's preference is in the database.
248 preferences
= config
.db
.store
.query(Preferences
).filter_by(
249 id=anne
.preferences
.id)
250 self
.assertEqual(preferences
.count(), 1)
251 # Delete the user via REST.
252 content
, response
= call_api(
253 'http://localhost:9001/3.0/users/anne@example.com',
255 self
.assertEqual(response
.status
, 204)
256 # The user's preference has been deleted.
258 preferences
= config
.db
.store
.query(Preferences
).filter_by(
259 id=anne
.preferences
.id)
260 self
.assertEqual(preferences
.count(), 0)
264 class TestLogin(unittest
.TestCase
):
265 """Test user 'login' (really just password verification)."""
270 user_manager
= getUtility(IUserManager
)
272 self
.anne
= user_manager
.create_user(
273 'anne@example.com', 'Anne Person')
274 self
.anne
.password
= config
.password_context
.encrypt('abc123')
276 def test_login_with_cleartext_password(self
):
277 # A user can log in with the correct clear text password.
278 content
, response
= call_api(
279 'http://localhost:9001/3.0/users/anne@example.com/login', {
280 'cleartext_password': 'abc123',
282 self
.assertEqual(response
.status
, 204)
283 # But the user cannot log in with an incorrect password.
284 with self
.assertRaises(HTTPError
) as cm
:
286 'http://localhost:9001/3.0/users/anne@example.com/login', {
287 'cleartext_password': 'not-the-password',
289 self
.assertEqual(cm
.exception
.code
, 403)
291 def test_wrong_parameter(self
):
292 # A bad request because it is mistyped the required attribute.
293 with self
.assertRaises(HTTPError
) as cm
:
294 call_api('http://localhost:9001/3.0/users/1/login', {
295 'hashed_password': 'bad hash',
297 self
.assertEqual(cm
.exception
.code
, 400)
299 def test_not_enough_parameters(self
):
300 # A bad request because it is missing the required attribute.
301 with self
.assertRaises(HTTPError
) as cm
:
302 call_api('http://localhost:9001/3.0/users/1/login', {
304 self
.assertEqual(cm
.exception
.code
, 400)
306 def test_too_many_parameters(self
):
307 # A bad request because it has too many attributes.
308 with self
.assertRaises(HTTPError
) as cm
:
309 call_api('http://localhost:9001/3.0/users/1/login', {
310 'cleartext_password': 'abc123',
311 'display_name': 'Annie Personhood',
313 self
.assertEqual(cm
.exception
.code
, 400)
315 def test_successful_login_updates_password(self
):
316 # Passlib supports updating the hash when the hash algorithm changes.
317 # When a user logs in successfully, the password will be updated if
320 # Start by hashing Anne's password with a different hashing algorithm
321 # than the one that the REST runner uses by default during testing.
322 config_file
= os
.path
.join(config
.VAR_DIR
, 'passlib-tmp.config')
323 with
open(config_file
, 'w') as fp
:
328 with
configuration('passwords', configuration
=config_file
):
330 self
.anne
.password
= config
.password_context
.encrypt('abc123')
331 # Just ensure Anne's password is hashed correctly.
332 self
.assertEqual(self
.anne
.password
,
333 'e99a18c428cb38d5f260853678922e03')
334 # Now, Anne logs in with a successful password. This should change it
335 # back to the plaintext hash.
336 call_api('http://localhost:9001/3.0/users/1/login', {
337 'cleartext_password': 'abc123',
339 self
.assertEqual(self
.anne
.password
, '{plaintext}abc123')
343 class TestLP1074374(unittest
.TestCase
):
344 """LP: #1074374 - deleting a user left their address records active."""
349 self
.user_manager
= getUtility(IUserManager
)
351 self
.mlist
= create_list('test@example.com')
352 self
.anne
= self
.user_manager
.create_user(
353 'anne@example.com', 'Anne Person')
355 def test_deleting_user_deletes_address(self
):
357 user_id
= self
.anne
.user_id
358 call_api('http://localhost:9001/3.0/users/anne@example.com',
360 # The user record is gone.
361 self
.assertIsNone(self
.user_manager
.get_user_by_id(user_id
))
362 self
.assertIsNone(self
.user_manager
.get_user('anne@example.com'))
363 # Anne's address is also gone.
364 self
.assertIsNone(self
.user_manager
.get_address('anne@example.com'))
366 def test_deleting_user_deletes_addresses(self
):
367 # All of Anne's linked addresses are deleted when her user record is
368 # deleted. So, register and link another address to Anne.
370 self
.anne
.register('aperson@example.org')
371 call_api('http://localhost:9001/3.0/users/anne@example.com',
373 self
.assertIsNone(self
.user_manager
.get_user('anne@example.com'))
374 self
.assertIsNone(self
.user_manager
.get_user('aperson@example.org'))
376 def test_lp_1074374(self
):
377 # Specific steps to reproduce the bug:
378 # - create a user through the REST API (well, we did that outside the
379 # REST API here, but that should be fine)
380 # - delete that user through the API
381 # - repeating step 1 gives a 500 status code
382 # - /3.0/addresses still contains the original address
383 # - /3.0/members gives a 500
385 user_id
= self
.anne
.user_id
386 address
= list(self
.anne
.addresses
)[0]
387 self
.mlist
.subscribe(address
)
388 call_api('http://localhost:9001/3.0/users/anne@example.com',
390 content
, response
= call_api('http://localhost:9001/3.0/addresses')
391 # There are no addresses, and thus no entries in the returned JSON.
392 self
.assertNotIn('entries', content
)
393 self
.assertEqual(content
['total_size'], 0)
394 # There are also no members.
395 content
, response
= call_api('http://localhost:9001/3.0/members')
396 self
.assertNotIn('entries', content
)
397 self
.assertEqual(content
['total_size'], 0)
398 # Now we can create a new user record for Anne, and subscribe her to
399 # the mailing list, this time all through the API.
400 call_api('http://localhost:9001/3.0/users', dict(
401 email
='anne@example.com',
403 call_api('http://localhost:9001/3.0/members', dict(
404 list_id
='test.example.com',
405 subscriber
='anne@example.com',
407 pre_verified
=True, pre_confirmed
=True, pre_approved
=True))
408 # This is not the Anne you're looking for. (IOW, the new Anne is a
410 content
, response
= call_api(
411 'http://localhost:9001/3.0/users/anne@example.com')
412 self
.assertNotEqual(user_id
, content
['user_id'])
413 # Anne has an address record.
414 content
, response
= call_api('http://localhost:9001/3.0/addresses')
415 self
.assertEqual(content
['total_size'], 1)
416 self
.assertEqual(content
['entries'][0]['email'], 'anne@example.com')
417 # Anne is also a member of the mailing list.
418 content
, response
= call_api('http://localhost:9001/3.0/members')
419 self
.assertEqual(content
['total_size'], 1)
420 member
= content
['entries'][0]
423 'http://localhost:9001/3.0/addresses/anne@example.com')
424 self
.assertEqual(member
['email'], 'anne@example.com')
425 self
.assertEqual(member
['delivery_mode'], 'regular')
426 self
.assertEqual(member
['list_id'], 'test.example.com')
427 self
.assertEqual(member
['role'], 'member')
431 class TestLP1419519(unittest
.TestCase
):
432 # LP: #1419519 - deleting a user with many linked addresses does not delete
433 # all address records.
437 # Create a user and link 10 addresses to that user.
438 self
.manager
= getUtility(IUserManager
)
440 anne
= self
.manager
.create_user('anne@example.com', 'Anne Person')
442 email
= 'a{:02d}@example.com'.format(i
)
443 address
= self
.manager
.create_address(email
)
446 def test_delete_user(self
):
447 # Deleting the user deletes all their linked addresses.
449 # We start with 11 addresses in the database.
450 emails
= sorted(address
.email
for address
in self
.manager
.addresses
)
451 self
.assertEqual(emails
, [
464 content
, response
= call_api(
465 'http://localhost:9001/3.0/users/anne@example.com',
467 self
.assertEqual(response
.status
, 204)
468 # Now there should be no addresses in the database.
470 emails
= sorted(address
.email
for address
in self
.manager
.addresses
)
471 self
.assertEqual(len(emails
), 0)