Repository: gratipay/gratipay.com

Issue:
Spaces in usernames & empty usernames
You can currently change your username to either an empty string (resulting in your account being lost), or a string filled entirely of spaces. Neither of these should be possible. 

I'm not convinced that we should allow spaces in usernames at all, since it makes for confusing URLs. Similarly, neither Twitter or GitHub allow usernames with spaces.

[IRC](https://botbot.me/freenode/gittip/msg/6022706/)



Discussion:
The empty string is already taken. I want to let them keep it as a trophy. I'm fine restricting whitespace usernames from now on, though I'd like to continue to allow spaces in usernames.


Resolving patch:
diff --git a/gittip/models/participant.py b/gittip/models/participant.py
--- a/gittip/models/participant.py
+++ b/gittip/models/participant.py
@@ -260,17 +260,21 @@ def change_username(self, suggested):
         """
         # TODO: reconsider allowing unicode usernames
         typecheck(suggested, unicode)
+        suggested = suggested.strip()
+
+        if not suggested:
+            raise UsernameIsEmpty(suggested)
 
         if len(suggested) > 32:
-            raise UsernameTooLong
+            raise UsernameTooLong(suggested)
 
         if set(suggested) - ASCII_ALLOWED_IN_USERNAME:
-            raise UsernameContainsInvalidCharacters
+            raise UsernameContainsInvalidCharacters(suggested)
 
         lowercased = suggested.lower()
 
         if lowercased in gittip.RESTRICTED_USERNAMES:
-            raise UsernameIsRestricted
+            raise UsernameIsRestricted(suggested)
 
         if suggested != self.username:
             try:
@@ -285,9 +289,11 @@ def change_username(self, suggested):
             except IntegrityError:
                 raise UsernameAlreadyTaken(suggested)
 
-            assert (suggested, lowercased) == actual  # sanity check
+            assert (suggested, lowercased) == actual # sanity check
             self.set_attributes(username=suggested, username_lower=lowercased)
 
+        return suggested
+
 
     def update_goal(self, goal):
         typecheck(goal, (Decimal, None))
@@ -680,11 +686,16 @@ class ProblemChangingUsername(Exception):
     def __str__(self):
         return self.msg.format(self.args[0])
 
+class UsernameIsEmpty(ProblemChangingUsername):
+    msg = "You need to provide a username!"
+
 class UsernameTooLong(ProblemChangingUsername):
     msg = "The username '{}' is too long."
 
+# Not passing the potentially unicode characters back because of:
+# https://github.com/gittip/aspen-python/issues/177
 class UsernameContainsInvalidCharacters(ProblemChangingUsername):
-    msg = "The username '{}' contains invalid characters."
+    msg = "That username contains invalid characters."
 
 class UsernameIsRestricted(ProblemChangingUsername):
     msg = "The username '{}' is restricted."


Test patch:
diff --git a/tests/test_participant.py b/tests/test_participant.py
--- a/tests/test_participant.py
+++ b/tests/test_participant.py
@@ -13,7 +13,8 @@
 from gittip.elsewhere.twitter import TwitterAccount
 from gittip.models._mixin_elsewhere import NeedConfirmation
 from gittip.models.participant import Participant
-from gittip.models.participant import ( UsernameTooLong
+from gittip.models.participant import ( UsernameIsEmpty
+                                      , UsernameTooLong
                                       , UsernameAlreadyTaken
                                       , UsernameContainsInvalidCharacters
                                       , UsernameIsRestricted
@@ -218,6 +219,23 @@ def test_changing_username_successfully(self):
         actual = Participant.from_username('user2')
         assert self.participant == actual, actual
 
+    def test_changing_username_to_nothing(self):
+        with assert_raises(UsernameIsEmpty):
+            self.participant.change_username('')
+
+    def test_changing_username_to_all_spaces(self):
+        with assert_raises(UsernameIsEmpty):
+            self.participant.change_username('    ')
+
+    def test_changing_username_strips_spaces(self):
+        self.participant.change_username('  aaa  ')
+        actual = Participant.from_username('aaa')
+        assert self.participant == actual, actual
+
+    def test_changing_username_returns_the_new_username(self):
+        returned = self.participant.change_username('  foo bar baz  ')
+        assert returned == 'foo bar baz', returned
+
     def test_changing_username_to_too_long(self):
         with assert_raises(UsernameTooLong):
             self.participant.change_username('123456789012345678901234567890123')
diff --git a/tests/test_username_json.py b/tests/test_username_json.py
--- a/tests/test_username_json.py
+++ b/tests/test_username_json.py
@@ -33,22 +33,26 @@ def test_anonymous_gets_404(self):
         response = self.change_username("bob", user=None)
         assert response.code == 404, (response.code, response.body)
 
-    def test_invalid_is_400(self):
+    def test_empty(self):
+        response = self.change_username('      ')
+        assert response.code == 400, (response.code, response.body)
+
+    def test_invalid(self):
         response = self.change_username("\u2034".encode('utf8'))
         assert response.code == 400, (response.code, response.body)
 
-    def test_restricted_username_is_400(self):
+    def test_restricted_username(self):
         response = self.change_username("assets")
         assert response.code == 400, (response.code, response.body)
 
-    def test_unavailable_is_409(self):
+    def test_unavailable(self):
         self.make_participant("bob")
         response = self.change_username("bob")
-        assert response.code == 409, (response.code, response.body)
+        assert response.code == 400, (response.code, response.body)
 
-    def test_too_long_is_413(self):
+    def test_too_long(self):
         self.make_participant("bob")
         response = self.change_username("I am way too long, and you know it, "
                                         "and I know it, and the American "
                                         "people know it.")
-        assert response.code == 413, (response.code, response.body)
+        assert response.code == 400, (response.code, response.body)