Repository: oppia/oppia

Issue:
[CD] Add "contributor dashboard debug" flag to Oppia local dev server start script
When the flag is enabled, the start script will do the following:
- Create an admin user with username "a" and assign curriculum, translation, and question admin rights to "a".
- Create a non-admin user with username "b" and give the user "submit question" rights.
- Populate sample lessons for translations. This could be via the "Load dummy new structures data" admin action.
- Add the topics linked to the lessons above to a classroom. This is needed for the topics to show up in the topic selectors and for the linked opportunities to show up in the "Submit Question" tab. Note, the topics will need to be published.

These steps will help to automate the steps required to populate sample data when developing for the contributor dashboard, largely replicating the steps [here](https://docs.google.com/document/d/1JYX4nvTcblaVVYAlTi7rApE0lWSBx0v_ZCCr_8WW4Wc/edit#heading=h.1nfdy29ioghr).
[CD] Add "contributor dashboard debug" flag to Oppia local dev server start script
When the flag is enabled, the start script will do the following:
- Create an admin user with username "a" and assign curriculum, translation, and question admin rights to "a".
- Create a non-admin user with username "b" and give the user "submit question" rights.
- Populate sample lessons for translations. This could be via the "Load dummy new structures data" admin action.
- Add the topics linked to the lessons above to a classroom. This is needed for the topics to show up in the topic selectors and for the linked opportunities to show up in the "Submit Question" tab. Note, the topics will need to be published.

These steps will help to automate the steps required to populate sample data when developing for the contributor dashboard, largely replicating the steps [here](https://docs.google.com/document/d/1JYX4nvTcblaVVYAlTi7rApE0lWSBx0v_ZCCr_8WW4Wc/edit#heading=h.1nfdy29ioghr).


Discussion:

Resolving patch:
diff --git a/scripts/contributor_dashboard_debug.py b/scripts/contributor_dashboard_debug.py
new file mode 100644
--- /dev/null
+++ b/scripts/contributor_dashboard_debug.py
@@ -0,0 +1,265 @@
+# coding: utf-8
+#
+# Copyright 2022 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This script assists development of the contributor dashboard. When the
+"contributor dashboard debug" flag is enabled, the start script will call the
+"populate_debug_data" function, which does the following:
+    1. Create an admin user with the username "a" and assign curriculum,
+    translation, and question admin rights to "a".
+    2. Create a non-admin user with the username "b" and give the user "submit
+    question" rights.
+    3. Populate sample lessons for translations. This is achieved by reproducing
+    the "Load dummy new structures data" admin action.
+    4. Add the topics linked to the lessons above to a classroom. This is needed
+    for the topics, if published, to show up in the topic selectors and for the
+    linked opportunities to show up in the "Submit Question" tab.
+"""
+
+from __future__ import annotations
+
+import contextlib
+import hashlib
+import json
+import os
+
+from core import feconf
+from core.constants import constants
+
+import firebase_admin
+from firebase_admin import auth as firebase_auth
+import requests
+from typing import Dict, Iterator, List
+from typing_extensions import Final
+
+FIREBASE_AUTH_EMULATOR_HOST: Final = (
+    'localhost:%s' % feconf.FIREBASE_EMULATOR_PORT)
+FIREBASE_SIGN_IN_URL: Final = (
+    'http://%s/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword'
+    % FIREBASE_AUTH_EMULATOR_HOST)
+
+SUPER_ADMIN_EMAIL: Final = feconf.ADMIN_EMAIL_ADDRESS
+SUPER_ADMIN_USERNAME: Final = 'a'
+SUPER_ADMIN_ROLES: Final = [
+    feconf.ROLE_ID_CURRICULUM_ADMIN, feconf.ROLE_ID_TRANSLATION_ADMIN,
+    feconf.ROLE_ID_QUESTION_ADMIN]
+
+CONTRIBUTOR_EMAIL: Final = 'contributor@example.com'
+CONTRIBUTOR_USERNAME: Final = 'b'
+
+CLASSROOM_NAME: Final = 'math'
+CLASSROOM_URL_FRAGMENT: Final = 'math'
+
+
+class ContributorDashboardDebugInitializer:
+    """Contains functions that populate sample data by sending requests to the
+    development server.
+
+    Attributes:
+        session: object(Session). The requests.Session object to send requests
+            to the Oppia server.
+        base_url: str. The base url of the Oppia server.
+        csrf_token: str. The csrf token of the current session.
+    """
+
+    def __init__(self, base_url: str) -> None:
+        self.session = requests.session()
+        self.base_url = base_url
+        self.csrf_token = ''
+
+    def populate_debug_data(self) -> None:
+        """Populate sample data to help develop for the contributor
+        dashboard.
+        """
+        firebase_admin.initialize_app(
+            options={'projectId': feconf.OPPIA_PROJECT_ID})
+
+        self._sign_up_new_user(SUPER_ADMIN_EMAIL, SUPER_ADMIN_USERNAME)
+        self._sign_up_new_user(CONTRIBUTOR_EMAIL, CONTRIBUTOR_USERNAME)
+
+        self._sign_in(SUPER_ADMIN_EMAIL)
+        self.csrf_token = self._get_csrf_token()
+        self._assign_admin_roles(SUPER_ADMIN_ROLES, SUPER_ADMIN_USERNAME)
+        self._add_submit_question_rights(CONTRIBUTOR_USERNAME)
+        self._generate_sample_new_structures_data()
+        self._add_topics_to_classroom(CLASSROOM_NAME, CLASSROOM_URL_FRAGMENT)
+
+    def _sign_up_new_user(self, email: str, username: str) -> None:
+        """Sign up a new user based on email and username. The password is
+        generated automatically from email.
+        """
+        # In development mode, the "Sign In" button will call the function
+        # AuthService.signInWithEmail() in the file
+        # core/templates/services/auth.service.ts. That function generates a
+        # password through the MD5 hash of email addresses to save developers
+        # and end-to-end test authors the trouble of providing passwords. This
+        # script also uses the MD5 hash when creating a user in the Firebase
+        # Authentication emulator. When we want to sign in a user created using
+        # this script, clicking the "Sign In" button will generate a valid
+        # password which is the same as that in the emulator.
+        password = hashlib.md5(email.encode('utf-8')).hexdigest()
+
+        # The FIREBASE_AUTH_EMULATOR_HOST environment variable is set to connect
+        # with the Firebase Authentication emulator.
+        with self._set_environ(
+            'FIREBASE_AUTH_EMULATOR_HOST', FIREBASE_AUTH_EMULATOR_HOST
+        ):
+            # Create a new user in Firebase.
+            firebase_auth.create_user(email=email, password=password)
+
+        # Sign up the new user in Oppia and set its username.
+        self._sign_in(email)
+        self._make_request('GET', '/signup?return_url=/')
+        self.csrf_token = self._get_csrf_token()
+
+        params = {'payload': json.dumps({
+            'username': username,
+            'agreed_to_terms': True,
+            'default_dashboard': constants.DASHBOARD_TYPE_LEARNER,
+        }), 'csrf_token': self.csrf_token}
+
+        self._make_request('POST', feconf.SIGNUP_DATA_URL, params=params)
+
+        # End current session, i.e. log out.
+        self._make_request('GET', '/session_end')
+        self.csrf_token = ''
+
+    @contextlib.contextmanager
+    def _set_environ(self, env_name: str, value: str) -> Iterator[None]:
+        """Temporarily set the environment variable 'env_name' to 'value' while
+        inside the ``with`` block.
+        """
+        old_value = os.environ.get(env_name)
+        os.environ[env_name] = value
+        try:
+            yield
+        finally:
+            if old_value is None:
+                del os.environ[env_name]
+            else:
+                os.environ[env_name] = old_value
+
+    def _sign_in(self, email: str) -> None:
+        """Begins a session with the given email, i.e. log in with the email."""
+        password = hashlib.md5(email.encode('utf-8')).hexdigest()
+
+        token_id = self._sign_in_with_email_and_password(email, password)
+        headers = {'Authorization': 'Bearer %s' % token_id}
+
+        self._make_request('GET', '/session_begin', headers=headers)
+
+    def _sign_in_with_email_and_password(
+        self, email: str, password: str
+    ) -> str | None:
+        """Signs in with email and password, and returns the token id."""
+        token_id = requests.post(
+            FIREBASE_SIGN_IN_URL,
+            params={'key': 'fake-api-key'},
+            json={
+                'email': email,
+                'password': password
+            }
+        ).json()['idToken']
+
+        return str(token_id) if token_id else None
+
+    def _get_csrf_token(self) -> str:
+        """Gets the CSRF token."""
+        response = self._make_request('GET', '/csrfhandler')
+        csrf_token = str(
+            json.loads(response.text[len(feconf.XSSI_PREFIX):])['token'])
+
+        return csrf_token
+
+    def _assign_admin_roles(self, roles: List[str], username: str) -> None:
+        """Assigns the given roles to the user with the given username."""
+        for role in roles:
+            params = {
+                'payload': json.dumps({
+                    'role': role,
+                    'username': username
+                }),
+                'csrf_token': self.csrf_token
+            }
+            self._make_request(
+                'PUT', feconf.ADMIN_ROLE_HANDLER_URL, params=params)
+
+    def _add_submit_question_rights(self, username: str) -> None:
+        """Adds submit question rights to the user with the given username."""
+        params = {
+            'payload': json.dumps({'username': username}),
+            'csrf_token': self.csrf_token
+        }
+
+        self._make_request(
+            'POST', '/contributionrightshandler/submit_question', params=params)
+
+    def _generate_sample_new_structures_data(self) -> None:
+        """Generates sample new structures data."""
+        params = {
+            'payload': json.dumps({
+                'action': 'generate_dummy_new_structures_data'}),
+            'csrf_token': self.csrf_token
+        }
+
+        self._make_request('POST', '/adminhandler', params=params)
+
+    def _add_topics_to_classroom(
+        self, classroom_name: str, classroom_url_fragment: str
+    ) -> None:
+        """Adds all dummy topics to a classroom."""
+        response = self._make_request(
+            'GET', '/topics_and_skills_dashboard/data')
+        topic_summary_dicts = json.loads(
+            response.text[len(feconf.XSSI_PREFIX):])['topic_summary_dicts']
+        topic_ids = [
+            topic_summary_dict['id'] for topic_summary_dict in
+            topic_summary_dicts]
+
+        params = {
+            'payload': json.dumps({
+                'action': 'save_config_properties',
+                'new_config_property_values': {
+                    'classroom_pages_data': [{
+                        'name': classroom_name,
+                        'url_fragment': classroom_url_fragment,
+                        'course_details': '',
+                        'topic_list_intro': '',
+                        'topic_ids': topic_ids
+                    }]
+                }
+            }),
+            'csrf_token': self.csrf_token
+        }
+
+        self._make_request('POST', '/adminhandler', params=params)
+
+    def _make_request(
+        self,
+        method: str,
+        url: str,
+        params: Dict[str, str] | None = None,
+        headers: Dict[str, str] | None = None
+    ) -> requests.Response:
+        """Makes a request to the Oppia server."""
+        if params is None:
+            params = {}
+        if headers is None:
+            headers = {}
+
+        response = self.session.request(
+            method, self.base_url + url, headers=headers, params=params)
+
+        return response
diff --git a/scripts/start.py b/scripts/start.py
--- a/scripts/start.py
+++ b/scripts/start.py
@@ -37,6 +37,7 @@
 from . import servers # isort:skip  pylint: disable=wrong-import-position, wrong-import-order
 
 from core.constants import constants # isort:skip  pylint: disable=wrong-import-position, wrong-import-order
+from scripts import contributor_dashboard_debug # isort:skip  pylint: disable=wrong-import-position, wrong-import-order
 
 _PARSER = argparse.ArgumentParser(
     description="""
@@ -77,6 +78,11 @@
     '--source_maps',
     help='optional; if specified, build webpack with source maps.',
     action='store_true')
+_PARSER.add_argument(
+    '--contributor_dashboard_debug',
+    help='optional; if specified, populate sample data that can be used to help'
+         'develop for the contributor dashboard.',
+    action='store_true')
 
 PORT_NUMBER_FOR_GAE_SERVER = 8181
 
@@ -178,6 +184,14 @@ def main(args: Optional[Sequence[str]] = None) -> None:
             skip_sdk_update_check=True,
             port=PORT_NUMBER_FOR_GAE_SERVER))
 
