Пример #1
0
    def test_get_progress_multiple_users(self):
        user_id1 = self._make_user_and_get_id()
        user_id2 = self._make_user_and_get_id(phone_number="987654321")
        event = DrillStarted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill=Drill(slug=get_all_drill_slugs()[0], name="name",
                        prompts=[]),
            first_prompt=self.prompt,
            created_time=datetime.datetime.now(datetime.timezone.utc) -
            datetime.timedelta(minutes=32),
        )
        self.repo.update_user(self._make_batch([event]))

        event2 = DrillStarted(
            phone_number="987654321",
            user_profile=UserProfile(True),
            drill=Drill(slug=get_all_drill_slugs()[1], name="name",
                        prompts=[]),
            first_prompt=self.prompt,
            created_time=datetime.datetime.now(datetime.timezone.utc) -
            datetime.timedelta(minutes=32),
        )
        self.repo.update_user(self._make_batch([event2]))

        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(2, len(drill_progresses))
        if drill_progresses[0].user_id == user_id1:
            drill_progress1 = drill_progresses[0]
            drill_progress2 = drill_progresses[1]
        else:
            drill_progress1 = drill_progresses[1]
            drill_progress2 = drill_progresses[0]
        self.assertEqual(
            DrillProgress(
                phone_number=self.phone_number,
                user_id=user_id1,
                first_unstarted_drill_slug=get_all_drill_slugs()[1],
                first_incomplete_drill_slug=get_all_drill_slugs()[0],
            ),
            drill_progress1,
        )
        self.assertEqual(
            DrillProgress(
                phone_number="987654321",
                user_id=user_id2,
                first_unstarted_drill_slug=get_all_drill_slugs()[0],
                first_incomplete_drill_slug=get_all_drill_slugs()[0],
            ),
            drill_progress2,
        )
Пример #2
0
    def test_drill_with_one_prompt(self, *args):
        choose_language_drill = Drill(
            name="test-drill",
            slug="test-drill",
            prompts=[
                Prompt(
                    slug="ignore-response-1",
                    messages=[PromptMessage(text="{{msg1}}")],
                    response_user_profile_key="language",
                ),
            ],
        )
        self.dialog_state.user_profile.validated = True
        self.dialog_state.current_drill = choose_language_drill
        self._set_current_prompt(0, drill=choose_language_drill)
        command = ProcessSMSMessage(self.phone_number, "es")
        batch = self._process_command(command)

        self._assert_event_types(
            batch,
            DialogEventType.COMPLETED_PROMPT,
            DialogEventType.DRILL_COMPLETED,
        )
        self.assertEqual(batch.events[0].user_profile_updates, {"language": "es"})
        self.assertEqual(batch.user_profile.language, "es")
Пример #3
0
    def test_get_progress_empty(self):
        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(0, len(drill_progresses))

        # no started drills, so we won't receive anything
        self._make_user_and_get_id()
        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(0, len(drill_progresses))

        for slug in get_all_drill_slugs():
            event = DrillStarted(
                phone_number=self.phone_number,
                user_profile=UserProfile(True),
                drill=Drill(slug=slug, name="name", prompts=[]),
                first_prompt=self.prompt,
            )
            event2 = DrillCompleted(
                phone_number=self.phone_number,
                user_profile=UserProfile(True),
                drill_instance_id=event.drill_instance_id,
            )
            self.repo.update_user(self._make_batch([event, event2]))
        # all drills complete, so we won't receive anything
        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(0, len(drill_progresses))
Пример #4
0
    def test_drill_started_and_completed(self):
        user_id = self._make_user_and_get_id()
        event = DrillStarted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill=Drill(slug=get_all_drill_slugs()[0],
                        name="drill",
                        prompts=[]),
            first_prompt=self.prompt,
        )
        self.repo.update_user(self._make_batch([event]))
        drill_status = self.repo.get_drill_status(user_id,
                                                  get_all_drill_slugs()[0])
        self.assertEqual(event.created_time, drill_status.started_time)
        self.assertIsNone(drill_status.completed_time)

        event2 = DrillCompleted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill_instance_id=event.drill_instance_id,
        )
        self.repo.update_user(self._make_batch([event2]))
        drill_status = self.repo.get_drill_status(user_id,
                                                  get_all_drill_slugs()[0])
        self.assertEqual(event.created_time, drill_status.started_time)
        self.assertEqual(event2.created_time, drill_status.completed_time)
