Add TwilioVerifySender
This commit is contained in:
parent
7057476048
commit
17ba630014
|
@ -3,12 +3,14 @@ twilio: # Twilio gateway configuration
|
|||
accountToken:
|
||||
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: # Twilio SID for a Verify service
|
||||
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
iosVerificationText: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNgVerificationText: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001VerificationText: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103VerificationText: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
genericVerificationText: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
androidAppHash: # Hash appended to Android
|
||||
|
||||
push:
|
||||
queueSize: # Size of push pending queue
|
||||
|
|
|
@ -26,6 +26,9 @@ public class TwilioConfiguration {
|
|||
@NotEmpty
|
||||
private String nanpaMessagingServiceSid;
|
||||
|
||||
@NotEmpty
|
||||
private String verifyServiceSid;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
@ -49,6 +52,9 @@ public class TwilioConfiguration {
|
|||
@NotEmpty
|
||||
private String genericVerificationText;
|
||||
|
||||
@NotEmpty
|
||||
private String androidAppHash;
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
@ -93,6 +99,15 @@ public class TwilioConfiguration {
|
|||
this.nanpaMessagingServiceSid = nanpaMessagingServiceSid;
|
||||
}
|
||||
|
||||
public String getVerifyServiceSid() {
|
||||
return verifyServiceSid;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setVerifyServiceSid(String verifyServiceSid) {
|
||||
this.verifyServiceSid = verifyServiceSid;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
@ -155,4 +170,12 @@ public class TwilioConfiguration {
|
|||
public void setGenericVerificationText(String genericVerificationText) {
|
||||
this.genericVerificationText = genericVerificationText;
|
||||
}
|
||||
|
||||
public String getAndroidAppHash() {
|
||||
return androidAppHash;
|
||||
}
|
||||
|
||||
public void setAndroidAppHash(String androidAppHash) {
|
||||
this.androidAppHash = androidAppHash;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
package org.whispersystems.textsecuregcm.sms;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale.LanguageRange;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
class TwilioVerifySender {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TwilioVerifySender.class);
|
||||
|
||||
static final Set<String> TWILIO_VERIFY_LANGUAGES = Set.of(
|
||||
"af",
|
||||
"ar",
|
||||
"ca",
|
||||
"zh",
|
||||
"zh-CN",
|
||||
"zh-HK",
|
||||
"hr",
|
||||
"cs",
|
||||
"da",
|
||||
"nl",
|
||||
"en",
|
||||
"en-GB",
|
||||
"fi",
|
||||
"fr",
|
||||
"de",
|
||||
"el",
|
||||
"he",
|
||||
"hi",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-BR",
|
||||
"ro",
|
||||
"ru",
|
||||
"es",
|
||||
"sv",
|
||||
"tl",
|
||||
"th",
|
||||
"tr",
|
||||
"vi");
|
||||
|
||||
private final String accountId;
|
||||
private final String accountToken;
|
||||
|
||||
private final URI verifyServiceUri;
|
||||
private final URI verifyApprovalBaseUri;
|
||||
private final String androidAppHash;
|
||||
private final FaultTolerantHttpClient httpClient;
|
||||
|
||||
TwilioVerifySender(String baseUri, FaultTolerantHttpClient httpClient, TwilioConfiguration twilioConfiguration) {
|
||||
|
||||
this.accountId = twilioConfiguration.getAccountId();
|
||||
this.accountToken = twilioConfiguration.getAccountToken();
|
||||
|
||||
this.verifyServiceUri = URI
|
||||
.create(baseUri + "/v2/Services/" + twilioConfiguration.getVerifyServiceSid() + "/Verifications");
|
||||
this.verifyApprovalBaseUri = URI
|
||||
.create(baseUri + "/v2/Services/" + twilioConfiguration.getVerifyServiceSid() + "/Verifications/");
|
||||
|
||||
this.androidAppHash = twilioConfiguration.getAndroidAppHash();
|
||||
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
CompletableFuture<Optional<String>> deliverSmsVerificationWithVerify(String destination, Optional<String> clientType,
|
||||
String verificationCode, List<LanguageRange> languageRanges) {
|
||||
|
||||
HttpRequest request = buildVerifyRequest("sms", destination, verificationCode, findBestLocale(languageRanges),
|
||||
clientType);
|
||||
|
||||
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenApply(this::parseResponse)
|
||||
.handle(this::extractVerifySid);
|
||||
}
|
||||
|
||||
private Optional<String> findBestLocale(List<LanguageRange> priorityList) {
|
||||
return Util.findBestLocale(priorityList, TwilioVerifySender.TWILIO_VERIFY_LANGUAGES);
|
||||
}
|
||||
|
||||
private TwilioVerifyResponse parseResponse(HttpResponse<String> response) {
|
||||
ObjectMapper mapper = SystemMapper.getMapper();
|
||||
|
||||
if (response.statusCode() >= 200 && response.statusCode() < 300) {
|
||||
if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) {
|
||||
return new TwilioVerifyResponse(TwilioVerifyResponse.SuccessResponse.fromBody(mapper, response.body()));
|
||||
} else {
|
||||
return new TwilioVerifyResponse(new TwilioVerifyResponse.SuccessResponse());
|
||||
}
|
||||
}
|
||||
|
||||
if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) {
|
||||
return new TwilioVerifyResponse(TwilioVerifyResponse.FailureResponse.fromBody(mapper, response.body()));
|
||||
} else {
|
||||
return new TwilioVerifyResponse(new TwilioVerifyResponse.FailureResponse());
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<Optional<String>> deliverVoxVerificationWithVerify(String destination, String verificationCode,
|
||||
List<LanguageRange> languageRanges) {
|
||||
|
||||
HttpRequest request = buildVerifyRequest("call", destination, verificationCode, findBestLocale(languageRanges),
|
||||
Optional.empty());
|
||||
|
||||
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenApply(this::parseResponse)
|
||||
.handle(this::extractVerifySid);
|
||||
}
|
||||
|
||||
private Optional<String> extractVerifySid(TwilioVerifyResponse twilioVerifyResponse, Throwable throwable) {
|
||||
|
||||
if (throwable != null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (twilioVerifyResponse.isFailure()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(twilioVerifyResponse.successResponse.getSid());
|
||||
}
|
||||
|
||||
private HttpRequest buildVerifyRequest(String channel, String destination, String verificationCode,
|
||||
Optional<String> locale, Optional<String> clientType) {
|
||||
|
||||
final Map<String, String> requestParameters = new HashMap<>();
|
||||
requestParameters.put("To", destination);
|
||||
requestParameters.put("CustomCode", verificationCode);
|
||||
requestParameters.put("Channel", channel);
|
||||
locale.ifPresent(loc -> requestParameters.put("Locale", loc));
|
||||
clientType.filter(client -> client.startsWith("android"))
|
||||
.ifPresent(ignored -> requestParameters.put("AppHash", androidAppHash));
|
||||
|
||||
return HttpRequest.newBuilder()
|
||||
.uri(verifyServiceUri)
|
||||
.POST(FormDataBodyPublisher.of(requestParameters))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Authorization", "Basic " + Base64.encodeBytes((accountId + ":" + accountToken).getBytes()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> reportVerificationSucceeded(String verificationSid) {
|
||||
|
||||
final Map<String, String> requestParameters = new HashMap<>();
|
||||
requestParameters.put("Status", "approved");
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(verifyApprovalBaseUri.resolve(verificationSid))
|
||||
.POST(FormDataBodyPublisher.of(requestParameters))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Authorization", "Basic " + Base64.encodeBytes((accountId + ":" + accountToken).getBytes()))
|
||||
.build();
|
||||
|
||||
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenApply(this::parseResponse)
|
||||
.handle((response, throwable) -> throwable == null
|
||||
&& response.isSuccess()
|
||||
&& "approved".equals(response.successResponse.getStatus()));
|
||||
}
|
||||
|
||||
public static class TwilioVerifyResponse {
|
||||
|
||||
private SuccessResponse successResponse;
|
||||
private FailureResponse failureResponse;
|
||||
|
||||
TwilioVerifyResponse(SuccessResponse successResponse) {
|
||||
this.successResponse = successResponse;
|
||||
}
|
||||
|
||||
TwilioVerifyResponse(FailureResponse failureResponse) {
|
||||
this.failureResponse = failureResponse;
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
return successResponse != null;
|
||||
}
|
||||
|
||||
boolean isFailure() {
|
||||
return failureResponse != null;
|
||||
}
|
||||
|
||||
private static class SuccessResponse {
|
||||
|
||||
@NotEmpty
|
||||
public String sid;
|
||||
|
||||
@NotEmpty
|
||||
public String status;
|
||||
|
||||
static SuccessResponse fromBody(ObjectMapper mapper, String body) {
|
||||
try {
|
||||
return mapper.readValue(body, SuccessResponse.class);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error parsing twilio success response: " + e);
|
||||
return new SuccessResponse();
|
||||
}
|
||||
}
|
||||
|
||||
public String getSid() {
|
||||
return sid;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailureResponse {
|
||||
|
||||
@JsonProperty
|
||||
private int status;
|
||||
|
||||
@JsonProperty
|
||||
private String message;
|
||||
|
||||
@JsonProperty
|
||||
private int code;
|
||||
|
||||
static FailureResponse fromBody(ObjectMapper mapper, String body) {
|
||||
try {
|
||||
return mapper.readValue(body, FailureResponse.class);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error parsing twilio response: " + e);
|
||||
return new FailureResponse();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ import java.security.SecureRandom;
|
|||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Locale.LanguageRange;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -188,4 +188,8 @@ public class Util {
|
|||
final long currentTimeSeconds = offset.addTo(clock.instant()).getLong(ChronoField.INSTANT_SECONDS);
|
||||
return TimeUnit.DAYS.toMillis(TimeUnit.SECONDS.toDays(currentTimeSeconds));
|
||||
}
|
||||
|
||||
public static Optional<String> findBestLocale(List<LanguageRange> priorityList, Collection<String> supportedLocales) {
|
||||
return Optional.ofNullable(Locale.lookupTag(priorityList, supportedLocales));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
package org.whispersystems.textsecuregcm.sms;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.github.tomakehurst.wiremock.junit.WireMockRule;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale.LanguageRange;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import junitparams.JUnitParamsRunner;
|
||||
import junitparams.Parameters;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.util.ExecutorUtils;
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@RunWith(JUnitParamsRunner.class)
|
||||
public class TwilioVerifySenderTest {
|
||||
|
||||
private static final String ACCOUNT_ID = "test_account_id";
|
||||
private static final String ACCOUNT_TOKEN = "test_account_token";
|
||||
private static final String MESSAGING_SERVICE_SID = "test_messaging_services_id";
|
||||
private static final String NANPA_MESSAGING_SERVICE_SID = "nanpa_test_messaging_service_id";
|
||||
private static final String VERIFY_SERVICE_SID = "verify_service_sid";
|
||||
private static final String LOCAL_DOMAIN = "test.com";
|
||||
private static final String ANDROID_APP_HASH = "someHash";
|
||||
|
||||
private static final String VERIFICATION_SID = "verification";
|
||||
|
||||
@Rule
|
||||
public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort().dynamicHttpsPort());
|
||||
|
||||
private TwilioVerifySender sender;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final TwilioConfiguration twilioConfiguration = createTwilioConfiguration();
|
||||
|
||||
final FaultTolerantHttpClient httpClient = FaultTolerantHttpClient.newBuilder()
|
||||
.withCircuitBreaker(twilioConfiguration.getCircuitBreaker())
|
||||
.withRetry(twilioConfiguration.getRetry())
|
||||
.withVersion(HttpClient.Version.HTTP_2)
|
||||
.withConnectTimeout(Duration.ofSeconds(10))
|
||||
.withRedirect(HttpClient.Redirect.NEVER)
|
||||
.withExecutor(ExecutorUtils.newFixedThreadBoundedQueueExecutor(10, 100))
|
||||
.withName("twilio")
|
||||
.build();
|
||||
|
||||
sender = new TwilioVerifySender("http://localhost:" + wireMockRule.port(), httpClient, twilioConfiguration);
|
||||
}
|
||||
|
||||
private TwilioConfiguration createTwilioConfiguration() {
|
||||
|
||||
TwilioConfiguration configuration = new TwilioConfiguration();
|
||||
|
||||
configuration.setAccountId(ACCOUNT_ID);
|
||||
configuration.setAccountToken(ACCOUNT_TOKEN);
|
||||
configuration.setMessagingServiceSid(MESSAGING_SERVICE_SID);
|
||||
configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID);
|
||||
configuration.setVerifyServiceSid(VERIFY_SERVICE_SID);
|
||||
configuration.setLocalDomain(LOCAL_DOMAIN);
|
||||
configuration.setAndroidAppHash(ANDROID_APP_HASH);
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private void setupSuccessStubForVerify() {
|
||||
wireMockRule.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
||||
.willReturn(aResponse()
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"sid\": \"" + VERIFICATION_SID + "\", \"status\": \"pending\"}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForDeliverSmsVerificationWithVerify")
|
||||
public void deliverSmsVerificationWithVerify(@Nullable final String client, @Nullable final String languageRange,
|
||||
final boolean expectAppHash, @Nullable final String expectedLocale) throws Exception {
|
||||
|
||||
setupSuccessStubForVerify();
|
||||
|
||||
List<LanguageRange> languageRanges = Optional.ofNullable(languageRange)
|
||||
.map(LanguageRange::parse)
|
||||
.orElse(Collections.emptyList());
|
||||
|
||||
final Optional<String> verificationSid = sender
|
||||
.deliverSmsVerificationWithVerify("+14153333333", Optional.ofNullable(client), "123456",
|
||||
languageRanges).get();
|
||||
|
||||
assertEquals(VERIFICATION_SID, verificationSid.get());
|
||||
|
||||
verify(1, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||
.withRequestBody(equalTo(
|
||||
(expectedLocale == null ? "" : "Locale=" + expectedLocale + "&")
|
||||
+ "Channel=sms&To=%2B14153333333&CustomCode=123456"
|
||||
+ (expectAppHash ? "&AppHash=" + ANDROID_APP_HASH : "")
|
||||
)));
|
||||
}
|
||||
|
||||
private static Object argumentsForDeliverSmsVerificationWithVerify() {
|
||||
return new Object[][]{
|
||||
// client, languageRange, expectAppHash, expectedLocale
|
||||
{"ios", "fr-CA, en", false, "fr"},
|
||||
{"android-2021-03", "zh-HK, it", true, "zh-HK"},
|
||||
{null, null, false, null}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForDeliverVoxVerificationWithVerify")
|
||||
public void deliverVoxVerificationWithVerify(@Nullable final String languageRange,
|
||||
@Nullable final String expectedLocale) throws Exception {
|
||||
|
||||
setupSuccessStubForVerify();
|
||||
|
||||
final List<LanguageRange> languageRanges = Optional.ofNullable(languageRange)
|
||||
.map(LanguageRange::parse)
|
||||
.orElse(Collections.emptyList());
|
||||
|
||||
final Optional<String> verificationSid = sender
|
||||
.deliverVoxVerificationWithVerify("+14153333333", "123456", languageRanges).get();
|
||||
|
||||
assertEquals(VERIFICATION_SID, verificationSid.get());
|
||||
|
||||
verify(1, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||
.withRequestBody(equalTo(
|
||||
(expectedLocale == null ? "" : "Locale=" + expectedLocale + "&")
|
||||
+ "Channel=call&To=%2B14153333333&CustomCode=123456")));
|
||||
}
|
||||
|
||||
private static Object argumentsForDeliverVoxVerificationWithVerify() {
|
||||
return new Object[][]{
|
||||
// languageRange, expectedLocale
|
||||
{"fr-CA, en", "fr"},
|
||||
{"zh-HK, it", "zh-HK"},
|
||||
{"en-CAA, en", "en"},
|
||||
{null, null}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmsFiveHundred() throws Exception {
|
||||
wireMockRule.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
||||
.willReturn(aResponse()
|
||||
.withStatus(500)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"message\": \"Server error!\"}")));
|
||||
|
||||
final Optional<String> verificationSid = sender
|
||||
.deliverSmsVerificationWithVerify("+14153333333", Optional.empty(), "123456", Collections.emptyList()).get();
|
||||
|
||||
assertThat(verificationSid).isEmpty();
|
||||
|
||||
verify(3, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||
.withRequestBody(equalTo("Channel=sms&To=%2B14153333333&CustomCode=123456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVoxFiveHundred() throws Exception {
|
||||
wireMockRule.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
||||
.willReturn(aResponse()
|
||||
.withStatus(500)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"message\": \"Server error!\"}")));
|
||||
|
||||
final Optional<String> verificationSid = sender
|
||||
.deliverVoxVerificationWithVerify("+14153333333", "123456", Collections.emptyList()).get();
|
||||
|
||||
assertThat(verificationSid).isEmpty();
|
||||
|
||||
verify(3, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications"))
|
||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||
.withRequestBody(equalTo("Channel=call&To=%2B14153333333&CustomCode=123456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportVerificationSucceeded() throws Exception {
|
||||
|
||||
wireMockRule.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID))
|
||||
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("{\"status\": \"approved\", \"sid\": \"" + VERIFICATION_SID + "\"}")));
|
||||
|
||||
final Boolean success = sender.reportVerificationSucceeded(VERIFICATION_SID).get();
|
||||
|
||||
assertThat(success).isTrue();
|
||||
|
||||
verify(1, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID))
|
||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||
.withRequestBody(equalTo("Status=approved")));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package org.whispersystems.textsecuregcm.tests.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale.LanguageRange;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
class LocaleTest {
|
||||
|
||||
private static final Set<String> SUPPORTED_LOCALES = Set.of("es", "en", "zh", "zh-HK");
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testFindBestLocale(@Nullable final String languageRange, @Nullable final String expectedLocale) {
|
||||
|
||||
final List<LanguageRange> languageRanges = Optional.ofNullable(languageRange)
|
||||
.map(LanguageRange::parse)
|
||||
.orElse(Collections.emptyList());
|
||||
|
||||
assertEquals(Optional.ofNullable(expectedLocale), Util.findBestLocale(languageRanges, SUPPORTED_LOCALES));
|
||||
}
|
||||
|
||||
static Stream<Arguments> testFindBestLocale() {
|
||||
return Stream.of(
|
||||
// languageRange, expectedLocale
|
||||
Arguments.of("en-US, fr", "en"),
|
||||
Arguments.of("es-ES", "es"),
|
||||
Arguments.of("zh-Hant-HK, zh-HK", "zh"),
|
||||
// zh-HK is supported, but Locale#lookup truncates from the end, per RFC-4647
|
||||
Arguments.of("zh-Hant-HK", "zh"),
|
||||
Arguments.of("zh-HK", "zh-HK"),
|
||||
Arguments.of("de", null),
|
||||
Arguments.of(null, null)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue