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_advance_demo_user(self, get_drill_mock): 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._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 main() -> None: global SEQ if len(sys.argv) > 1: lang = sys.argv[1] else: lang = "en" repo = InMemoryRepository(lang) validator = FakeRegistrationValidator() # kick off the language choice drill process_command( ProcessSMSMessage(PHONE_NUMBER, "00-language", registration_validator=validator), "1", repo=repo, ) try: while True: message = input("> ") SEQ += 1 process_command( ProcessSMSMessage(PHONE_NUMBER, message, registration_validator=validator), str(SEQ), repo=repo, ) except EOFError: pass dialog_state = repo.fetch_dialog_state(PHONE_NUMBER) print(f"{dialog_state.user_profile}")
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_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_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)
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_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 handle_inbound_commands(commands: List[InboundCommand]): for command in commands: if command.command_type == InboundCommandType.INBOUND_SMS: process_command( ProcessSMSMessage(phone_number=command.payload["From"], content=command.payload["Body"]), command.sequence_number, ) elif command.command_type == InboundCommandType.START_DRILL: process_command( StartDrill( phone_number=command.payload["phone_number"], drill_slug=command.payload["drill_slug"], ), command.sequence_number, ) elif command.command_type == InboundCommandType.TRIGGER_REMINDER: process_command( TriggerReminder( phone_number=command.payload["phone_number"], drill_instance_id=uuid.UUID( command.payload["drill_instance_id"]), prompt_slug=command.payload["prompt_slug"], ), command.sequence_number, ) else: raise RuntimeError(f"Unknown command: {command.command_type}") return {"statusCode": 200}
def test_conclude_with_right_answer(self, get_drill_mock): self.dialog_state.user_profile.validated = True self._set_current_prompt(3, should_advance=True) 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.dialog_state.drill_instance_id) advanced_event: AdvancedToNextPrompt = batch.events[1] # type: ignore self.assertEqual(self.drill.prompts[4], advanced_event.prompt) self.assertEqual(self.dialog_state.drill_instance_id, advanced_event.drill_instance_id) drill_completed_event: DrillCompleted = batch.events[2] # type: ignore self.assertEqual(self.dialog_state.drill_instance_id, drill_completed_event.drill_instance_id)
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_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")
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_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_advance_sequence_numbers(self, get_drill_mock): validator = MagicMock() validation_payload = CodeValidationPayload( valid=True, account_info={"company": "WeWork"}) 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_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_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)
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_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_revalidate_demo_user(self, get_drill_mock): 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._process_command(command) validation_payload = CodeValidationPayload( valid=True, account_info={"company": "WeWork"}) 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_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, get_drill_mock): validator = MagicMock() validation_payload = CodeValidationPayload( valid=True, account_info={"company": "WeWork"}) 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 )
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_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_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_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)
def handle_inbound_commands(commands: List[InboundCommand]) -> dict: for command in commands: if command.command_type is InboundCommandType.INBOUND_SMS: process_command( ProcessSMSMessage( phone_number=command.payload["From"], content=command.payload["Body"], ), command.sequence_number, ) elif command.command_type is InboundCommandType.START_DRILL: process_command( StartDrill( phone_number=command.payload["phone_number"], drill_slug=command.payload["drill_slug"], drill_body=command.payload["drill_body"], drill_instance_id=command.payload["drill_instance_id"], ), command.sequence_number, ) elif command.command_type is InboundCommandType.SEND_AD_HOC_MESSAGE: process_command( SendAdHocMessage( phone_number=command.payload["phone_number"], message=command.payload["message"], media_url=command.payload["media_url"], ), command.sequence_number, ) elif command.command_type is InboundCommandType.UPDATE_USER: process_command( UpdateUser( phone_number=command.payload["phone_number"], user_profile_data=command.payload["user_profile_data"], purge_drill_state=command.payload.get("purge_drill_state") or False, ), command.sequence_number, ) else: raise RuntimeError(f"Unknown command: {command.command_type}") return {"statusCode": 200}
def test_advance_with_too_many_wrong_answers(self, get_drill_mock): self.dialog_state.user_profile.validated = True self._set_current_prompt(2, should_advance=False) 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)
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)