Пример #5
0
 def test_user_revalidated(self):
     user_id = self._make_user_and_get_id()
     for slug in get_all_drill_slugs():
         drill = copy(self.drill)
         drill.slug = slug
         event = DrillStarted(
             phone_number=self.phone_number,
             user_profile=UserProfile(True),
             drill=Drill(slug=slug, name="name", prompts=[]),
             first_prompt=self.prompt,
         )
         event2 = DrillCompleted(
             phone_number=self.phone_number,
             user_profile=UserProfile(True),
             drill_instance_id=event.drill_instance_id,
         )
         self.repo._mark_drill_started(user_id, event, self.repo.engine)
         self.repo._mark_drill_completed(event2, self.repo.engine)
         drill_status = self.repo.get_drill_status(user_id, slug)
         self.assertIsNotNone(drill_status.started_time)
         self.assertIsNotNone(drill_status.completed_time)
     self.repo.update_user(
         self._make_batch([
             UserValidated(
                 phone_number=self.phone_number,
                 user_profile=UserProfile(True),
                 code_validation_payload=CodeValidationPayload(valid=True),
             )
         ]))
     for slug in get_all_drill_slugs():
         drill_status = self.repo.get_drill_status(user_id, slug)
         self.assertIsNone(drill_status.drill_instance_id)
         self.assertIsNone(drill_status.started_time)
         self.assertIsNone(drill_status.completed_time)
Пример #6
0
class StartDrill(Command):
    def __init__(self, phone_number: str, drill_slug: str, drill_body: dict,
                 drill_instance_id: uuid.UUID) -> None:
        super().__init__(phone_number)
        self.drill_slug = drill_slug
        self.drill_instance_id = drill_instance_id
        self.drill = Drill(**drill_body)

    def __str__(self) -> str:
        return f"Start Drill: {self.drill_slug}"

    def execute(
        self, dialog_state: DialogState
    ) -> List[stopcovid.dialog.models.events.DialogEvent]:
        if dialog_state.user_profile.opted_out:
            logging.warning(
                f"Attempted to initiate a drill for {dialog_state.phone_number} who has opted out."
            )
            return []
        return [
            DrillStarted(
                phone_number=self.phone_number,
                user_profile=dialog_state.user_profile,
                drill=self.drill,
                first_prompt=self.drill.first_prompt(),
                drill_instance_id=self.drill_instance_id,
            )
        ]
Пример #7
0
 def setUp(self):
     logging.disable(logging.CRITICAL)
     self.repo = DrillProgressRepository(get_test_sqlalchemy_engine)
     self.repo.drop_and_recreate_tables_testing_only()
     self.phone_number = "123456789"
     self.prompt = Prompt(slug="first", messages=[])
     self.drill = Drill(slug="slug", name="name", prompts=[self.prompt])
     self.seq = 0
Пример #8
0
 def setUp(self) -> None:
     self.prompt = Prompt(
         slug="my-prompt",
         messages=[
             PromptMessage(text="one",
                           media_url="http://giphy.com/puppies/1"),
             PromptMessage(text="two"),
         ],
     )
     self.drill = Drill(name="01 START",
                        slug="01-start",
                        prompts=[self.prompt])
Пример #9
0
 def setUp(self) -> None:
     logging.disable(logging.CRITICAL)
     self.drill_instance_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
     self.phone_number = "123456789"
     self.dialog_state = DialogState(
         phone_number=self.phone_number,
         seq="0",
         drill_instance_id=self.drill_instance_id,
     )
     self.drill = Drill(
         name="test-drill",
         slug="test-drill",
         prompts=[
             Prompt(slug="ignore-response-1", messages=[PromptMessage(text="{{msg1}}")]),
             Prompt(
                 slug="store-response",
                 messages=[PromptMessage(text="{{msg1}}")],
                 response_user_profile_key="self_rating_1",
             ),
             Prompt(
                 slug="graded-response-1",
                 messages=[PromptMessage(text="{{msg1}}")],
                 correct_response="{{response1}}",
             ),
             Prompt(
                 slug="graded-response-2",
                 messages=[PromptMessage(text="{{msg1}}")],
                 correct_response="{{response1}}",
             ),
             Prompt(slug="ignore-response-2", messages=[PromptMessage(text="{{msg1}}")]),
         ],
     )
     self.repo = MagicMock()
     self.repo.fetch_dialog_state = MagicMock(return_value=self.dialog_state)
     self.repo.persist_dialog_state = MagicMock()
     self.next_seq = 1
     self.now = datetime.now(UTC)
