Repository: freedomofpress/securedrop

Issue:
Handle IntegrityError on /admin/add
# Steps to reproduce

1. Provision development VM
2. Make an administrator account
3. Sign in 
4. Click "Admin"
5. Click "Add user" button
6. Fill in the form, setting the username to username of an existing user
7. Click "Add user"

# What should happen

Ideally a flashed message would appear saying "That username is already in use".

# What actually happens

![screen shot 2017-05-08 at 3 36 59 pm](https://cloud.githubusercontent.com/assets/7832803/25828322/68340ec0-3404-11e7-9fd4-10dc39bd0cc9.png)

# Proposed Fix

Catch the exception and flash the friendly error message as suggested above. Whoever fixes this ideally should also write a unit test to guard against future regressions.


Discussion:

Resolving patch:
diff --git a/securedrop/journalist.py b/securedrop/journalist.py
--- a/securedrop/journalist.py
+++ b/securedrop/journalist.py
@@ -190,8 +190,9 @@ def admin_add_user():
                         Journalist.MIN_PASSWORD_LEN, Journalist.MAX_PASSWORD_LEN
                     ), "error")
             except IntegrityError as e:
+                db_session.rollback()
                 form_valid = False
-                if "username is not unique" in str(e):
+                if "UNIQUE constraint failed: journalists.username" in str(e):
                     flash("That username is already in use",
                           "error")
                 else:
diff --git a/securedrop/manage.py b/securedrop/manage.py
--- a/securedrop/manage.py
+++ b/securedrop/manage.py
@@ -64,12 +64,6 @@ def add_journalist(): # pragma: no cover
 def _add_user(is_admin=False): # pragma: no cover
     while True:
         username = raw_input('Username: ')
-        if Journalist.query.filter_by(username=username).first():
-            print('Sorry, that username is already in use.')
-        else:
-            break
-
-    while True:
         password = getpass('Password: ')
         password_again = getpass('Confirm Password: ')
 
@@ -106,7 +100,8 @@ def _add_user(is_admin=False): # pragma: no cover
         db_session.add(user)
         db_session.commit()
     except Exception as exc:
-        if 'username is not unique' in exc:
+        db_session.rollback()
+        if "UNIQUE constraint failed: journalists.username" in str(exc):
             print('ERROR: That username is already taken!')
         else:
             exc_type, exc_value, exc_traceback = sys.exc_info()


Test patch:
diff --git a/securedrop/tests/test_journalist.py b/securedrop/tests/test_journalist.py
--- a/securedrop/tests/test_journalist.py
+++ b/securedrop/tests/test_journalist.py
@@ -97,7 +97,6 @@ def test_user_has_link_to_edit_account_page_in_index_page(self):
                                                          "Edit Account")
         self.assertIn(edit_account_link, resp.data)
 
-
     def test_admin_has_link_to_admin_index_page_in_index_page(self):
         resp = self.client.post(url_for('login'),
                                data=dict(username=self.admin.username,
@@ -218,6 +217,15 @@ def test_admin_add_user_password_mismatch_warning(self):
                                           is_admin=False))
         self.assertIn('Passwords didn', resp.data)
 
+    def test_admin_add_user_when_username_already_in_use(self):
+        self._login_admin()
+        resp = self.client.post(url_for('admin_add_user'),
+                                data=dict(username=self.admin.username,
+                                          password='testtesttest',
+                                          password_again='testtesttest',
+                                          is_admin=False))
+        self.assertIn('That username is already in use', resp.data)
+
     def test_max_password_length(self):
         """Creating a Journalist with a password that is greater than the
         maximum password length should raise an exception"""
diff --git a/securedrop/tests/test_manage.py b/securedrop/tests/test_manage.py
--- a/securedrop/tests/test_manage.py
+++ b/securedrop/tests/test_manage.py
@@ -1,10 +1,43 @@
 # -*- coding: utf-8 -*-
 
 import manage
+import mock
+from StringIO import StringIO
+import sys
 import unittest
+import __builtin__
+
+import utils
 
 
 class TestManagePy(unittest.TestCase):
     def test_parse_args(self):
         # just test that the arg parser is stable
         manage.get_args()
+
+
+class TestManagementCommand(unittest.TestCase):
+    def setUp(self):
+        utils.env.setup()
+
+    def tearDown(self):
+        utils.env.teardown()
+
+    @mock.patch("__builtin__.raw_input", return_value='N')
+    @mock.patch("manage.getpass", return_value='testtesttest')
+    @mock.patch("sys.stdout", new_callable=StringIO)
+    def test_exception_handling_when_duplicate_username(self, mock_raw_input,
+                                                        mock_getpass,
+                                                        mock_stdout):
+        """Regression test for duplicate username logic in manage.py"""
+
+        # Inserting the user for the first time should succeed
+        return_value = manage._add_user()
+        self.assertEqual(return_value, 0)
+        self.assertIn('successfully added', sys.stdout.getvalue())
+
+        # Inserting the user for a second time should fail
+        return_value = manage._add_user()
+        self.assertEqual(return_value, 1)
+        self.assertIn('ERROR: That username is already taken!',
+                      sys.stdout.getvalue())