Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMS phone verification using user's locale #191

Merged
merged 4 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public VerificationResult sendVerificationText(Request req, Response res) {
);
}

Verification verification = NotificationUtils.sendVerificationText(phoneNumber);
Verification verification = NotificationUtils.sendVerificationText(phoneNumber, otpUser.preferredLocale);
if (verification == null) {
logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown error sending verification text");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,39 @@ public static String sendSMS(String toPhone, String body) {
}
}

/**
* Get a supported Twilio locale for a given locale in IETF's BPC 47 format.
* See https://www.twilio.com/docs/verify/supported-languages#verify-default-template
*/
public static String getTwilioLocale(String locale) {
if (locale == null) {
return "en";
}
// The Twilio's supported locales are just the first two letters of the user's locale,
// unless it is zh-HK, pt-BR, or en-GB.
switch (locale) {
case "en-GB":
case "pt-BR":
case "zh-HK":
return locale;
default:
return locale.length() < 2 ? "en" : locale.substring(0, 2);
}
}

/**
* Send verification text to phone number (i.e., a code that the recipient will use to verify ownership of the
* number via the OTP web app).
*/
public static Verification sendVerificationText(String phoneNumber) {
public static Verification sendVerificationText(String phoneNumber, String locale) {
if (TWILIO_ACCOUNT_SID == null || TWILIO_AUTH_TOKEN == null) {
LOG.error("SMS notifications not configured correctly.");
return null;
}
try {
Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
VerificationCreator smsVerifier = Verification.creator(TWILIO_VERIFICATION_SERVICE_SID, phoneNumber, "sms");
smsVerifier.setLocale(getTwilioLocale(locale));
Verification verification = smsVerifier.create();
LOG.info("SMS verification ({}) sent successfully", verification.getSid());
return verification;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,120 +1,33 @@
package org.opentripplanner.middleware.utils;

import com.twilio.rest.verify.v2.service.Verification;
import com.twilio.rest.verify.v2.service.VerificationCheck;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.IOException;

import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.opentripplanner.middleware.testutils.PersistenceTestUtils.createUser;
import static org.opentripplanner.middleware.utils.ConfigUtils.isRunningCi;
import static org.opentripplanner.middleware.utils.NotificationUtils.OTP_ADMIN_DASHBOARD_FROM_EMAIL;
import static org.opentripplanner.middleware.utils.NotificationUtils.getTwilioLocale;

/**
* Contains tests for the various notification utilities to send SMS, email messages, and push notifications.
* Note: these tests require the environment variables RUN_E2E=true and valid values for TEST_TO_EMAIL, TEST_TO_PHONE,
* and TEST_TO_PUSH. Furthermore, TEST_TO_PHONE must be a verified phone number in a valid Twilio account.
* Contains tests for the various notification utilities.
* Tests in this file do not require specific environment variables
* and do not contain end-to-end notification actions.
*/
public class NotificationUtilsTest extends OtpMiddlewareTestEnvironment {
private static final Logger LOG = LoggerFactory.getLogger(NotificationUtilsTest.class);
private static OtpUser user;

/**
* Note: In order to run the notification tests, these values must be provided in in system
* environment variables, which can be defined in a run configuration in your IDE.
*/
private static final String email = System.getenv("TEST_TO_EMAIL");
/** Phone must be in the form "+15551234" and must be verified first in order to send notifications */
private static final String phone = System.getenv("TEST_TO_PHONE");
/** Push notification is conventionally a user.email value and must be known to the mobile team's push API */
private static final String push = System.getenv("TEST_TO_PUSH");
/**
* Currently, since these tests require target email/SMS values, these tests should not run on CI.
*/
private static final boolean shouldTestsRun =
!isRunningCi && IS_END_TO_END && email != null && phone != null && push != null;

@BeforeAll
public static void setup() throws IOException {
assumeTrue(shouldTestsRun);
user = createUser(email, phone);
}

@AfterAll
public static void tearDown() {
if (user != null) Persistence.otpUsers.removeById(user.id);
}

class NotificationUtilsTest {
@Test
public void canSendPushNotification() {
String ret = NotificationUtils.sendPush(
// Conventionally user.email
push,
"Tough little ship!"
);
LOG.info("Push notification (ret={}) sent to {}", ret, push);
Assertions.assertNotNull(ret);
void canGetTwilioLocale() {
Assertions.assertEquals("en-GB", getTwilioLocale("en-GB"));
Assertions.assertEquals("fr", getTwilioLocale("fr-FR"));
Assertions.assertEquals("zh", getTwilioLocale("zh"));
Assertions.assertEquals("zh-HK", getTwilioLocale("zh-HK"));
Assertions.assertEquals("pt", getTwilioLocale("pt"));
Assertions.assertEquals("pt-BR", getTwilioLocale("pt-BR"));
}

@Test
public void canSendSparkpostEmailNotification() {
boolean success = NotificationUtils.sendEmailViaSparkpost(
OTP_ADMIN_DASHBOARD_FROM_EMAIL,
user.email,
"Hi there",
"This is the body",
null
);
Assertions.assertTrue(success);
}

@Test
public void canSendSmsNotification() {
// Note: toPhone must be verified.
String messageId = NotificationUtils.sendSMS(
// Note: phone number is configured in setup method above.
user.phoneNumber,
"This is the ship that made the Kessel Run in fourteen parsecs?"
);
LOG.info("Notification (id={}) successfully sent to {}", messageId, user.phoneNumber);
Assertions.assertNotNull(messageId);
}

/**
* Tests whether a verification code can be sent to a phone number.
*/
@Test
public void canSendTwilioVerificationText() {
Verification verification = NotificationUtils.sendVerificationText(
// Note: phone number is configured in setup method above.
user.phoneNumber
);
LOG.info("Verification status: {}", verification.getStatus());
Assertions.assertNotNull(verification.getSid());
}

/**
* Tests whether a verification code can be checked with the Twilio service. Note: if running locally, the {@link
* #canSendTwilioVerificationText()} test can be run first (with your own mobile phone number) and the code sent to
* your phone can be used below (in place of 123456) to generate an "approved" status.
*/
@Test
public void canCheckSmsVerificationCode() {
VerificationCheck check = NotificationUtils.checkSmsVerificationCode(
// Note: phone number is configured in setup method above.
user.phoneNumber,
"123456"
);
LOG.info("Verification status: {}", check.getStatus());
Assertions.assertNotNull(check.getSid());
@ParameterizedTest
@ValueSource(strings = {"", "e", "en", "en-US"})
@NullSource
void twilioLocaleDefaultsToEnglish(String locale) {
Assertions.assertEquals("en", getTwilioLocale(locale));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.opentripplanner.middleware.utils;

import com.twilio.rest.verify.v2.service.Verification;
import com.twilio.rest.verify.v2.service.VerificationCheck;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.opentripplanner.middleware.testutils.PersistenceTestUtils.createUser;
import static org.opentripplanner.middleware.utils.ConfigUtils.isRunningCi;
import static org.opentripplanner.middleware.utils.NotificationUtils.OTP_ADMIN_DASHBOARD_FROM_EMAIL;

/**
* Contains tests for the various notification utilities to send SMS, email messages, and push notifications.
* Note: these tests require the environment variables RUN_E2E=true and valid values for TEST_TO_EMAIL, TEST_TO_PHONE,
* and TEST_TO_PUSH. Furthermore, TEST_TO_PHONE must be a verified phone number in a valid Twilio account.
*/
public class NotificationUtilsTestCI extends OtpMiddlewareTestEnvironment {
private static final Logger LOG = LoggerFactory.getLogger(NotificationUtilsTestCI.class);
private static OtpUser user;

/**
* Note: In order to run the notification tests, these values must be provided in in system
* environment variables, which can be defined in a run configuration in your IDE.
*/
private static final String email = System.getenv("TEST_TO_EMAIL");
/** Phone must be in the form "+15551234" and must be verified first in order to send notifications */
private static final String phone = System.getenv("TEST_TO_PHONE");
/** Push notification is conventionally a user.email value and must be known to the mobile team's push API */
private static final String push = System.getenv("TEST_TO_PUSH");
/**
* Currently, since these tests require target email/SMS values, these tests should not run on CI.
*/
private static final boolean shouldTestsRun =
!isRunningCi && IS_END_TO_END && email != null && phone != null && push != null;

@BeforeAll
public static void setup() throws IOException {
assumeTrue(shouldTestsRun);
user = createUser(email, phone);
}

@AfterAll
public static void tearDown() {
if (user != null) Persistence.otpUsers.removeById(user.id);
}

@Test
public void canSendPushNotification() {
String ret = NotificationUtils.sendPush(
// Conventionally user.email
push,
"Tough little ship!"
);
LOG.info("Push notification (ret={}) sent to {}", ret, push);
Assertions.assertNotNull(ret);
}

@Test
public void canSendSparkpostEmailNotification() {
boolean success = NotificationUtils.sendEmailViaSparkpost(
OTP_ADMIN_DASHBOARD_FROM_EMAIL,
user.email,
"Hi there",
"This is the body",
null
);
Assertions.assertTrue(success);
}

@Test
public void canSendSmsNotification() {
// Note: toPhone must be verified.
String messageId = NotificationUtils.sendSMS(
// Note: phone number is configured in setup method above.
user.phoneNumber,
"This is the ship that made the Kessel Run in fourteen parsecs?"
);
LOG.info("Notification (id={}) successfully sent to {}", messageId, user.phoneNumber);
Assertions.assertNotNull(messageId);
}

/**
* Tests whether a verification code can be sent to a phone number.
*/
@Test
public void canSendTwilioVerificationText() {
Verification verification = NotificationUtils.sendVerificationText(
// Note: phone number is configured in setup method above.
user.phoneNumber,
"en"
);
LOG.info("Verification status: {}", verification.getStatus());
Assertions.assertNotNull(verification.getSid());
}

/**
* Tests whether a verification code can be checked with the Twilio service. Note: if running locally, the {@link
* #canSendTwilioVerificationText()} test can be run first (with your own mobile phone number) and the code sent to
* your phone can be used below (in place of 123456) to generate an "approved" status.
*/
@Test
public void canCheckSmsVerificationCode() {
VerificationCheck check = NotificationUtils.checkSmsVerificationCode(
// Note: phone number is configured in setup method above.
user.phoneNumber,
"123456"
);
LOG.info("Verification status: {}", check.getStatus());
Assertions.assertNotNull(check.getSid());
}
}
Loading