Repository: mdn/kuma

Issue:
Sign-up with clashing usernames is busted
**Summary**

What's happening here is that `peterbe@gmail.com` is trying to sign up. It would have suggested a default username of `peterbe` (from `email.split('@')[0]`) but that username is clashing with an GitHub user. 
<img width="893" alt="Screen Shot 2020-02-03 at 11 07 03 AM" src="https://user-images.githubusercontent.com/26739/73675677-e16d0d00-4680-11ea-8f46-81a9e8627c61.png">




Discussion:
In triage, we decided to, for now, just add a number to the username till it's not taken. 

```python
username = email.split('@')[0]
increment = 1
while is_taken(username):
   increment += 1
   username = f"{email.split('@')[0]}{increment}"
```

Resolving patch:
diff --git a/kuma/users/views.py b/kuma/users/views.py
--- a/kuma/users/views.py
+++ b/kuma/users/views.py
@@ -578,9 +578,12 @@ def get_form(self, form_class=None):
             email = form.initial.get('email', '')
             if isinstance(email, tuple):
                 email = email[0]
-            suggested_username = email.split('@')[0]
-            if not User.objects.filter(username__iexact=suggested_username).exists():
-                form.initial['username'] = suggested_username
+            suggested_username = suggested_username_base = email.split('@')[0]
+            increment = 1
+            while User.objects.filter(username__iexact=suggested_username).exists():
+                increment += 1
+                suggested_username = f'{suggested_username_base}{increment}'
+            form.initial['username'] = suggested_username
 
         self.matching_user = None
         initial_username = form.initial.get('username', None)


Test patch:
diff --git a/kuma/users/tests/__init__.py b/kuma/users/tests/__init__.py
--- a/kuma/users/tests/__init__.py
+++ b/kuma/users/tests/__init__.py
@@ -4,6 +4,7 @@
 from allauth.socialaccount.models import SocialApp
 from allauth.socialaccount.providers import registry
 from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
+from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
 from django.contrib.auth import get_user_model
 from django.utils.crypto import get_random_string
 
@@ -13,6 +14,7 @@
 from kuma.wiki.tests import revision as create_revision
 
 from ..providers.github.provider import KumaGitHubProvider
+from ..providers.google.provider import KumaGoogleProvider
 
 
 class UserTestMixin(object):
@@ -124,6 +126,22 @@ class SocialTestMixin(object):
         }
     ]
 
+    google_token_data = {
+        'uid': 2,
+        'access_token': 'google_token',
+    }
+    # These fields come from the google provider unit tests in django-allauth
+    google_profile_data = {
+        'family_name': 'Page',
+        'name': 'Sergey',
+        'picture': 'https://lh5.googleusercontent.com/photo.jpg',
+        'locale': 'le',
+        'email': 'example@gmail.com',
+        'given_name': 'Ser',
+        'id': '108204268033311374519',
+        'verified_email': True,
+    }
+
     def github_login(
             self, token_data=None, profile_data=None, email_data=None,
             process='login',
@@ -210,3 +228,75 @@ def ensure_github_app(self):
         provider = registry.by_id(KumaGitHubProvider.id)
         app = SocialApp.objects.get(provider=provider.id)
         return app
+
+    def google_login(
+            self,
+            token_data=None, profile_data=None, email_data=None,
+            process='login',
+            token_status_code=200,
+            profile_status_code=200,
+            email_status_code=200,
+            token_exc=None,
+            profile_exc=None,
+            email_exc=None):
+        """
+        Mock a login to Google and return the response.
+
+        Keyword Arguments:
+        token_data - OAuth token data, or None for default
+        profile_data - Google profile data, or None for default
+        email_data - Google email data, or None for default
+        process - 'login', 'connect', or 'redirect'
+        token_status_code - HTTP code for posting token (default 200)
+        profile_status_code - HTTP code for getting profile (default 200)
+        email_status_code - HTTP code for getting email addresses (default 200)
+        """
+        login_url = reverse('google_login')
+        callback_url = reverse('google_callback')
+        self.ensure_google_app()
+
+        # Start the login process
+        # Store state in the session, and redirect the user to Google
+        login_response = self.client.get(login_url, {'process': process})
+        assert login_response.status_code == 302
+        location = urlparse(login_response['location'])
+        query = parse_qs(location.query)
+        assert callback_url in query['redirect_uri'][0]
+        state = query['state'][0]
+
+        # Callback from Google, mock follow-on Google responses
+        with requests_mock.Mocker() as mock_requests:
+            # The callback view will make requests back to Google:
+            # The OAuth2 authentication token (or error)
+            if token_exc:
+                mock_requests.post(
+                    GoogleOAuth2Adapter.access_token_url,
+                    exc=token_exc)
+            else:
+                mock_requests.post(
+                    GoogleOAuth2Adapter.access_token_url,
+                    json=token_data or self.google_token_data,
+                    headers={'content-type': 'application/json'},
+                    status_code=token_status_code)
+
+            # The authenticated user's profile data
+            if profile_exc:
+                mock_requests.get(
+                    GoogleOAuth2Adapter.profile_url,
+                    exc=profile_exc)
+            else:
+                mock_requests.get(
+                    GoogleOAuth2Adapter.profile_url,
+                    json=profile_data or self.google_profile_data,
+                    status_code=profile_status_code)
+
+            # Simulate the callback from Google
+            data = {'code': 'google_code', 'state': state}
+            response = self.client.get(callback_url, data, follow=True)
+
+        return response
+
+    def ensure_google_app(self):
+        """Ensure a Google SocialApp is installed, configured."""
+        provider = registry.by_id(KumaGoogleProvider.id)
+        return SocialApp.objects.get(provider=provider.id)
diff --git a/kuma/users/tests/test_views.py b/kuma/users/tests/test_views.py
--- a/kuma/users/tests/test_views.py
+++ b/kuma/users/tests/test_views.py
@@ -1163,6 +1163,21 @@ def test_matching_user(self):
         assert_no_cache_header(response)
         assert response.context['matching_user'] == octocat
 
+    def test_clashing_username(self):
+        """First a GitHub user exists. Then a Google user tries to sign up
+        whose email address, when `email.split('@')[0]` would become the same
+        as the existing GitHub user.
+        """
+        user(username='octocat', save=True)
+        self.google_login(profile_data=dict(
+            self.google_profile_data,
+            email='octocat@gmail.com',
+        ))
+        response = self.client.get(self.signup_url)
+        assert response.status_code == 200
+        doc = pq(response.content)
+        assert doc.find('input[name="username"]').val() == 'octocat2'
+
     def test_email_addresses(self):
         public_email = 'octocat-public@example.com'
         private_email = 'octocat-private@example.com'