Пример #10
0
    def test_get_progress_recent_interaction(self):
        self._make_user_and_get_id()
        event = DrillStarted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill=Drill(slug=get_all_drill_slugs()[0], name="name",
                        prompts=[]),
            first_prompt=self.prompt,
            created_time=datetime.datetime.now(datetime.timezone.utc) -
            datetime.timedelta(minutes=20),
        )
        self.repo.update_user(self._make_batch([event]))

        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(0, len(drill_progresses))
Пример #11
0
    def test_get_progress_for_users_one_user(self):
        user_id = self._make_user_and_get_id()
        event = DrillStarted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill=Drill(slug=get_all_drill_slugs()[0], name="name",
                        prompts=[]),
            first_prompt=self.prompt,
            created_time=datetime.datetime.now(datetime.timezone.utc) -
            datetime.timedelta(minutes=32),
        )
        self.repo.update_user(self._make_batch([event]))

        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(1, len(drill_progresses))
        self.assertEqual(
            DrillProgress(
                user_id=user_id,
                phone_number=self.phone_number,
                first_incomplete_drill_slug=get_all_drill_slugs()[0],
                first_unstarted_drill_slug=get_all_drill_slugs()[1],
            ),
            drill_progresses[0],
        )

        event2 = DrillCompleted(
            phone_number=self.phone_number,
            user_profile=UserProfile(True),
            drill_instance_id=event.drill_instance_id,
            created_time=datetime.datetime.now(datetime.timezone.utc) -
            datetime.timedelta(minutes=31),
        )
        self.repo.update_user(self._make_batch([event2]))
        drill_progresses = list(
            self.repo.get_progress_for_users_who_need_drills(30))
        self.assertEqual(1, len(drill_progresses))
        self.assertEqual(
            DrillProgress(
                user_id=user_id,
                phone_number=self.phone_number,
                first_incomplete_drill_slug=get_all_drill_slugs()[1],
                first_unstarted_drill_slug=get_all_drill_slugs()[1],
            ),
            drill_progresses[0],
        )
Пример #12
0
 def test_delete_user(self):
     event = DrillStarted(
         phone_number=self.phone_number,
         user_profile=UserProfile(True),
         drill=Drill(slug=get_all_drill_slugs()[0], name="name",
                     prompts=[]),
         first_prompt=self.prompt,
         created_time=datetime.datetime.now(datetime.timezone.utc) -
         datetime.timedelta(minutes=32),
     )
     self.repo.update_user(self._make_batch([event]))
     self.assertIsNotNone(
         self.repo.get_user_for_phone_number(self.phone_number,
                                             self.repo.engine))
     self.repo.delete_user_info(self.phone_number)
     self.assertIsNone(
         self.repo.get_user_for_phone_number(self.phone_number,
                                             self.repo.engine))
Пример #13
0
    def setUp(self):
        self.phone = "+15554238324"
        self.validated_user_profile = UserProfileSchema().load({
            "validated": True,
            "language": "en",
            "name": "Mario",
            "is_demo": False
        })
        self.non_validated_user_profile = UserProfileSchema().load({
            "validated":
            False,
            "language":
            "en",
            "name":
            "Luigi",
            "is_demo":
            False
        })

        self.drill = Drill(
            name="Test Drill",
            slug="test-drill",
            prompts=[
                Prompt(slug="ignore-response-1",
                       messages=[PromptMessage(text="Hello")]),
                Prompt(
                    slug="graded-response-1",
                    messages=[
                        PromptMessage(text="Intro!"),
                        PromptMessage(text="Question 1")
                    ],
                    correct_response="a",
                ),
                Prompt(
                    slug="graded-response-2",
                    messages=[PromptMessage(text="Question 2")],
                    correct_response="b",
                ),
            ],
        )
    def setUp(self):
        self.phone = "+15554238324"
        self.validated_user_profile = UserProfile(
            validated=True,
            language="en",
            name="Mario",
            is_demo=False,
            account_info=AccountInfo(
                employer_id=1,
                employer_name="Tacombi",
                unit_id=1,
                unit_name="unit name",
            ),
        )
        self.non_validated_user_profile = UserProfile(validated=False,
                                                      language="en",
                                                      name="Luigi",
                                                      is_demo=False)

        self.drill = Drill(
            name="Test Drill",
            slug="test-drill",
            prompts=[
                Prompt(slug="ignore-response-1",
                       messages=[PromptMessage(text="Hello")]),
                Prompt(
                    slug="graded-response-1",
                    messages=[
                        PromptMessage(text="Intro!"),
                        PromptMessage(text="Question 1"),
                    ],
                    correct_response="a) Philadelphia",
                ),
                Prompt(
                    slug="graded-response-2",
                    messages=[PromptMessage(text="Question 2")],
                    correct_response="b",
                ),
            ],
        )
