Exemple #1
0
def handler(event, context):
    verify_deploy_stage()
    kinesis = boto3.client("kinesis")
    stage = os.environ["STAGE"]
    idempotency_checker = IdempotencyChecker()

    inbound_commands = [
        _make_inbound_command(record) for record in event["Records"]
    ]
    for command in inbound_commands:
        if command.command_type == InboundCommandType.INBOUND_SMS:
            if not idempotency_checker.already_processed(
                    command.sequence_number, IDEMPOTENCY_REALM):
                logging.info(
                    f"Logging an INBOUND_SMS message in the message log")
                twilio_webhook = command.payload["twilio_webhook"]
                kinesis.put_record(
                    Data=json.dumps({
                        "type": "INBOUND_SMS",
                        "payload": twilio_webhook
                    }),
                    PartitionKey=command.payload["From"],
                    StreamName=f"message-log-{stage}",
                )
                idempotency_checker.record_as_processed(
                    command.sequence_number, IDEMPOTENCY_REALM,
                    IDEMPOTENCY_EXPIRATION_MINUTES)

    return {"statusCode": 200}
class ReminderTriggerer:
    def __init__(self, **kwargs):
        self.stage = os.environ.get("STAGE")
        self.drill_progress_repo = self._get_drill_progress_repo()
        self.command_publisher = CommandPublisher()
        self.idempotency_checker = IdempotencyChecker()

    def _get_drill_progress_repo(self):
        return DrillProgressRepository()

    def trigger_reminders(self):
        drill_instances = self.drill_progress_repo.get_incomplete_drills(
            inactive_for_minutes_floor=REMINDER_TRIGGER_FLOOR_MINUTES,
            inactive_for_minutes_ceil=REMINDER_TRIGGER_CEIL_MINUTES,
        )

        for drill_instance in drill_instances:
            idempotency_key = (
                f"{drill_instance.drill_instance_id}-{drill_instance.current_prompt_slug}"
            )
            if self.idempotency_checker.already_processed(idempotency_key, IDEMPOTENCY_REALM):
                continue

            # The dialog agent wont send a reminder for the same drill/prompt combo twice
            # publishing to the stream twice should be avoided, but isn't a big deal.
            self.command_publisher.publish_trigger_reminder_commands([drill_instance])
            self.idempotency_checker.record_as_processed(
                idempotency_key, IDEMPOTENCY_REALM, IDEMPOTENCY_EXPIRATION_MINUTES
            )
 def setUp(self) -> None:
     os.environ["STAGE"] = "test"
     self.idempotency_checker = IdempotencyChecker(
         region_name="us-west-2",
         endpoint_url="http://localhost:9000",
         aws_access_key_id="fake-key",
         aws_secret_access_key="fake-secret",
     )
     self.idempotency_checker.drop_and_recreate_table()
Exemple #4
0
def handler(event: dict, context: dict) -> dict:
    verify_deploy_stage()

    kinesis = get_boto3_client("kinesis")
    stage = os.environ["STAGE"]
    idempotency_checker = IdempotencyChecker()

    form = extract_form(event)
    if not is_signature_valid(event, form, stage):
        logging.warning("signature validation failed")
        return {"statusCode": 403}

    idempotency_key = event["headers"]["I-Twilio-Idempotency-Token"]
    if idempotency_checker.already_processed(idempotency_key,
                                             IDEMPOTENCY_REALM):
        logging.info(
            f"Already processed webhook with idempotency key {idempotency_key}. Skipping."
        )
        return {"statusCode": 200}
    if "MessageStatus" in form:
        logging.info(
            f"Outbound message to {form['To']}: Recording STATUS_UPDATE in message log"
        )
        kinesis.put_record(
            Data=json.dumps({
                "type": "STATUS_UPDATE",
                "received_at": datetime.now(UTC).isoformat(),
                "payload": form,
            }),
            PartitionKey=form["To"],
            StreamName=f"message-log-{stage}",
        )
    else:
        logging.info(f"Inbound message from {form['From']}: '{form['Body']}'")
        CommandPublisher().publish_process_sms_command(form["From"],
                                                       form["Body"], form)

    idempotency_checker.record_as_processed(idempotency_key, IDEMPOTENCY_REALM,
                                            IDEMPOTENCY_EXPIRATION_MINUTES)
    return {
        "statusCode": 200,
        "headers": {
            "content-type": "application/xml"
        },
        "body": str(MessagingResponse()),
    }
    def setUp(self):
        os.environ["STAGE"] = "test"
        self.drill_progress_repo = DrillProgressRepository(
            db.get_test_sqlalchemy_engine)
        self.drill_progress_repo.drop_and_recreate_tables_testing_only()
        self.phone_number = "123456789"
        self.user_id = self.drill_progress_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.drill_progress_repo.engine,
        )

        drill_db_patch = patch(
            "stopcovid.drill_progress.trigger_reminders.ReminderTriggerer._get_drill_progress_repo",
            return_value=self.drill_progress_repo,
        )
        drill_db_patch.start()

        self.addCleanup(drill_db_patch.stop)

        self.idempotency_checker = IdempotencyChecker(
            region_name="us-west-2",
            endpoint_url="http://localhost:9000",
            aws_access_key_id="fake-key",
            aws_secret_access_key="fake-secret",
        )
        self.idempotency_checker.drop_and_recreate_table()

        idempotency_patch = patch(
            "stopcovid.drill_progress.trigger_reminders.IdempotencyChecker",
            return_value=self.idempotency_checker,
        )
        idempotency_patch.start()
        self.addCleanup(idempotency_patch.stop)