+        if parsed_args.contributor_dashboard_debug:
+            initializer = (
+                contributor_dashboard_debug
+                .ContributorDashboardDebugInitializer(
+                    base_url='http://localhost:%s' % PORT_NUMBER_FOR_GAE_SERVER)
+            )
+            initializer.populate_debug_data()
+
         managed_web_browser = (
             None if parsed_args.no_browser else
             servers.create_managed_web_browser(PORT_NUMBER_FOR_GAE_SERVER))  # type: ignore[no-untyped-call]


Test patch:
diff --git a/scripts/backend_test_shards.json b/scripts/backend_test_shards.json
--- a/scripts/backend_test_shards.json
+++ b/scripts/backend_test_shards.json
@@ -55,7 +55,8 @@
         "core.jobs.batch_jobs.story_migration_jobs_test",
         "core.jobs.batch_jobs.email_deletion_jobs_test",
         "core.jobs.batch_jobs.blog_post_search_indexing_jobs_test",
-        "core.jobs.io.cache_io_test"
+        "core.jobs.io.cache_io_test",
+        "scripts.contributor_dashboard_debug_test"
     ],
     "2": [
         "core.domain.rights_manager_test",
diff --git a/scripts/contributor_dashboard_debug_test.py b/scripts/contributor_dashboard_debug_test.py
new file mode 100644
--- /dev/null
+++ b/scripts/contributor_dashboard_debug_test.py
@@ -0,0 +1,302 @@
+# coding: utf-8
+#
+# Copyright 2022 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unit tests for scripts/contributor_dashboard_debug.py."""
+
+from __future__ import annotations
+
+import hashlib
+import os
+
+from core import feconf
+from core.controllers import base
+from core.domain import opportunity_services
+from core.domain import question_fetchers
+from core.domain import skill_services
+from core.domain import story_fetchers
+from core.domain import topic_fetchers
+from core.domain import user_services
+from core.platform import models
+from core.platform.auth import firebase_auth_services_test
+from core.tests import test_utils
+
+from scripts import contributor_dashboard_debug
+
+import firebase_admin
+from firebase_admin import auth as firebase_auth
+
+import requests
+from typing import Dict, List, Optional
+import webtest
+
+auth_services = models.Registry.import_auth_services()
+
+
+class MockResponse:
+
+    def __init__(
+        self,
+        ok: bool = True,
+        json: Dict[str, str] | None = None,
+        status_code: int = 200,
+        reason: str = 'foo'
+    ) -> None:
+        if json is None:
+            json = {}
+        self.ok = ok
+        self.json_dict = json
+        self.status_code = status_code
+        self.reason = reason
+
+    def json(self) -> Dict[str, str]:
+        """Get json dict or raise ValueError if json_dict not a dict."""
+        if not isinstance(self.json_dict, dict):
+            raise ValueError('Payload not JSON.')
+        return self.json_dict
+
+
+class ContributorDashboardDebugInitializerTests(test_utils.GenericTestBase):
+
+    def setUp(self) -> None:
+        super().setUp()
+        self.firebase_sdk_stub = (
+            firebase_auth_services_test.FirebaseAdminSdkStub())
+        self.firebase_sdk_stub.install(self)
+        self.token_by_email: Dict[str, str] = {}
+        self.token_of_current_user: Optional[str] = None
+        self.initializer = (
+            contributor_dashboard_debug.ContributorDashboardDebugInitializer(
+                base_url=''))
+
+        self.initializer.csrf_token = self.get_new_csrf_token()
+
+        self.request_swap = self.swap(
+            self.initializer.session,
+            'request',
+            self._mock_request)
+
+        self.init_app_swap = self.swap_with_call_counter(
+            firebase_admin, 'initialize_app')
+
+        admin_password = hashlib.md5(
+            contributor_dashboard_debug.SUPER_ADMIN_EMAIL.encode(
+                'utf-8')).hexdigest()
+        contributor_password = hashlib.md5(
+            contributor_dashboard_debug.CONTRIBUTOR_EMAIL.encode(
+                'utf-8')).hexdigest()
+        self.create_user_swap = self.swap_with_checks(
+            firebase_auth, 'create_user', self._mock_firebase_auth_create_user,
+            expected_kwargs=[
+                {
+                    'email': contributor_dashboard_debug.SUPER_ADMIN_EMAIL,
+                    'password': admin_password
+                },
+                {
+                    'email': contributor_dashboard_debug.CONTRIBUTOR_EMAIL,
+                    'password': contributor_password
+                }
+            ]
+        )
+
+        self.begin_session_swap = self.swap(
+            self.initializer, '_sign_in',
+            self._mock_login_as_admin)
+
+    def tearDown(self) -> None:
+        self.firebase_sdk_stub.uninstall()
+        self.logout()
+        super().tearDown()
+
+    def _mock_request(
+        self,
+        method: str,
+        url: str,
+        params: Dict[str, str] | None = None,
+        headers: Dict[str, str] | None = None
+    ) -> webtest.TestResponse:
+        """Returns a mock response for the given request."""
+        if method == 'GET':
+            if url == '/session_begin':
+                assert headers is not None
+                cur_token = headers.get('Authorization', '').strip('Bearer ')
+                for email, token in self.token_by_email.items():
+                    if token == cur_token:
+                        self._mock_login_as_admin(email)
+                        break
+            with self.swap(
+                base, 'load_template', test_utils.mock_load_template
+            ):
+                return self.testapp.get(url, params=params, headers=headers)
+        if method == 'POST':
+            return self.testapp.post(url, params=params, headers=headers)
+        if method == 'PUT':
+            return self.testapp.put(url, params=params, headers=headers)
+
+    def _mock_firebase_auth_create_user(self, **kwargs: str) -> None:
+        """Mock for firebase_auth.create_user()."""
+        email = kwargs['email']
+        auth_id = self.get_auth_id_from_email(email)
+        self.token_by_email[email] = (
+            self.firebase_sdk_stub.create_user(auth_id, email))
+        print('token_by_email: %s' % self.token_by_email)
+
+    def _mock_login_as_admin(self, email: str) -> None:
+        """Sets the environment variables to simulate a login of admin."""
+        self.login(email, is_super_admin=True)
+
+    def test_populate_debug_data(self) -> None:
+        with self.request_swap, self.create_user_swap, self.begin_session_swap:
+            with self.init_app_swap as init_app_counter:
+                self.initializer.populate_debug_data()
+
+        self.assertEqual(init_app_counter.times_called, 1)
+        # Asserts that the function _mock_login_as_admin() is called.
+        self.assertEqual(
+            os.environ['USER_EMAIL'],
+            contributor_dashboard_debug.SUPER_ADMIN_EMAIL)
+        self.assertEqual(os.environ['USER_IS_ADMIN'], '1')
+
+        self._assert_user_roles(
+            contributor_dashboard_debug.SUPER_ADMIN_USERNAME,
+            contributor_dashboard_debug.SUPER_ADMIN_ROLES)
+        self._assert_can_submit_question_suggestions(
+            contributor_dashboard_debug.CONTRIBUTOR_USERNAME)
+        self._assert_generate_sample_new_structures_data()
+        self._assert_topics_in_classroom(
+            contributor_dashboard_debug.CLASSROOM_NAME)
+
+    def _assert_user_roles(self, username: str, roles: List[str]) -> None:
+        """Asserts that the user has the given roles."""
+        user_settings = user_services.get_user_settings_from_username(username)
+        assert user_settings is not None
+        self.assertEqual(
+            user_settings.roles, [feconf.ROLE_ID_FULL_USER] + roles)
+
+    def _assert_can_submit_question_suggestions(self, username: str) -> None:
+        """Asserts that the user can submit question suggestions."""
+        user_id = user_services.get_user_id_from_username(username)
+        assert user_id is not None
+        self.assertTrue(user_services.can_submit_question_suggestions(user_id))
+
+    def _assert_generate_sample_new_structures_data(self) -> None:
+        """Asserts that the sample new structures data is generated."""
+        topic_summaries = topic_fetchers.get_all_topic_summaries()
+        self.assertEqual(len(topic_summaries), 2)
+
+        topic = topic_fetchers.get_topic_by_name('Dummy Topic 1')
+        assert topic is not None
+        story_id = topic.canonical_story_references[0].story_id
+        self.assertIsNotNone(
+            story_fetchers.get_story_by_id(story_id, strict=False))
+
+        skill_summaries = skill_services.get_all_skill_summaries()
+        self.assertEqual(len(skill_summaries), 3)
+
+        questions, _ = (
+            question_fetchers.get_questions_and_skill_descriptions_by_skill_ids(
+                10,
+                [
+                    skill_summaries[0].id,
+                    skill_summaries[1].id,
+                    skill_summaries[2].id
+                ],
+                0)
+        )
+        self.assertEqual(len(questions), 3)
+
+        translation_opportunities, _, _ = (
+            opportunity_services.get_translation_opportunities('hi', '', None))
+        self.assertEqual(len(translation_opportunities), 3)
+
+    def _assert_topics_in_classroom(self, classroom_name: str) -> None:
+        """Asserts that test topics are in the classroom."""
+        classroom_dict = self.get_json(
+            '%s/%s' % (feconf.CLASSROOM_DATA_HANDLER, classroom_name))
+        topic_summaries = topic_fetchers.get_all_topic_summaries()
+        for topic_summary in topic_summaries:
+            topic_summary_dict = dict(topic_summary.to_dict())
+            topic_summary_dict['is_published'] = False
+            self.assertIn(
+                topic_summary_dict, classroom_dict['topic_summary_dicts'])
+
+    def test_sign_up_new_user(self) -> None:
+        auth_host = os.environ.get('FIREBASE_AUTH_EMULATOR_HOST')
+        if auth_host is None:
+            os.environ['FIREBASE_AUTH_EMULATOR_HOST'] = '0000'
+
+        with self.request_swap, self.create_user_swap, self.begin_session_swap:
+            with self.init_app_swap as init_app_counter:
+                self.initializer.populate_debug_data()
+
+        self.assertEqual(init_app_counter.times_called, 1)
+        # Asserts that the environment variable 'FIREBASE_AUTH_EMULATOR_HOST' is
+        # not changed after calling the function populate_debug_data().
+        if auth_host is None:
+            self.assertEqual(os.environ['FIREBASE_AUTH_EMULATOR_HOST'], '0000')
+            del os.environ['FIREBASE_AUTH_EMULATOR_HOST']
+        else:
+            self.assertNotIn('FIREBASE_AUTH_EMULATOR_HOST', auth_host)
+
+        self._assert_sign_up_new_user(
+            contributor_dashboard_debug.SUPER_ADMIN_EMAIL,
+            contributor_dashboard_debug.SUPER_ADMIN_USERNAME)
+        self._assert_sign_up_new_user(
+            contributor_dashboard_debug.CONTRIBUTOR_EMAIL,
+            contributor_dashboard_debug.CONTRIBUTOR_USERNAME)
+
+    def _assert_sign_up_new_user(self, email: str, username: str) -> None:
+        """Asserts that the function _mock_firebase_auth_create_user() is called
+        and a user with the given email and username is created.
+        """
+        self.assertIsNotNone(self.firebase_sdk_stub.get_user_by_email(email))
+        user_settings = user_services.get_user_settings_from_email(email)
+        assert user_settings is not None
+        self.assertEqual(user_settings.username, username)
+
+    def test_sign_in(self) -> None:
+        sign_in_swap = self.swap_with_checks(
+            requests, 'post', self._mock_firebase_auth_sign_in,
+            expected_args=[
+                (contributor_dashboard_debug.FIREBASE_SIGN_IN_URL,),
+                (contributor_dashboard_debug.FIREBASE_SIGN_IN_URL,),
+                (contributor_dashboard_debug.FIREBASE_SIGN_IN_URL,)
+            ]
+        )
+
+        with self.request_swap, self.create_user_swap:
+            with sign_in_swap, self.init_app_swap as init_app_counter:
+                self.initializer.populate_debug_data()
+
+        self.assertEqual(init_app_counter.times_called, 1)
+        # Asserts that the function _mock_firebase_auth_sign_in() is called.
+        self.assertEqual(
+            self.token_of_current_user,
+            self.token_by_email[contributor_dashboard_debug.SUPER_ADMIN_EMAIL])
+        # Asserts that the function mock_establish_auth_session() is called.
+        self.assertEqual(
+            os.environ['USER_EMAIL'],
+            contributor_dashboard_debug.SUPER_ADMIN_EMAIL)
+        self.assertEqual(os.environ['USER_IS_ADMIN'], '1')
+
+    def _mock_firebase_auth_sign_in(
+        self, _: str, **kwargs: Dict[str, str]
+    ) -> MockResponse:
+        """Mock for the post request to FIREBASE_SIGN_IN_URL, where the response
+        including the token information.
+        """
+        email = kwargs['json']['email']
+        self.token_of_current_user = self.token_by_email[email]
+        return MockResponse(json={'idToken': self.token_of_current_user})
diff --git a/scripts/start_test.py b/scripts/start_test.py
--- a/scripts/start_test.py
+++ b/scripts/start_test.py
@@ -20,6 +20,7 @@
 from core.tests import test_utils
 from scripts import build
 from scripts import common
+from scripts import contributor_dashboard_debug
 from scripts import extend_index_yaml
 from scripts import install_third_party_libs
 from scripts import servers
@@ -244,3 +245,35 @@ def test_source_maps_are_compiled_by_webpack(self) -> None:
                 )
             ],
             self.print_arr)