Пример #15
0
 def setUp(self):
     logging.disable(logging.CRITICAL)
     self.repo = DrillProgressRepository(db.get_test_sqlalchemy_engine)
     self.repo.drop_and_recreate_tables_testing_only()
     self.phone_number = "123456789"
     self.prompt1 = Prompt(slug="first", messages=[])
     self.prompt2 = Prompt(slug="second", messages=[])
     self.drill = Drill(slug="slug", name="name", prompts=[self.prompt1])
     self.seq = 1
     self.user_id = self.repo._create_or_update_user(
         DialogEventBatch(
             events=[
                 UserValidated(self.phone_number, UserProfile(True),
                               CodeValidationPayload(True))
             ],
             phone_number=self.phone_number,
             seq="0",
             batch_id=uuid.uuid4(),
         ),
         None,
         self.repo.engine,
     )
     self.drill_instance = self._make_drill_instance()
Пример #16
0
 def __init__(self, phone_number: str, drill_slug: str, drill_body: dict,
              drill_instance_id: uuid.UUID) -> None:
     super().__init__(phone_number)
     self.drill_slug = drill_slug
     self.drill_instance_id = drill_instance_id
     self.drill = Drill(**drill_body)
Пример #17
0
from stopcovid.drills.drills import Prompt, PromptMessage, Drill

DRILL = Drill(
    name="test-drill",
    slug="test-drill",
    prompts=[
        Prompt(slug="ignore-response-1",
               messages=[PromptMessage(text="{{msg1}}")]),
        Prompt(
            slug="store-response",
            messages=[PromptMessage(text="{{msg1}}")],
            response_user_profile_key="self_rating_1",
        ),
        Prompt(
            slug="graded-response-1",
            messages=[PromptMessage(text="{{msg1}}")],
            correct_response="{{response1}}",
        ),
        Prompt(
            slug="graded-response-2",
            messages=[PromptMessage(text="{{msg1}}")],
            correct_response="{{response1}}",
        ),
        Prompt(slug="ignore-response-2",
               messages=[PromptMessage(text="{{msg1}}")]),
    ],
)