class TestIdempotency(unittest.TestCase):
    def setUp(self) -> None:
        os.environ["STAGE"] = "test"
        self.idempotency_checker = IdempotencyChecker(
            region_name="us-west-2",
            endpoint_url="http://localhost:9000",
            aws_access_key_id="fake-key",
            aws_secret_access_key="fake-secret",
        )
        self.idempotency_checker.drop_and_recreate_table()

    def test_idempotency(self):
        self.assertFalse(
            self.idempotency_checker.already_processed("idempotency",
                                                       "realm1"))
        self.idempotency_checker.record_as_processed("idempotency", "realm1",
                                                     5)
        self.assertTrue(
            self.idempotency_checker.already_processed("idempotency",
                                                       "realm1"))
 def __init__(self, **kwargs):
     self.stage = os.environ.get("STAGE")
     self.drill_progress_repo = self._get_drill_progress_repo()
     self.command_publisher = CommandPublisher()
     self.idempotency_checker = IdempotencyChecker()
class TestReminderTriggers(unittest.TestCase):
    def setUp(self):
        os.environ["STAGE"] = "test"
        self.drill_progress_repo = DrillProgressRepository(
            db.get_test_sqlalchemy_engine)
        self.drill_progress_repo.drop_and_recreate_tables_testing_only()
        self.phone_number = "123456789"
        self.user_id = self.drill_progress_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.drill_progress_repo.engine,
        )

        drill_db_patch = patch(
            "stopcovid.drill_progress.trigger_reminders.ReminderTriggerer._get_drill_progress_repo",
            return_value=self.drill_progress_repo,
        )
        drill_db_patch.start()

        self.addCleanup(drill_db_patch.stop)

        self.idempotency_checker = IdempotencyChecker(
            region_name="us-west-2",
            endpoint_url="http://localhost:9000",
            aws_access_key_id="fake-key",
            aws_secret_access_key="fake-secret",
        )
        self.idempotency_checker.drop_and_recreate_table()

        idempotency_patch = patch(
            "stopcovid.drill_progress.trigger_reminders.IdempotencyChecker",
            return_value=self.idempotency_checker,
        )
        idempotency_patch.start()
        self.addCleanup(idempotency_patch.stop)

    def _get_incomplete_drill_with_last_prompt_started_min_ago(self, min_ago):
        return make_drill_instance(
            current_prompt_start_time=datetime.datetime.now(
                datetime.timezone.utc) - datetime.timedelta(minutes=min_ago),
            completion_time=None,
            user_id=self.user_id,
        )

    def test_reminder_triggerer_ignores_drills_below_inactivity_threshold(
            self, publish_mock):
        drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago(
            10)
        self.drill_progress_repo._save_drill_instance(drill_instance)
        ReminderTriggerer().trigger_reminders()
        publish_mock.assert_not_called()

    def test_reminder_triggerer_ignores_drills_above_inactivity_threshold(
            self, publish_mock):
        two_day_old_drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago(
            60 * 24 * 2)
        self.drill_progress_repo._save_drill_instance(
            two_day_old_drill_instance)
        ReminderTriggerer().trigger_reminders()
        publish_mock.assert_not_called()

    def test_reminder_triggerer_triggers_reminder_and_persists_idempotency_key(
            self, publish_mock):
        drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago(
            5 * 60)
        self.drill_progress_repo._save_drill_instance(drill_instance)
        expected_idempotency_key = (
            f"{drill_instance.drill_instance_id}-{drill_instance.current_prompt_slug}"
        )
        self.assertFalse(
            self.idempotency_checker.already_processed(
                expected_idempotency_key, IDEMPOTENCY_REALM))
        ReminderTriggerer().trigger_reminders()
        publish_mock.assert_called_once_with([drill_instance])
        self.assertTrue(
            self.idempotency_checker.already_processed(
                expected_idempotency_key, IDEMPOTENCY_REALM))

    def test_does_not_double_trigger_reminders(self, publish_mock):
        drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago(
            5 * 60)
        self.drill_progress_repo._save_drill_instance(drill_instance)
        ReminderTriggerer().trigger_reminders()
        publish_mock.assert_called_once_with([drill_instance])
        publish_mock.reset_mock()
        ReminderTriggerer().trigger_reminders()
        publish_mock.assert_not_called()