Repository: pypi/warehouse

Issue:
Validate username format before attempting to flush it to the db
Validate a valid username prior to trying to save it to the database, using the same logic as is already in the database.

---

https://sentry.io/python-software-foundation/warehouse-production/issues/258353802/

```
IntegrityError: new row for relation "accounts_user" violates check constraint "accounts_user_valid_username"

  File "sqlalchemy/engine/base.py", line 1182, in _execute_context
    context)
  File "sqlalchemy/engine/default.py", line 470, in do_execute
    cursor.execute(statement, parameters)

IntegrityError: (psycopg2.IntegrityError) new row for relation "accounts_user" violates check constraint "accounts_user_valid_username"
(30 additional frame(s) were not displayed)
...
  File "warehouse/raven.py", line 41, in raven_tween
    return handler(request)
  File "warehouse/cache/http.py", line 69, in conditional_http_tween
    response = handler(request)
  File "warehouse/cache/http.py", line 33, in wrapped
    return view(context, request)
  File "warehouse/accounts/views.py", line 226, in register
    form.email.data
  File "warehouse/accounts/services.py", line 152, in create_user
    self.db.flush()

IntegrityError: (psycopg2.IntegrityError) new row for relation "accounts_user" violates check constraint "accounts_user_valid_username"
DETAIL:  Failing row contains 
```


Discussion:

Resolving patch:
diff --git a/warehouse/accounts/forms.py b/warehouse/accounts/forms.py
--- a/warehouse/accounts/forms.py
+++ b/warehouse/accounts/forms.py
@@ -29,6 +29,18 @@ class CredentialsMixin:
                     "a username with under 50 characters."
                 )
             ),
+            # the regexp below must match the CheckConstraint
+            # for the username field in accounts.models.User
+            wtforms.validators.Regexp(
+                r'^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$',
+                message=(
+                    "The username is invalid. Usernames "
+                    "must be composed of letters, numbers, "
+                    "dots, hyphens and underscores. And must "
+                    "also start and finish with a letter or number. "
+                    "Please choose a different username."
+                )
+            )
         ],
     )
 


Test patch:
diff --git a/tests/unit/accounts/test_forms.py b/tests/unit/accounts/test_forms.py
--- a/tests/unit/accounts/test_forms.py
+++ b/tests/unit/accounts/test_forms.py
@@ -311,6 +311,28 @@ def test_username_exists(self):
             "Please choose a different username."
         )
 
+    @pytest.mark.parametrize("username", ['_foo', 'bar_', 'foo^bar'])
+    def test_username_is_valid(self, username):
+        form = forms.RegistrationForm(
+            data={"username": username},
+            user_service=pretend.stub(
+                find_userid=pretend.call_recorder(lambda _: None),
+            ),
+            recaptcha_service=pretend.stub(
+                enabled=False,
+                verify_response=pretend.call_recorder(lambda _: None),
+            ),
+        )
+        assert not form.validate()
+        assert (
+            form.username.errors.pop() ==
+            "The username is invalid. Usernames "
+            "must be composed of letters, numbers, "
+            "dots, hyphens and underscores. And must "
+            "also start and finish with a letter or number. "
+            "Please choose a different username."
+        )
+
     def test_password_strength(self):
         cases = (
             ("foobar", False),