NOW = datetime.datetime.now(datetime.timezone.utc)
Пример #18
0
class TestProcessCommand(unittest.TestCase):
    def setUp(self) -> None:
        logging.disable(logging.CRITICAL)
        self.drill_instance_id = uuid.UUID("11111111-1111-1111-1111-111111111111")
        self.phone_number = "123456789"
        self.dialog_state = DialogState(
            phone_number=self.phone_number,
            seq="0",
            drill_instance_id=self.drill_instance_id,
        )
        self.drill = Drill(
            name="test-drill",
            slug="test-drill",
            prompts=[
                Prompt(slug="ignore-response-1", messages=[PromptMessage(text="{{msg1}}")]),
                Prompt(
                    slug="store-response",
                    messages=[PromptMessage(text="{{msg1}}")],
                    response_user_profile_key="self_rating_1",
                ),
                Prompt(
                    slug="graded-response-1",
                    messages=[PromptMessage(text="{{msg1}}")],
                    correct_response="{{response1}}",
                ),
                Prompt(
                    slug="graded-response-2",
                    messages=[PromptMessage(text="{{msg1}}")],
                    correct_response="{{response1}}",
                ),
                Prompt(slug="ignore-response-2", messages=[PromptMessage(text="{{msg1}}")]),
            ],
        )
        self.repo = MagicMock()
        self.repo.fetch_dialog_state = MagicMock(return_value=self.dialog_state)
        self.repo.persist_dialog_state = MagicMock()
        self.next_seq = 1
        self.now = datetime.now(UTC)

    def _process_command(self, command) -> DialogEventBatch:
        persist_dialog_call_count = self.repo.persist_dialog_state.call_count
        process_command(command, str(self.next_seq), repo=self.repo)
        self.next_seq += 1
        self.assertEqual(
            persist_dialog_call_count + 1,
            len(self.repo.persist_dialog_state.call_args_list),
        )
        return self.repo.persist_dialog_state.call_args[0][0]

    def _assert_event_types(self, batch: DialogEventBatch, *args: DialogEventType):
        self.assertEqual(len(args), len(batch.events), f"{args} vs {batch.events}")
        for i in range(len(batch.events)):
            self.assertEqual(args[i], batch.events[i].event_type)

    def _set_current_prompt(self, prompt_index: int, drill: Optional[Drill] = None):
        if not drill:
            drill = self.drill
        self.dialog_state.current_drill = drill
        prompt = drill.prompts[prompt_index]
        self.dialog_state.current_prompt_state = PromptState(slug=prompt.slug, start_time=self.now)

    def test_skip_processed_sequence_numbers(self):
        command = Mock(wraps=ProcessSMSMessage(self.phone_number, "hey"))
        process_command(command, "0", repo=self.repo)
        self.assertFalse(command.execute.called)

    def test_advance_sequence_numbers(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(
            valid=True,
            account_info={
                "employer_id": 1,
                "unit_id": 1,
                "employer_name": "employer_name",
                "unit_name": "unit_name",
            },
        )
        validator.validate_code = MagicMock(return_value=validation_payload)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)
        batch = self._process_command(command)
        self.assertEqual(1, len(batch.events))
        self.assertEqual("1", self.dialog_state.seq)

    def test_first_message_validates_user(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(
            valid=True,
            account_info={
                "employer_id": 1,
                "unit_id": 1,
                "employer_name": "employer_name",
                "unit_name": "unit_name",
            },
        )
        validator.validate_code = MagicMock(return_value=validation_payload)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)
        self.assertFalse(self.dialog_state.user_profile.validated)

        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.USER_VALIDATED)
        self.assertEqual(
            validation_payload, batch.events[0].code_validation_payload  # type: ignore
        )
        # and account info is set on the event and user profile
        self.assertEqual(
            batch.events[0].user_profile.account_info,
            AccountInfo(
                employer_id=1, unit_id=1, employer_name="employer_name", unit_name="unit_name"
            ),
        )

    def test_advance_demo_user(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(valid=True, is_demo=True)
        validator.validate_code = MagicMock(return_value=validation_payload)
        self.assertFalse(self.dialog_state.user_profile.validated)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)

        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.USER_VALIDATED)

        command = StartDrill(self.phone_number, self.drill.slug, self.drill.dict(), uuid.uuid4())
        self._process_command(command)

        # the user's next message isn't a validation code - so we just keep going
        validation_payload = CodeValidationPayload(valid=False)
        validator.validate_code = MagicMock(return_value=validation_payload)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)

        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.COMPLETED_PROMPT,
            DialogEventType.ADVANCED_TO_NEXT_PROMPT,
        )

    def test_first_message_does_not_validate_user(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(valid=False)
        validator.validate_code = MagicMock(return_value=validation_payload)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)
        self.assertFalse(self.dialog_state.user_profile.validated)

        batch = self._process_command(command)

        self._assert_event_types(batch, DialogEventType.USER_VALIDATION_FAILED)

    def test_start_drill(self):
        self.dialog_state.user_profile.validated = True
        command = StartDrill(self.phone_number, self.drill.slug, self.drill.dict(), uuid.uuid4())

        batch = self._process_command(command)

        self._assert_event_types(batch, DialogEventType.DRILL_STARTED)
        event: DrillStarted = batch.events[0]  # type: ignore
        self.assertEqual(self.drill.dict(), event.drill.dict())
        self.assertEqual(self.drill.first_prompt().slug, event.first_prompt.slug)
        self.assertIsNotNone(event.drill_instance_id)

    def test_start_drill_not_validated(self):
        self.dialog_state.user_profile.validated = False
        self.dialog_state.user_profile.opted_out = False
        command = StartDrill(self.phone_number, self.drill.slug, self.drill.dict(), uuid.uuid4())

        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.DRILL_STARTED)

    def test_revalidate_demo_user(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(valid=True, is_demo=True)
        validator.validate_code = MagicMock(return_value=validation_payload)
        self.assertFalse(self.dialog_state.user_profile.validated)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)

        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.USER_VALIDATED)

        command = StartDrill(self.phone_number, self.drill.slug, self.drill.dict(), uuid.uuid4())
        self._process_command(command)

        validation_payload = CodeValidationPayload(
            valid=True,
            account_info={
                "employer_id": 1,
                "unit_id": 1,
                "employer_name": "employer_name",
                "unit_name": "unit_name",
            },
        )
        validator.validate_code = MagicMock(return_value=validation_payload)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)

        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.USER_VALIDATED)

    def test_revalidate_user_without_org(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(valid=True, is_demo=True)
        validator.validate_code = MagicMock(return_value=validation_payload)
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=None)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.USER_VALIDATED)
        validation_payload = CodeValidationPayload(
            valid=True,
            account_info={
                "employer_id": 1,
                "unit_id": 1,
                "employer_name": "employer_name",
                "unit_name": "unit_name",
            },
        )

    def test_doesnt_revalidate_someone_with_an_org(self):
        validator = MagicMock()
        validation_payload = CodeValidationPayload(valid=True, is_demo=True)
        validator.validate_code = MagicMock(return_value=validation_payload)
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "hey", registration_validator=validator)
        batch = self._process_command(command)

        # not a USER_VALIDATED event
        self._assert_event_types(batch, DialogEventType.UNHANDLED_MESSAGE_RECEIVED)

    def test_start_drill_opted_out(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.opted_out = True
        command = StartDrill(self.phone_number, self.drill.slug, self.drill.dict(), uuid.uuid4())

        batch = self._process_command(command)
        self.assertEqual(0, len(batch.events))

    def test_complete_and_advance(self):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(0)
        command = ProcessSMSMessage(self.phone_number, "go")
        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.COMPLETED_PROMPT,
            DialogEventType.ADVANCED_TO_NEXT_PROMPT,
        )
        completed_event: CompletedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(completed_event.prompt, self.drill.prompts[0])
        self.assertEqual(completed_event.response, "go")
        self.assertEqual(completed_event.drill_instance_id, self.dialog_state.drill_instance_id)

        advanced_event: AdvancedToNextPrompt = batch.events[1]  # type: ignore
        self.assertEqual(self.drill.prompts[1], advanced_event.prompt)
        self.assertEqual(self.dialog_state.drill_instance_id, advanced_event.drill_instance_id)

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=False)
    def test_repeat_with_wrong_answer(self, *args):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(2)
        command = ProcessSMSMessage(self.phone_number, "completely wrong answer")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.FAILED_PROMPT)

        failed_event: FailedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(failed_event.prompt, self.drill.prompts[2])
        self.assertFalse(failed_event.abandoned)
        self.assertEqual(failed_event.response, "completely wrong answer")
        self.assertEqual(failed_event.drill_instance_id, self.dialog_state.drill_instance_id)

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=False)
    def test_advance_with_too_many_wrong_answers(self, *args):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(2)
        self.dialog_state.current_prompt_state.failures = 1

        command = ProcessSMSMessage(self.phone_number, "completely wrong answer")
        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.FAILED_PROMPT,
            DialogEventType.ADVANCED_TO_NEXT_PROMPT,
        )

        failed_event: FailedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(failed_event.prompt, self.drill.prompts[2])
        self.assertTrue(failed_event.abandoned)
        self.assertEqual(failed_event.response, "completely wrong answer")
        self.assertEqual(failed_event.drill_instance_id, self.dialog_state.drill_instance_id)

        advanced_event: AdvancedToNextPrompt = batch.events[1]  # type: ignore
        self.assertEqual(self.drill.prompts[3], advanced_event.prompt)
        self.assertEqual(self.dialog_state.drill_instance_id, advanced_event.drill_instance_id)

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=True)
    def test_conclude_with_right_answer(self, *args):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(3)
        command = ProcessSMSMessage(self.phone_number, "foo")
        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.COMPLETED_PROMPT,
            DialogEventType.ADVANCED_TO_NEXT_PROMPT,
            DialogEventType.DRILL_COMPLETED,
        )
        completed_event: CompletedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(completed_event.prompt, self.drill.prompts[3])
        self.assertEqual(completed_event.response, "foo")
        self.assertEqual(completed_event.drill_instance_id, self.drill_instance_id)

        advanced_event: AdvancedToNextPrompt = batch.events[1]  # type: ignore
        self.assertEqual(self.drill.prompts[4], advanced_event.prompt)

        self.assertEqual(advanced_event.drill_instance_id, self.drill_instance_id)

        drill_completed_event: DrillCompleted = batch.events[2]  # type: ignore
        self.assertEqual(drill_completed_event.drill_instance_id, self.drill_instance_id)
        self.assertIsNone(self.dialog_state.drill_instance_id)

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=True)
    def test_drill_with_one_prompt(self, *args):
        choose_language_drill = Drill(
            name="test-drill",
            slug="test-drill",
            prompts=[
                Prompt(
                    slug="ignore-response-1",
                    messages=[PromptMessage(text="{{msg1}}")],
                    response_user_profile_key="language",
                ),
            ],
        )
        self.dialog_state.user_profile.validated = True
        self.dialog_state.current_drill = choose_language_drill
        self._set_current_prompt(0, drill=choose_language_drill)
        command = ProcessSMSMessage(self.phone_number, "es")
        batch = self._process_command(command)

        self._assert_event_types(
            batch,
            DialogEventType.COMPLETED_PROMPT,
            DialogEventType.DRILL_COMPLETED,
        )
        self.assertEqual(batch.events[0].user_profile_updates, {"language": "es"})
        self.assertEqual(batch.user_profile.language, "es")

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=False)
    def test_conclude_with_too_many_wrong_answers(self, *args):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(3)
        self.dialog_state.current_prompt_state.failures = 1

        command = ProcessSMSMessage(self.phone_number, "completely wrong answer")
        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.FAILED_PROMPT,
            DialogEventType.ADVANCED_TO_NEXT_PROMPT,
            DialogEventType.DRILL_COMPLETED,
        )

        failed_event: FailedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(failed_event.prompt, self.drill.prompts[3])
        self.assertTrue(failed_event.abandoned)
        self.assertEqual(failed_event.response, "completely wrong answer")
        self.assertEqual(failed_event.drill_instance_id, self.drill_instance_id)
        self.assertEqual(batch.events[0].user_profile_updates, None)

        advanced_event: AdvancedToNextPrompt = batch.events[1]  # type: ignore
        self.assertEqual(self.drill.prompts[4], advanced_event.prompt)
        self.assertEqual(advanced_event.drill_instance_id, self.drill_instance_id)

        drill_completed_event: DrillCompleted = batch.events[2]  # type: ignore
        self.assertEqual(drill_completed_event.drill_instance_id, self.drill_instance_id)
        self.assertEqual(self.dialog_state.drill_instance_id, None)

    @patch("stopcovid.drills.drills.Prompt.should_advance_with_answer", return_value=False)
    def test_fail_prompt_with_empty_response_stores_response_as_null(self, *args):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(3)
        self.dialog_state.current_prompt_state.failures = 0

        command = ProcessSMSMessage(self.phone_number, "")
        batch = self._process_command(command)
        self._assert_event_types(
            batch,
            DialogEventType.FAILED_PROMPT,
        )

        failed_event: FailedPrompt = batch.events[0]  # type: ignore
        self.assertEqual(failed_event.response, None)

    def test_opt_out(self):
        self.dialog_state.user_profile.validated = True
        self._set_current_prompt(0)
        command = ProcessSMSMessage(self.phone_number, "stop")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.OPTED_OUT)

    def test_message_during_opt_out(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.opted_out = True
        command = ProcessSMSMessage(self.phone_number, "it's not a bacteria")
        batch = self._process_command(command)
        self.assertEqual(0, len(batch.events))

    def test_opt_back_in(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        self.dialog_state.user_profile.opted_out = True
        command = ProcessSMSMessage(self.phone_number, "start")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.NEXT_DRILL_REQUESTED)

    def test_ask_for_help(self):
        command = ProcessSMSMessage(self.phone_number, "help")
        batch = self._process_command(command)
        self.assertEqual(0, len(batch.events))  # response handled by twilio

    def test_ask_for_help_validated(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "help")
        batch = self._process_command(command)
        self.assertEqual(0, len(batch.events))  # response handled by twilio

    def test_ask_for_more(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "more")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.NEXT_DRILL_REQUESTED)

    def test_ask_for_mas(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "mas")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.NEXT_DRILL_REQUESTED)

        command = ProcessSMSMessage(self.phone_number, "más")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.NEXT_DRILL_REQUESTED)

    def test_ask_for_drill(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        self.dialog_state.current_drill = None
        command = ProcessSMSMessage(self.phone_number, "go")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.DRILL_REQUESTED)

        command = ProcessSMSMessage(self.phone_number, "vamos")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.DRILL_REQUESTED)

    def test_ask_for_english_lesson_drill(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        self.dialog_state.current_drill = None
        command = ProcessSMSMessage(self.phone_number, "english")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.ENGLISH_LESSON_DRILL_REQUESTED)

        command = ProcessSMSMessage(self.phone_number, "esl")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.ENGLISH_LESSON_DRILL_REQUESTED)

    def test_send_ad_hoc_message(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        message = "An ad hoc message to a user"
        media_url = "https://gph.is/1w6mM6h"
        command = SendAdHocMessage(
            phone_number=self.phone_number, message=message, media_url=media_url
        )
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.AD_HOC_MESSAGE_SENT)

        event = batch.events[0]
        self.assertEqual(event.sms.body, message)  # type:ignore
        self.assertEqual(event.sms.media_url, media_url)  # type:ignore

    def test_ask_for_scheduling_drill(self):
        messages = ["schedule", "calendario"]
        for message in messages:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.SCHEDULING_DRILL_REQUESTED)
            self.assertIsNone(self.dialog_state.current_drill)
            self.assertIsNone(self.dialog_state.drill_instance_id)
            self.assertIsNone(self.dialog_state.current_prompt_state)
            self.assertEqual(
                batch.events[0].abandoned_drill_instance_id,
                uuid.UUID("11111111-1111-1111-1111-111111111111"),
            )

    def test_change_name_drill_requested(self):
        for message in ["name", "nombre"]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.NAME_CHANGE_DRILL_REQUESTED)
            self.assertIsNone(self.dialog_state.current_drill)
            self.assertIsNone(self.dialog_state.drill_instance_id)
            self.assertIsNone(self.dialog_state.current_prompt_state)
            self.assertEqual(
                batch.events[0].abandoned_drill_instance_id,
                uuid.UUID("11111111-1111-1111-1111-111111111111"),
            )

    def test_change_language_drill_requested(self):
        for message in ["lang", "language", "idioma"]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.LANGUAGE_CHANGE_DRILL_REQUESTED)
            self.assertIsNone(self.dialog_state.current_drill)
            self.assertIsNone(self.dialog_state.drill_instance_id)
            self.assertIsNone(self.dialog_state.current_prompt_state)
            self.assertEqual(
                batch.events[0].abandoned_drill_instance_id,
                uuid.UUID("11111111-1111-1111-1111-111111111111"),
            )

    def test_certain_keywords_ignored_while_during_lesson(self):
        for message in ["lang", "schedule", "horario", "more", "mas", "name"]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self._set_current_prompt(1)
            self.dialog_state.current_drill = self.drill
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(
                batch, DialogEventType.COMPLETED_PROMPT, DialogEventType.ADVANCED_TO_NEXT_PROMPT
            )

    def test_dashboard_requested(self):
        for message in ["info"]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.current_drill = "balbla"
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.DASHBOARD_REQUESTED)
            self.assertIsNone(
                batch.events[0].abandoned_drill_instance_id,
            )

    def test_support_requested(self):
        for message in [
            "support",
            "ayuda",
        ]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.current_drill = "balbla"
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.SUPPORT_REQUESTED)

    def test_menu_requested(self):
        for message in ["menu", "menú"]:
            self.dialog_state.user_profile.validated = True
            self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
            self.dialog_state.current_drill = "balbla"
            self.dialog_state.drill_instance_id = "11111111-1111-1111-1111-111111111111"
            self.dialog_state.current_prompt_state = "blabla"
            command = ProcessSMSMessage(self.phone_number, message)
            batch = self._process_command(command)
            self._assert_event_types(batch, DialogEventType.MENU_REQUESTED)

    def test_unhandled_message_received(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "BLABLABLA")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.UNHANDLED_MESSAGE_RECEIVED)
        self.assertEqual(batch.events[0].message, "BLABLABLA")

    def test_thank_you_received(self):
        self.dialog_state.user_profile.validated = True
        self.dialog_state.user_profile.account_info = AccountInfo(employer_id=1)
        command = ProcessSMSMessage(self.phone_number, "Thanks!!")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.THANK_YOU_RECEIVED)

    def test_demo_requested(self):
        command = ProcessSMSMessage(self.phone_number, "OPUS")
        batch = self._process_command(command)
        self._assert_event_types(batch, DialogEventType.DEMO_REQUESTED)

    def test_update_user(self):
        name = "foo"
        unit_id = 123
        employer_id = 456
        user_profile_data = {
            "name": name,
            "account_info": {"unit_id": unit_id, "employer_id": employer_id},
        }
        command = UpdateUser(self.phone_number, user_profile_data)

        batch = self._process_command(command)

        self._assert_event_types(batch, DialogEventType.USER_UPDATED)
        self.assertEqual(self.dialog_state.user_profile.name, name)
        self.assertEqual(self.dialog_state.user_profile.account_info.unit_id, unit_id)
        self.assertEqual(self.dialog_state.user_profile.account_info.employer_id, employer_id)