+
+    def test_start_servers_successfully_in_contributor_dashboard_debug_mode(
+        self
+    ) -> None:
+        with self.swap_install_third_party_libs:
+            from scripts import start
+        swap_build = self.swap_with_checks(
+            build, 'main', lambda **unused_kwargs: None,
+            expected_kwargs=[{'args': []}])
+        populate_data_swap = self.swap_with_call_counter(
+            contributor_dashboard_debug.ContributorDashboardDebugInitializer,
+            'populate_debug_data'
+        )
+        with self.swap_cloud_datastore_emulator, self.swap_webpack_compiler:
+            with self.swap_elasticsearch_dev_server, self.swap_redis_server:
+                with self.swap_firebase_auth_emulator, self.swap_dev_appserver:
+                    with self.swap_extend_index_yaml, swap_build:
+                        with self.swap_create_server, self.swap_print:
+                            with populate_data_swap as populate_data_counter:
+                                start.main(args=['--contributor_dashboard'])
+
+        self.assertEqual(populate_data_counter.times_called, 1)
+        self.assertIn(
+            [
+                'INFORMATION',
+                (
+                    'Local development server is ready! Opening a default web '
+                    'browser window pointing to it: '
+                    'http://localhost:%s/' % PORT_NUMBER_FOR_GAE_SERVER
+                )
+            ],
+            self.print_arr)