Fixes #136 on the 3.0 maintenance branch.
[mailman.git] / src / mailman / rest / tests / test_users.py
blob3c4a549d7d214c3f9fe01a4086cda1266fb76434
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)
8 # any later version.
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
13 # more details.
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."""
20 __all__ = [
21 'TestLP1074374',
22 'TestLP1419519',
23 'TestLogin',
24 'TestUsers',
28 import os
29 import unittest
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):
44 layer = RESTLayer
46 def setUp(self):
47 with transaction():
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',
67 }, method='PATCH')
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',
75 }, method='PATCH')
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',
84 }, method='PUT')
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',
93 }, method='PUT')
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',
106 method='DELETE')
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.
111 with transaction():
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',
117 method='DELETE')
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',
121 method='DELETE')
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),
125 method='DELETE')
126 self.assertEqual(cm.exception.code, 404)
128 def test_get_after_delete(self):
129 # You cannot GET a user record after deleting them.
130 with transaction():
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)
138 # Delete the user.
139 content, response = call_api(
140 'http://localhost:9001/3.0/users/anne@example.com',
141 method='DELETE')
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)
198 # Create the user.
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
235 # an exception.
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.
244 with transaction():
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',
254 method='DELETE')
255 self.assertEqual(response.status, 204)
256 # The user's preference has been deleted.
257 with transaction():
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)."""
267 layer = RESTLayer
269 def setUp(self):
270 user_manager = getUtility(IUserManager)
271 with transaction():
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',
281 }, method='POST')
282 self.assertEqual(response.status, 204)
283 # But the user cannot log in with an incorrect password.
284 with self.assertRaises(HTTPError) as cm:
285 call_api(
286 'http://localhost:9001/3.0/users/anne@example.com/login', {
287 'cleartext_password': 'not-the-password',
288 }, method='POST')
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
318 # necessary.
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:
324 print("""\
325 [passlib]
326 schemes = hex_md5
327 """, file=fp)
328 with configuration('passwords', configuration=config_file):
329 with transaction():
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."""
346 layer = RESTLayer
348 def setUp(self):
349 self.user_manager = getUtility(IUserManager)
350 with transaction():
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):
356 with transaction():
357 user_id = self.anne.user_id
358 call_api('http://localhost:9001/3.0/users/anne@example.com',
359 method='DELETE')
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.
369 with transaction():
370 self.anne.register('aperson@example.org')
371 call_api('http://localhost:9001/3.0/users/anne@example.com',
372 method='DELETE')
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
384 with transaction():
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',
389 method='DELETE')
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',
402 password='bbb'))
403 call_api('http://localhost:9001/3.0/members', dict(
404 list_id='test.example.com',
405 subscriber='anne@example.com',
406 role='member',
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
409 # different user).
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]
421 self.assertEqual(
422 member['address'],
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.
434 layer = RESTLayer
436 def setUp(self):
437 # Create a user and link 10 addresses to that user.
438 self.manager = getUtility(IUserManager)
439 with transaction():
440 anne = self.manager.create_user('anne@example.com', 'Anne Person')
441 for i in range(10):
442 email = 'a{:02d}@example.com'.format(i)
443 address = self.manager.create_address(email)
444 anne.link(address)
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, [
452 'a00@example.com',
453 'a01@example.com',
454 'a02@example.com',
455 'a03@example.com',
456 'a04@example.com',
457 'a05@example.com',
458 'a06@example.com',
459 'a07@example.com',
460 'a08@example.com',
461 'a09@example.com',
462 'anne@example.com',
464 content, response = call_api(
465 'http://localhost:9001/3.0/users/anne@example.com',
466 method='DELETE')
467 self.assertEqual(response.status, 204)
468 # Now there should be no addresses in the database.
469 config.db.abort()
470 emails = sorted(address.email for address in self.manager.addresses)
471 self.assertEqual(len(emails), 0)