Discard old Twilio machinery and rely entirely on the stand-alone registration service

This commit is contained in:
Jon Chambers 2022-10-07 17:11:37 -04:00 committed by Jon Chambers
parent 78f95e4859
commit 74d65b37a8
19 changed files with 159 additions and 2466 deletions

View File

@ -62,29 +62,6 @@ dynamoDbTables:
subscriptions: subscriptions:
tableName: Example_Subscriptions tableName: Example_Subscriptions
twilio: # Twilio gateway configuration
accountId: unset
accountToken: unset
nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA.
messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA.
verifyServiceSid: unset # Twilio SID for a Verify service
localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service.
defaultClientVerificationTexts:
ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
androidNg: example %1$s # 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.
android202001: example %1$s # 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.
android202103: example %1$s # 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.
generic: example %1$s # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
regionalClientVerificationTexts: # Map of country codes to custom texts
999: # example country code
ios: example %1$s # all keys from defaultClientVerificationTexts are required
androidNg: example %1$s
android202001: example %1$s
android202103: example %1$s
generic: example %1$s
androidAppHash: example # Hash appended to Android
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
cacheCluster: # Redis server configuration for cache cluster cacheCluster: # Redis server configuration for cache cluster
configurationUri: redis://redis.example.com:6379/ configurationUri: redis://redis.example.com:6379/

View File

@ -45,7 +45,6 @@ import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfig
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration; import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration; import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration; import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration; import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration; import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
@ -75,11 +74,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private DynamoDbTables dynamoDbTables; private DynamoDbTables dynamoDbTables;
@NotNull
@Valid
@JsonProperty
private TwilioConfiguration twilio;
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
@ -297,10 +291,6 @@ public class WhisperServerConfiguration extends Configuration {
return webSocket; return webSocket;
} }
public TwilioConfiguration getTwilioConfiguration() {
return twilio;
}
public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() { public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() {
return awsAttachments; return awsAttachments;
} }

View File

@ -163,9 +163,6 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient; import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.AccountCleaner; import org.whispersystems.textsecuregcm.storage.AccountCleaner;
@ -440,9 +437,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager); ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator( ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true); config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator( ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
@ -499,8 +493,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager); AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager); DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
SmsSender smsSender = new SmsSender(twilioSmsSender);
MessageSender messageSender = new MessageSender(clientPresenceManager, messagesManager, pushNotificationManager, pushLatencyManager); MessageSender messageSender = new MessageSender(clientPresenceManager, messagesManager, pushNotificationManager, pushLatencyManager);
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor); ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager); TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
@ -646,9 +638,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
// these should be common, but use @Auth DisabledPermittedAccount, which isnt supported yet on websocket // these should be common, but use @Auth DisabledPermittedAccount, which isnt supported yet on websocket
environment.jersey().register( environment.jersey().register(
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
smsSender, registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
recaptchaClient, pushNotificationManager, verifyExperimentEnrollmentManager, recaptchaClient, pushNotificationManager, changeNumberManager, backupCredentialsGenerator));
changeNumberManager, backupCredentialsGenerator, experimentEnrollmentManager));
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager)); environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
final List<Object> commonControllers = Lists.newArrayList( final List<Object> commonControllers = Lists.newArrayList(

View File

@ -1,159 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.google.common.annotations.VisibleForTesting;
import java.util.Collections;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class TwilioConfiguration {
@NotEmpty
private String accountId;
@NotEmpty
private String accountToken;
@NotEmpty
private String localDomain;
@NotEmpty
private String messagingServiceSid;
@NotEmpty
private String nanpaMessagingServiceSid;
@NotEmpty
private String verifyServiceSid;
@NotNull
@Valid
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@NotNull
@Valid
private RetryConfiguration retry = new RetryConfiguration();
@Valid
private TwilioVerificationTextConfiguration defaultClientVerificationTexts;
@Valid
private Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts = Collections.emptyMap();
@NotEmpty
private String androidAppHash;
@NotEmpty
private String verifyServiceFriendlyName;
public String getAccountId() {
return accountId;
}
@VisibleForTesting
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public String getAccountToken() {
return accountToken;
}
@VisibleForTesting
public void setAccountToken(String accountToken) {
this.accountToken = accountToken;
}
public String getLocalDomain() {
return localDomain;
}
@VisibleForTesting
public void setLocalDomain(String localDomain) {
this.localDomain = localDomain;
}
public String getMessagingServiceSid() {
return messagingServiceSid;
}
@VisibleForTesting
public void setMessagingServiceSid(String messagingServiceSid) {
this.messagingServiceSid = messagingServiceSid;
}
public String getNanpaMessagingServiceSid() {
return nanpaMessagingServiceSid;
}
@VisibleForTesting
public void setNanpaMessagingServiceSid(String nanpaMessagingServiceSid) {
this.nanpaMessagingServiceSid = nanpaMessagingServiceSid;
}
public String getVerifyServiceSid() {
return verifyServiceSid;
}
@VisibleForTesting
public void setVerifyServiceSid(String verifyServiceSid) {
this.verifyServiceSid = verifyServiceSid;
}
public CircuitBreakerConfiguration getCircuitBreaker() {
return circuitBreaker;
}
@VisibleForTesting
public void setCircuitBreaker(CircuitBreakerConfiguration circuitBreaker) {
this.circuitBreaker = circuitBreaker;
}
public RetryConfiguration getRetry() {
return retry;
}
@VisibleForTesting
public void setRetry(RetryConfiguration retry) {
this.retry = retry;
}
public TwilioVerificationTextConfiguration getDefaultClientVerificationTexts() {
return defaultClientVerificationTexts;
}
@VisibleForTesting
public void setDefaultClientVerificationTexts(TwilioVerificationTextConfiguration defaultClientVerificationTexts) {
this.defaultClientVerificationTexts = defaultClientVerificationTexts;
}
public Map<String,TwilioVerificationTextConfiguration> getRegionalClientVerificationTexts() {
return regionalClientVerificationTexts;
}
@VisibleForTesting
public void setRegionalClientVerificationTexts(final Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts) {
this.regionalClientVerificationTexts = regionalClientVerificationTexts;
}
public String getAndroidAppHash() {
return androidAppHash;
}
public void setAndroidAppHash(String androidAppHash) {
this.androidAppHash = androidAppHash;
}
public void setVerifyServiceFriendlyName(String serviceFriendlyName) {
this.verifyServiceFriendlyName = serviceFriendlyName;
}
public String getVerifyServiceFriendlyName() {
return verifyServiceFriendlyName;
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.NotEmpty;
public class TwilioCountrySenderIdConfiguration {
@NotEmpty
private String countryCode;
@NotEmpty
private String senderId;
public String getCountryCode() {
return countryCode;
}
@VisibleForTesting
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getSenderId() {
return senderId;
}
@VisibleForTesting
public void setSenderId(String senderId) {
this.senderId = senderId;
}
}

View File

@ -1,67 +0,0 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
public class TwilioVerificationTextConfiguration {
@JsonProperty
@NotEmpty
private String ios;
@JsonProperty
@NotEmpty
private String androidNg;
@JsonProperty
@NotEmpty
private String android202001;
@JsonProperty
@NotEmpty
private String android202103;
@JsonProperty
@NotEmpty
private String generic;
public String getIosText() {
return ios;
}
public void setIosText(String ios) {
this.ios = ios;
}
public String getAndroidNgText() {
return androidNg;
}
public void setAndroidNgText(final String androidNg) {
this.androidNg = androidNg;
}
public String getAndroid202001Text() {
return android202001;
}
public void setAndroid202001Text(final String android202001) {
this.android202001 = android202001;
}
public String getAndroid202103Text() {
return android202103;
}
public void setAndroid202103Text(final String android202103) {
this.android202103 = android202103;
}
public String getGenericText() {
return generic;
}
public void setGenericText(final String generic) {
this.generic = generic;
}
}

View File

@ -29,10 +29,6 @@ public class DynamicConfiguration {
@Valid @Valid
private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration(); private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration();
@JsonProperty
@Valid
private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration();
@JsonProperty @JsonProperty
@Valid @Valid
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration(); private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
@ -86,15 +82,6 @@ public class DynamicConfiguration {
return payments; return payments;
} }
public DynamicTwilioConfiguration getTwilioConfiguration() {
return twilio;
}
@VisibleForTesting
public void setTwilioConfiguration(DynamicTwilioConfiguration twilioConfiguration) {
this.twilio = twilioConfiguration;
}
public DynamicCaptchaConfiguration getCaptchaConfiguration() { public DynamicCaptchaConfiguration getCaptchaConfiguration() {
return captcha; return captcha;
} }

View File

@ -1,23 +0,0 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
public class DynamicTwilioConfiguration {
@JsonProperty
@NotNull
private List<String> numbers = Collections.emptyList();
public List<String> getNumbers() {
return numbers;
}
@VisibleForTesting
public void setNumbers(List<String> numbers) {
this.numbers = numbers;
}
}

View File

@ -21,13 +21,9 @@ import io.micrometer.core.instrument.Tags;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
@ -83,7 +79,6 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices; import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.entities.UsernameRequest; import org.whispersystems.textsecuregcm.entities.UsernameRequest;
import org.whispersystems.textsecuregcm.entities.UsernameResponse; import org.whispersystems.textsecuregcm.entities.UsernameResponse;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
@ -93,8 +88,6 @@ import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.ClientType;
import org.whispersystems.textsecuregcm.registration.MessageTransport; import org.whispersystems.textsecuregcm.registration.MessageTransport;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
@ -112,7 +105,6 @@ import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
import org.whispersystems.textsecuregcm.util.Optionals; import org.whispersystems.textsecuregcm.util.Optionals;
import org.whispersystems.textsecuregcm.util.UsernameGenerator; import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/accounts") @Path("/v1/accounts")
@ -133,10 +125,6 @@ public class AccountController {
private static final String CAPTCHA_ATTEMPT_COUNTER_NAME = name(AccountController.class, "captcha"); private static final String CAPTCHA_ATTEMPT_COUNTER_NAME = name(AccountController.class, "captcha");
private static final String CHALLENGE_ISSUED_COUNTER_NAME = name(AccountController.class, "challengeIssued"); private static final String CHALLENGE_ISSUED_COUNTER_NAME = name(AccountController.class, "challengeIssued");
private static final String TWILIO_VERIFY_ERROR_COUNTER_NAME = name(AccountController.class, "twilioVerifyError");
private static final String TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME = name(AccountController.class, "twilioUndelivered");
private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(AccountController.class, "invalidAcceptLanguage");
private static final String NONSTANDARD_USERNAME_COUNTER_NAME = name(AccountController.class, "nonStandardUsername"); private static final String NONSTANDARD_USERNAME_COUNTER_NAME = name(AccountController.class, "nonStandardUsername");
private static final String CHALLENGE_PRESENT_TAG_NAME = "present"; private static final String CHALLENGE_PRESENT_TAG_NAME = "present";
@ -157,7 +145,6 @@ public class AccountController {
private final AccountsManager accounts; private final AccountsManager accounts;
private final AbusiveHostRules abusiveHostRules; private final AbusiveHostRules abusiveHostRules;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final RegistrationServiceClient registrationServiceClient; private final RegistrationServiceClient registrationServiceClient;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager; private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator; private final TurnTokenGenerator turnTokenGenerator;
@ -166,13 +153,8 @@ public class AccountController {
private final PushNotificationManager pushNotificationManager; private final PushNotificationManager pushNotificationManager;
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator; private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private final ChangeNumberManager changeNumberManager; private final ChangeNumberManager changeNumberManager;
@VisibleForTesting
static final String REGISTRATION_SERVICE_EXPERIMENT_NAME = "registration-service";
@VisibleForTesting @VisibleForTesting
static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
@ -180,33 +162,27 @@ public class AccountController {
AccountsManager accounts, AccountsManager accounts,
AbusiveHostRules abusiveHostRules, AbusiveHostRules abusiveHostRules,
RateLimiters rateLimiters, RateLimiters rateLimiters,
SmsSender smsSenderFactory,
RegistrationServiceClient registrationServiceClient, RegistrationServiceClient registrationServiceClient,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager, DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator, TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices, Map<String, Integer> testDevices,
RecaptchaClient recaptchaClient, RecaptchaClient recaptchaClient,
PushNotificationManager pushNotificationManager, PushNotificationManager pushNotificationManager,
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
ChangeNumberManager changeNumberManager, ChangeNumberManager changeNumberManager,
ExternalServiceCredentialGenerator backupServiceCredentialGenerator, ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
final ExperimentEnrollmentManager experimentEnrollmentManager)
{ {
this.pendingAccounts = pendingAccounts; this.pendingAccounts = pendingAccounts;
this.accounts = accounts; this.accounts = accounts;
this.abusiveHostRules = abusiveHostRules; this.abusiveHostRules = abusiveHostRules;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.registrationServiceClient = registrationServiceClient; this.registrationServiceClient = registrationServiceClient;
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
this.testDevices = testDevices; this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator; this.turnTokenGenerator = turnTokenGenerator;
this.recaptchaClient = recaptchaClient; this.recaptchaClient = recaptchaClient;
this.pushNotificationManager = pushNotificationManager; this.pushNotificationManager = pushNotificationManager;
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator; this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
this.changeNumberManager = changeNumberManager; this.changeNumberManager = changeNumberManager;
this.experimentEnrollmentManager = experimentEnrollmentManager;
} }
@Timed @Timed
@ -304,127 +280,6 @@ public class AccountController {
default -> throw new WebApplicationException(Response.status(422).build()); default -> throw new WebApplicationException(Response.status(422).build());
} }
if (experimentEnrollmentManager.isEnrolled(number, REGISTRATION_SERVICE_EXPERIMENT_NAME)) {
sendVerificationCodeViaRegistrationService(number,
maybeStoredVerificationCode,
acceptLanguage,
client,
transport);
} else {
sendVerificationCodeViaTwilioSender(number,
maybeStoredVerificationCode,
acceptLanguage,
userAgent,
client,
transport,
assessmentResult);
}
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport)))
.increment();
return Response.ok().build();
}
private void sendVerificationCodeViaTwilioSender(final String number,
final Optional<StoredVerificationCode> maybeStoredVerificationCode,
final Optional<String> acceptLanguage,
final String userAgent,
final Optional<String> client,
final String transport,
final Optional<RecaptchaClient.AssessmentResult> assessmentResult) {
final VerificationCode verificationCode = generateVerificationCode(number);
final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(),
maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null),
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid).orElse(null),
maybeStoredVerificationCode.map(StoredVerificationCode::sessionId).orElse(null));
pendingAccounts.store(number, storedVerificationCode);
List<Locale.LanguageRange> languageRanges;
try {
languageRanges = acceptLanguage.map(Locale.LanguageRange::parse).orElse(Collections.emptyList());
} catch (final IllegalArgumentException e) {
logger.debug("Could not get acceptable languages; Accept-Language: {}; User-Agent: {}",
acceptLanguage.orElse(""),
userAgent,
e);
Metrics.counter(INVALID_ACCEPT_LANGUAGE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
languageRanges = Collections.emptyList();
}
final boolean enrolledInVerifyExperiment = verifyExperimentEnrollmentManager.isEnrolled(client, number, languageRanges, transport);
final CompletableFuture<Optional<String>> sendVerificationWithTwilioVerifyFuture;
if (testDevices.containsKey(number)) {
// noop
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
} else if (transport.equals("sms")) {
if (enrolledInVerifyExperiment) {
sendVerificationWithTwilioVerifyFuture = smsSender.deliverSmsVerificationWithTwilioVerify(number, client, verificationCode.getVerificationCode(), languageRanges);
} else {
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
} else if (transport.equals("voice")) {
if (enrolledInVerifyExperiment) {
sendVerificationWithTwilioVerifyFuture = smsSender.deliverVoxVerificationWithTwilioVerify(number, verificationCode.getVerificationCode(), languageRanges);
} else {
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), languageRanges);
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
} else {
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
}
sendVerificationWithTwilioVerifyFuture.whenComplete((maybeVerificationSid, throwable) -> {
if (throwable != null) {
Metrics.counter(TWILIO_VERIFY_ERROR_COUNTER_NAME).increment();
logger.warn("Error with Twilio Verify", throwable);
return;
}
if (enrolledInVerifyExperiment && maybeVerificationSid.isEmpty() && assessmentResult.isPresent()) {
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
Metrics.counter(TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME, Tags.of(
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
Tag.of(REGION_TAG_NAME, region),
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(SCORE_TAG_NAME, assessmentResult.get().score())))
.increment();
}
maybeVerificationSid.ifPresent(twilioVerificationSid -> {
StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode(
storedVerificationCode.code(),
storedVerificationCode.timestamp(),
storedVerificationCode.pushCode(),
twilioVerificationSid,
storedVerificationCode.sessionId());
pendingAccounts.store(number, storedVerificationCodeWithVerificationSid);
});
});
}
private void sendVerificationCodeViaRegistrationService(final String number,
final Optional<StoredVerificationCode> maybeStoredVerificationCode,
final Optional<String> acceptLanguage,
final Optional<String> client,
final String transport) {
final Phonenumber.PhoneNumber phoneNumber; final Phonenumber.PhoneNumber phoneNumber;
try { try {
@ -461,6 +316,15 @@ public class AccountController {
sessionId); sessionId);
pendingAccounts.store(number, storedVerificationCode); pendingAccounts.store(number, storedVerificationCode);
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport)))
.increment();
return Response.ok().build();
} }
@Timed @Timed
@ -497,10 +361,6 @@ public class AccountController {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
.ifPresent(
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "registration"));
Optional<Account> existingAccount = accounts.getByE164(number); Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) { if (existingAccount.isPresent()) {
@ -552,23 +412,15 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number); rateLimiters.getVerifyLimiter().validate(number);
final Optional<StoredVerificationCode> maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); final boolean codeVerified = pendingAccounts.getCodeForNumber(number).map(storedVerificationCode ->
registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(),
final boolean codeVerified = maybeStoredVerificationCode.map(storedVerificationCode -> request.code(), REGISTRATION_RPC_TIMEOUT).join())
storedVerificationCode.sessionId() != null ?
registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(),
request.code(), REGISTRATION_RPC_TIMEOUT).join() :
storedVerificationCode.isValid(request.code()))
.orElse(false); .orElse(false);
if (!codeVerified) { if (!codeVerified) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
.ifPresent(
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "changeNumber"));
final Optional<Account> existingAccount = accounts.getByE164(number); final Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) { if (existingAccount.isPresent()) {
@ -1039,17 +891,6 @@ public class AccountController {
return false; return false;
} }
@VisibleForTesting protected
VerificationCode generateVerificationCode(String number) {
if (testDevices.containsKey(number)) {
return new VerificationCode(testDevices.get(number));
}
SecureRandom random = new SecureRandom();
int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt);
}
private String generatePushChallenge() { private String generatePushChallenge() {
SecureRandom random = new SecureRandom(); SecureRandom random = new SecureRandom();
byte[] challenge = new byte[16]; byte[] challenge = new byte[16];

View File

@ -1,57 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.sms;
import java.util.List;
import java.util.Locale.LanguageRange;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class SmsSender {
private final TwilioSmsSender twilioSender;
public SmsSender(TwilioSmsSender twilioSender) {
this.twilioSender = twilioSender;
}
public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode) {
// Fix up mexico numbers to 'mobile' format just for SMS delivery.
if (destination.startsWith("+52") && !destination.startsWith("+521")) {
destination = "+521" + destination.substring("+52".length());
}
twilioSender.deliverSmsVerification(destination, clientType, verificationCode);
}
public void deliverVoxVerification(String destination, String verificationCode, List<LanguageRange> languageRanges) {
twilioSender.deliverVoxVerification(destination, verificationCode, languageRanges);
}
public CompletableFuture<Optional<String>> deliverSmsVerificationWithTwilioVerify(String destination,
Optional<String> clientType,
String verificationCode, List<LanguageRange> languageRanges) {
// Fix up mexico numbers to 'mobile' format just for SMS delivery.
if (destination.startsWith("+52") && !destination.startsWith("+521")) {
destination = "+521" + destination.substring(3);
}
return twilioSender.deliverSmsVerificationWithVerify(destination, clientType, verificationCode, languageRanges);
}
public CompletableFuture<Optional<String>> deliverVoxVerificationWithTwilioVerify(String destination,
String verificationCode,
List<LanguageRange> languageRanges) {
return twilioSender.deliverVoxVerificationWithVerify(destination, verificationCode, languageRanges);
}
public void reportVerificationSucceeded(String verificationSid, @Nullable String userAgent, String context) {
twilioSender.reportVerificationSucceeded(verificationSid, userAgent, context);
}
}

View File

@ -1,345 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.sms;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.ExecutorUtils;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class TwilioSmsSender {
private static final Logger logger = LoggerFactory.getLogger(TwilioSmsSender.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
private final Meter priceMeter = metricRegistry.meter(name(getClass(), "price"));
static final String FAILED_REQUEST_COUNTER_NAME = name(TwilioSmsSender.class, "failedRequest");
static final String SERVICE_NAME_TAG = "service";
static final String STATUS_CODE_TAG_NAME = "statusCode";
static final String ERROR_CODE_TAG_NAME = "errorCode";
static final String COUNTRY_CODE_TAG_NAME = "countryCode";
/**
* @deprecated "region" conflicts with cloud provider region tags; prefer "regionCode" instead
*/
@Deprecated
static final String REGION_TAG_NAME = "region";
static final String REGION_CODE_TAG_NAME = "regionCode";
private final String accountId;
private final String accountToken;
private final String messagingServiceSid;
private final String nanpaMessagingServiceSid;
private final String localDomain;
private final Random random;
private final TwilioVerificationTextConfiguration defaultClientVerificationTexts;
private final Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts;
private final FaultTolerantHttpClient httpClient;
private final URI smsUri;
private final URI voxUri;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final TwilioVerifySender twilioVerifySender;
@VisibleForTesting
public TwilioSmsSender(String baseUri,
String baseVerifyUri,
TwilioConfiguration twilioConfiguration,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
Executor executor = ExecutorUtils.newFixedThreadBoundedQueueExecutor(10, 100);
this.accountId = twilioConfiguration.getAccountId();
this.accountToken = twilioConfiguration.getAccountToken();
this.localDomain = twilioConfiguration.getLocalDomain();
this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid();
this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid();
this.random = new Random(System.currentTimeMillis());
this.smsUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Messages.json");
this.voxUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Calls.json" );
this.httpClient = FaultTolerantHttpClient.newBuilder()
.withCircuitBreaker(twilioConfiguration.getCircuitBreaker())
.withRetry(twilioConfiguration.getRetry())
.withVersion(HttpClient.Version.HTTP_2)
.withConnectTimeout(Duration.ofSeconds(10))
.withRedirect(HttpClient.Redirect.NEVER)
.withExecutor(executor)
.withName("twilio")
.build();
this.defaultClientVerificationTexts = twilioConfiguration.getDefaultClientVerificationTexts();
this.regionalClientVerificationTexts = twilioConfiguration.getRegionalClientVerificationTexts();
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.twilioVerifySender = new TwilioVerifySender(baseVerifyUri, httpClient, twilioConfiguration);
}
public TwilioSmsSender(TwilioConfiguration twilioConfiguration, DynamicConfigurationManager dynamicConfigurationManager) {
this("https://api.twilio.com", "https://verify.twilio.com", twilioConfiguration, dynamicConfigurationManager);
}
public CompletableFuture<Boolean> deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode) {
Map<String, String> requestParameters = new HashMap<>();
requestParameters.put("To", destination);
requestParameters.put("MessagingServiceSid", "1".equals(Util.getCountryCode(destination)) ? nanpaMessagingServiceSid : messagingServiceSid);
requestParameters.put("Body", String.format(Locale.US, getBodyFormatString(destination, clientType.orElse(null)), verificationCode));
HttpRequest request = HttpRequest.newBuilder()
.uri(smsUri)
.POST(FormDataBodyPublisher.of(requestParameters))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes(StandardCharsets.UTF_8)))
.build();
smsMeter.mark();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(this::parseResponse)
.handle((response, throwable) -> processResponse(response, throwable, destination));
}
private String getBodyFormatString(@Nonnull String destination, @Nullable String clientType) {
final String countryCode = Util.getCountryCode(destination);
final TwilioVerificationTextConfiguration verificationTexts = regionalClientVerificationTexts
.getOrDefault(countryCode, defaultClientVerificationTexts);
final String result;
if ("ios".equals(clientType)) {
result = verificationTexts.getIosText();
} else if ("android-ng".equals(clientType)) {
result = verificationTexts.getAndroidNgText();
} else if ("android-2020-01".equals(clientType)) {
result = verificationTexts.getAndroid202001Text();
} else if ("android-2021-03".equals(clientType)) {
result = verificationTexts.getAndroid202103Text();
} else {
result = verificationTexts.getGenericText();
}
if ("86".equals(countryCode)) { // is China
return result + "\u2008";
// Twilio recommends adding this character to the end of strings delivered to China because some carriers in
// China are blocking GSM-7 encoding and this will force Twilio to send using UCS-2 instead.
} else {
return result;
}
}
public CompletableFuture<Boolean> deliverVoxVerification(String destination, String verificationCode, List<LanguageRange> languageRanges) {
String url = "https://" + localDomain + "/v1/voice/description/" + verificationCode;
final String languageQueryParams = languageRanges.stream()
.map(range -> Locale.forLanguageTag(range.getRange()))
.map(locale -> {
if (StringUtils.isNotBlank(locale.getCountry())) {
return locale.getLanguage().toLowerCase() + "-" + locale.getCountry().toUpperCase();
} else {
return locale.getLanguage().toLowerCase();
}
})
.map(languageTag -> "l=" + languageTag)
.collect(Collectors.joining("&"));
if (StringUtils.isNotBlank(languageQueryParams)) {
url += "?" + languageQueryParams;
}
Map<String, String> requestParameters = new HashMap<>();
requestParameters.put("Url", url);
requestParameters.put("To", destination);
requestParameters.put("From", getRandom(random, dynamicConfigurationManager.getConfiguration().getTwilioConfiguration().getNumbers()));
HttpRequest request = HttpRequest.newBuilder()
.uri(voxUri)
.POST(FormDataBodyPublisher.of(requestParameters))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes()))
.build();
voxMeter.mark();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(this::parseResponse)
.handle((response, throwable) -> processResponse(response, throwable, destination));
}
private String getRandom(Random random, List<String> elements) {
return elements.get(random.nextInt(elements.size()));
}
private boolean processResponse(TwilioResponse response, Throwable throwable, String destination) {
if (response != null && response.isSuccess()) {
priceMeter.mark((long) (response.successResponse.price * 1000));
return true;
} else if (response != null && response.isFailure()) {
String countryCode = Util.getCountryCode(destination);
String region = Util.getRegion(destination);
Metrics.counter(FAILED_REQUEST_COUNTER_NAME,
SERVICE_NAME_TAG, "classic",
STATUS_CODE_TAG_NAME, String.valueOf(response.failureResponse.status),
ERROR_CODE_TAG_NAME, String.valueOf(response.failureResponse.code),
COUNTRY_CODE_TAG_NAME, countryCode,
REGION_TAG_NAME, region,
REGION_CODE_TAG_NAME, region).increment();
logger.info("Failed with code={}, country={}",
response.failureResponse.code,
countryCode);
return false;
} else if (throwable != null) {
logger.info("Twilio request failed", throwable);
return false;
} else {
logger.warn("No response or throwable!");
return false;
}
}
private TwilioResponse 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 TwilioResponse(TwilioResponse.TwilioSuccessResponse.fromBody(mapper, response.body()));
} else {
return new TwilioResponse(new TwilioResponse.TwilioSuccessResponse());
}
}
if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) {
return new TwilioResponse(TwilioResponse.TwilioFailureResponse.fromBody(mapper, response.body()));
} else {
return new TwilioResponse(new TwilioResponse.TwilioFailureResponse());
}
}
public CompletableFuture<Optional<String>> deliverSmsVerificationWithVerify(String destination,
Optional<String> clientType, String verificationCode, List<LanguageRange> languageRanges) {
smsMeter.mark();
return twilioVerifySender.deliverSmsVerificationWithVerify(destination, clientType, verificationCode,
languageRanges);
}
public CompletableFuture<Optional<String>> deliverVoxVerificationWithVerify(String destination,
String verificationCode, List<LanguageRange> languageRanges) {
voxMeter.mark();
return twilioVerifySender.deliverVoxVerificationWithVerify(destination, verificationCode, languageRanges);
}
public CompletableFuture<Boolean> reportVerificationSucceeded(String verificationSid, @Nullable String userAgent,
String context) {
return twilioVerifySender.reportVerificationSucceeded(verificationSid, userAgent, context);
}
public static class TwilioResponse {
private TwilioSuccessResponse successResponse;
private TwilioFailureResponse failureResponse;
TwilioResponse(TwilioSuccessResponse successResponse) {
this.successResponse = successResponse;
}
TwilioResponse(TwilioFailureResponse failureResponse) {
this.failureResponse = failureResponse;
}
boolean isSuccess() {
return successResponse != null;
}
boolean isFailure() {
return failureResponse != null;
}
private static class TwilioSuccessResponse {
@JsonProperty
private double price;
static TwilioSuccessResponse fromBody(ObjectMapper mapper, String body) {
try {
return mapper.readValue(body, TwilioSuccessResponse.class);
} catch (IOException e) {
logger.warn("Error parsing twilio success response: " + e);
return new TwilioSuccessResponse();
}
}
}
private static class TwilioFailureResponse {
@JsonProperty
private int status;
@JsonProperty
private String message;
@JsonProperty
private int code;
static TwilioFailureResponse fromBody(ObjectMapper mapper, String body) {
try {
return mapper.readValue(body, TwilioFailureResponse.class);
} catch (IOException e) {
logger.warn("Error parsing twilio success response: " + e);
return new TwilioFailureResponse();
}
}
}
}
}

View File

@ -1,74 +0,0 @@
package org.whispersystems.textsecuregcm.sms;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
import java.util.Locale.LanguageRange;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
public class TwilioVerifyExperimentEnrollmentManager {
@VisibleForTesting
static final String EXPERIMENT_NAME = "twilio_verify_v1";
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private static final Set<String> INELIGIBLE_CLIENTS = Set.of("android-ng", "android-2020-01");
private final Set<String> signalExclusiveVoiceVerificationLanguages;
public TwilioVerifyExperimentEnrollmentManager(final VoiceVerificationConfiguration voiceVerificationConfiguration,
final ExperimentEnrollmentManager experimentEnrollmentManager) {
this.experimentEnrollmentManager = experimentEnrollmentManager;
// Signal voice verification supports several languages that Verify does not. We want to honor
// clients that prioritize these languages, even if they would normally be enrolled in the experiment
signalExclusiveVoiceVerificationLanguages = voiceVerificationConfiguration.getLocales().stream()
.map(loc -> loc.split("-")[0])
.filter(language -> !TwilioVerifySender.TWILIO_VERIFY_LANGUAGES.contains(language))
.collect(Collectors.toSet());
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public boolean isEnrolled(Optional<String> clientType, String number, List<LanguageRange> languageRanges,
String transport) {
final boolean clientEligible = clientType.map(client -> !INELIGIBLE_CLIENTS.contains(client))
.orElse(true);
final boolean languageEligible;
if ("sms".equals(transport)) {
// Signal only sends SMS in en, while Verify supports en + many other languages
languageEligible = true;
} else {
boolean clientPreferredLanguageOnlySupportedBySignal = false;
for (LanguageRange languageRange : languageRanges) {
final String language = languageRange.getRange().split("-")[0];
if (signalExclusiveVoiceVerificationLanguages.contains(language)) {
// Support is exclusive to Signal.
// Since this is the first match in the priority list, so let's break and honor it
clientPreferredLanguageOnlySupportedBySignal = true;
break;
}
if (TwilioVerifySender.TWILIO_VERIFY_LANGUAGES.contains(language)) {
// Twilio supports it, so we can stop looping
break;
}
// the language is supported by neither, so let's loop again
}
languageEligible = !clientPreferredLanguageOnlySupportedBySignal;
}
final boolean enrolled = experimentEnrollmentManager.isEnrolled(number, EXPERIMENT_NAME);
return clientEligible && languageEligible && enrolled;
}
}

View File

@ -1,324 +0,0 @@
package org.whispersystems.textsecuregcm.sms;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
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.annotation.Nullable;
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.metrics.UserAgentTagUtil;
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);
private static final String VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME = name(TwilioVerifySender.class,
"verificationSucceeded");
private static final String CONTEXT_TAG_NAME = "context";
private static final String STATUS_CODE_TAG_NAME = "statusCode";
private static final String ERROR_CODE_TAG_NAME = "errorCode";
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 String verifyServiceFriendlyName;
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.verifyServiceFriendlyName = twilioConfiguration.getVerifyServiceFriendlyName();
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((response, throwable) -> extractVerifySid(response, throwable, destination));
}
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((response, throwable) -> extractVerifySid(response, throwable, destination));
}
private Optional<String> extractVerifySid(TwilioVerifyResponse twilioVerifyResponse, Throwable throwable,
String destination) {
if (throwable != null) {
logger.warn("Failed to send Twilio request", throwable);
return Optional.empty();
}
if (twilioVerifyResponse.isFailure()) {
String countryCode = Util.getCountryCode(destination);
String region = Util.getRegion(destination);
Metrics.counter(TwilioSmsSender.FAILED_REQUEST_COUNTER_NAME,
TwilioSmsSender.SERVICE_NAME_TAG, "verify",
TwilioSmsSender.STATUS_CODE_TAG_NAME, String.valueOf(twilioVerifyResponse.failureResponse.status),
TwilioSmsSender.ERROR_CODE_TAG_NAME, String.valueOf(twilioVerifyResponse.failureResponse.code),
TwilioSmsSender.COUNTRY_CODE_TAG_NAME, countryCode,
TwilioSmsSender.REGION_TAG_NAME, region,
TwilioSmsSender.REGION_CODE_TAG_NAME, region).increment();
logger.info("Failed with code={}, country={}",
twilioVerifyResponse.failureResponse.code,
countryCode);
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);
requestParameters.put("CustomFriendlyName", verifyServiceFriendlyName);
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.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes()))
.build();
}
public CompletableFuture<Boolean> reportVerificationSucceeded(String verificationSid, @Nullable String userAgent,
String context) {
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.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes()))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(this::parseResponse)
.handle((response, throwable) -> processVerificationSucceededResponse(response, throwable, userAgent, context));
}
private boolean processVerificationSucceededResponse(@Nullable final TwilioVerifyResponse response,
@Nullable final Throwable throwable,
final String userAgent,
final String context) {
if (throwable == null) {
assert response != null;
final Tags tags = Tags.of(Tag.of(CONTEXT_TAG_NAME, context), UserAgentTagUtil.getPlatformTag(userAgent));
if (response.isSuccess() && "approved".equals(response.successResponse.getStatus())) {
// the other possible values of `status` are `pending` or `canceled`, but these can never happen in a response
// to this POST, so we dont consider them
Metrics.counter(VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME, tags)
.increment();
return true;
}
// at this point, response.isFailure() == true
Metrics.counter(
VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME,
Tags.of(ERROR_CODE_TAG_NAME, String.valueOf(response.failureResponse.code),
STATUS_CODE_TAG_NAME, String.valueOf(response.failureResponse.status))
.and(tags))
.increment();
} else {
logger.warn("Failed to send verification succeeded", throwable);
}
return false;
}
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();
}
}
}
}
}

View File

@ -209,32 +209,6 @@ class DynamicConfigurationTest {
} }
} }
@Test
void testParseTwilioConfiguration() throws JsonProcessingException {
{
final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true");
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
assertTrue(emptyConfig.getTwilioConfiguration().getNumbers().isEmpty());
}
{
final String twilioConfigYaml = REQUIRED_CONFIG.concat("""
twilio:
numbers:
- 2135551212
- 2135551313
""");
final DynamicTwilioConfiguration config =
DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow()
.getTwilioConfiguration();
assertEquals(List.of("2135551212", "2135551313"), config.getNumbers());
}
}
@Test @Test
void testParsePaymentsConfiguration() throws JsonProcessingException { void testParsePaymentsConfiguration() throws JsonProcessingException {
{ {

View File

@ -21,6 +21,7 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -86,7 +87,6 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.UsernameRequest; import org.whispersystems.textsecuregcm.entities.UsernameRequest;
import org.whispersystems.textsecuregcm.entities.UsernameResponse; import org.whispersystems.textsecuregcm.entities.UsernameResponse;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
@ -99,8 +99,6 @@ import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.ClientType;
import org.whispersystems.textsecuregcm.registration.MessageTransport; import org.whispersystems.textsecuregcm.registration.MessageTransport;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
@ -144,7 +142,6 @@ class AccountControllerTest {
private static final String INVALID_CAPTCHA_TOKEN = "invalid_token"; private static final String INVALID_CAPTCHA_TOKEN = "invalid_token";
private static final String TEST_NUMBER = "+14151111113"; private static final String TEST_NUMBER = "+14151111113";
private static final Integer TEST_VERIFICATION_CODE = 123456;
private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class); private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class);
private static AccountsManager accountsManager = mock(AccountsManager.class); private static AccountsManager accountsManager = mock(AccountsManager.class);
@ -158,7 +155,6 @@ class AccountControllerTest {
private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); private static RateLimiter usernameSetLimiter = mock(RateLimiter.class);
private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class);
private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class);
private static SmsSender smsSender = mock(SmsSender.class);
private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class);
private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
private static Account senderPinAccount = mock(Account.class); private static Account senderPinAccount = mock(Account.class);
@ -171,11 +167,6 @@ class AccountControllerTest {
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
private static TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = mock(
TwilioVerifyExperimentEnrollmentManager.class);
private static ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class);
private byte[] registration_lock_key = new byte[32]; private byte[] registration_lock_key = new byte[32];
private static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false); private static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
@ -194,17 +185,14 @@ class AccountControllerTest {
accountsManager, accountsManager,
abusiveHostRules, abusiveHostRules,
rateLimiters, rateLimiters,
smsSender,
registrationServiceClient, registrationServiceClient,
dynamicConfigurationManager, dynamicConfigurationManager,
turnTokenGenerator, turnTokenGenerator,
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE), Map.of(TEST_NUMBER, 123456),
recaptchaClient, recaptchaClient,
pushNotificationManager, pushNotificationManager,
verifyExperimentEnrollmentManager,
changeNumberManager, changeNumberManager,
storageCredentialGenerator, storageCredentialGenerator))
experimentEnrollmentManager))
.build(); .build();
@ -342,7 +330,6 @@ class AccountControllerTest {
usernameSetLimiter, usernameSetLimiter,
usernameReserveLimiter, usernameReserveLimiter,
usernameLookupLimiter, usernameLookupLimiter,
smsSender,
registrationServiceClient, registrationServiceClient,
turnTokenGenerator, turnTokenGenerator,
senderPinAccount, senderPinAccount,
@ -351,8 +338,6 @@ class AccountControllerTest {
senderTransfer, senderTransfer,
recaptchaClient, recaptchaClient,
pushNotificationManager, pushNotificationManager,
verifyExperimentEnrollmentManager,
experimentEnrollmentManager,
changeNumberManager); changeNumberManager);
clearInvocations(AuthHelper.DISABLED_DEVICE); clearInvocations(AuthHelper.DISABLED_DEVICE);
@ -455,7 +440,7 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.readEntity(String.class)).isBlank(); assertThat(response.readEntity(String.class)).isBlank();
verifyNoMoreInteractions(pushNotificationManager); verifyNoInteractions(pushNotificationManager);
} }
@Test @Test
@ -473,59 +458,17 @@ class AccountControllerTest {
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
verifyNoMoreInteractions(pushNotificationManager); verifyNoInteractions(pushNotificationManager);
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testSendCode(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
.queryParam("challenge", "1234-push")
.request()
.header("X-Forwarded-For", NICE_HOST)
.get();
assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) {
ArgumentCaptor<StoredVerificationCode> storedVerificationCodeArgumentCaptor = ArgumentCaptor
.forClass(StoredVerificationCode.class);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(), eq(Collections.emptyList()));
verify(pendingAccountsManager, times(2)).store(eq(SENDER), storedVerificationCodeArgumentCaptor.capture());
assertThat(storedVerificationCodeArgumentCaptor.getValue().twilioVerificationSid())
.isEqualTo("VerificationSid");
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
}
verifyNoMoreInteractions(smsSender);
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
} }
@Test @Test
void testSendCodeViaRegistrationService() throws NumberParseException { void testSendCode() throws NumberParseException {
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(sessionId)); .thenReturn(CompletableFuture.completedFuture(sessionId));
when(experimentEnrollmentManager.isEnrolled(SENDER, AccountController.REGISTRATION_SERVICE_EXPERIMENT_NAME))
.thenReturn(true);
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER)) .target(String.format("/v1/accounts/sms/code/%s", SENDER))
@ -543,8 +486,6 @@ class AccountControllerTest {
verify(pendingAccountsManager).store(eq(SENDER), argThat( verify(pendingAccountsManager).store(eq(SENDER), argThat(
storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) && storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) &&
"1234-push".equals(storedVerificationCode.pushCode()))); "1234-push".equals(storedVerificationCode.pushCode())));
verifyNoInteractions(smsSender);
} }
@Test @Test
@ -560,7 +501,7 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.readEntity(String.class)).isBlank(); assertThat(response.readEntity(String.class)).isBlank();
verify(smsSender, never()).deliverSmsVerification(any(), any(), any()); verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any());
} }
@Test @Test
@ -581,20 +522,14 @@ class AccountControllerTest {
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
verify(smsSender, never()).deliverSmsVerification(any(), any(), any()); verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any());
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) public void testSendCodeVoiceNoLocale() throws NumberParseException {
public void testSendCodeVoiceNoLocale(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(enrolledInVerifyExperiment); .thenReturn(CompletableFuture.completedFuture(new byte[16]));
if (enrolledInVerifyExperiment) {
when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -604,122 +539,18 @@ class AccountControllerTest {
.header("X-Forwarded-For", NICE_HOST) .header("X-Forwarded-For", NICE_HOST)
.get(); .get();
assertThat(response.getStatus()).isEqualTo(200); final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
if (enrolledInVerifyExperiment) { assertThat(response.getStatus()).isEqualTo(200);
verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Collections.emptyList())); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.VOICE, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} else {
verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Collections.emptyList()));
}
verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendCodeWithValidPreauth() throws NumberParseException {
public void testSendCodeVoiceSingleLocale(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(enrolledInVerifyExperiment); .thenReturn(CompletableFuture.completedFuture(new byte[16]));
if (enrolledInVerifyExperiment) {
when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/voice/code/%s", SENDER))
.queryParam("challenge", "1234-push")
.request()
.header("Accept-Language", "pt-BR")
.header("X-Forwarded-For", NICE_HOST)
.get();
assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) {
verify(smsSender)
.deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Locale.LanguageRange.parse("pt-BR")));
} else {
verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Locale.LanguageRange.parse("pt-BR")));
}
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void testSendCodeVoiceMultipleLocales(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/voice/code/%s", SENDER))
.queryParam("challenge", "1234-push")
.request()
.header("Accept-Language", "en-US;q=1, ar-US;q=0.9, fa-US;q=0.8, zh-Hans-US;q=0.7, ru-RU;q=0.6, zh-Hant-US;q=0.5")
.header("X-Forwarded-For", NICE_HOST)
.get();
assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) {
verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Locale.LanguageRange
.parse("en-US;q=1, ar-US;q=0.9, fa-US;q=0.8, zh-Hans-US;q=0.7, ru-RU;q=0.6, zh-Hant-US;q=0.5")));
} else {
verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Locale.LanguageRange
.parse("en-US;q=1, ar-US;q=0.9, fa-US;q=0.8, zh-Hans-US;q=0.7, ru-RU;q=0.6, zh-Hant-US;q=0.5")));
}
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testSendCodeVoiceInvalidLocale(boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/voice/code/%s", SENDER))
.queryParam("challenge", "1234-push")
.request()
.header("Accept-Language", "This is not a reasonable Accept-Language value")
.header("X-Forwarded-For", NICE_HOST)
.get();
// Should still send a code, just with no accept language
assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) {
verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Collections.emptyList()));
} else {
verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Collections.emptyList()));
}
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testSendCodeWithValidPreauth(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -731,17 +562,14 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) { final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER_PREAUTH, null);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(),
eq(Collections.emptyList())); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString());
}
verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
} }
@Test @Test
void testSendCodeWithInvalidPreauth() throws Exception { void testSendCodeWithInvalidPreauth() {
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH))
@ -752,12 +580,12 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
verifyNoMoreInteractions(abusiveHostRules); verifyNoInteractions(abusiveHostRules);
} }
@Test @Test
void testSendCodeWithNoPreauth() throws Exception { void testSendCodeWithNoPreauth() {
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH))
@ -767,22 +595,14 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(402); assertThat(response.getStatus()).isEqualTo(402);
verify(smsSender, never()).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString()); verifyNoInteractions(registrationServiceClient);
verify(smsSender, never()).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(), anyList());
} }
@Test
void testSendiOSCode() throws NumberParseException {
@ParameterizedTest when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
@ValueSource(booleans = {false, true}) .thenReturn(CompletableFuture.completedFuture(new byte[16]));
void testSendiOSCode(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -795,25 +615,15 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) { final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("ios")), anyString(),
eq(Collections.emptyList())); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.IOS, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("ios")), anyString());
}
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendAndroidNgCode() throws NumberParseException {
void testSendAndroidNgCode(final boolean enrolledInVerifyExperiment) throws Exception { when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -826,25 +636,13 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) { final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("android-ng")), anyString(),
eq(Collections.emptyList())); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.ANDROID_WITHOUT_FCM, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("android-ng")), anyString());
}
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendAbusiveHost() {
void testSendAbusiveHost(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -857,20 +655,14 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(402); assertThat(response.getStatus()).isEqualTo(402);
verify(abusiveHostRules).isBlocked(eq(ABUSIVE_HOST)); verify(abusiveHostRules).isBlocked(eq(ABUSIVE_HOST));
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendAbusiveHostWithValidCaptcha() throws NumberParseException {
void testSendAbusiveHostWithValidCaptcha(final boolean enrolledInVerifyExperiment) throws IOException {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(enrolledInVerifyExperiment); .thenReturn(CompletableFuture.completedFuture(new byte[16]));
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -882,27 +674,16 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verifyNoMoreInteractions(abusiveHostRules); final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
verifyNoInteractions(abusiveHostRules);
verify(recaptchaClient).verify(eq(VALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST)); verify(recaptchaClient).verify(eq(VALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST));
if (enrolledInVerifyExperiment) { verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(),
eq(Collections.emptyList()));
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
}
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendAbusiveHostWithInvalidCaptcha() {
void testSendAbusiveHostWithInvalidCaptcha(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER)) .target(String.format("/v1/accounts/sms/code/%s", SENDER))
@ -913,23 +694,13 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(402); assertThat(response.getStatus()).isEqualTo(402);
verifyNoMoreInteractions(abusiveHostRules); verifyNoInteractions(abusiveHostRules);
verify(recaptchaClient).verify(eq(INVALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST)); verify(recaptchaClient).verify(eq(INVALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST));
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendRateLimitedHostAutoBlock() {
void testSendRateLimitedHostAutoBlock(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER)) .target(String.format("/v1/accounts/sms/code/%s", SENDER))
@ -944,21 +715,12 @@ class AccountControllerTest {
verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_IP_HOST)); verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_IP_HOST));
verifyNoMoreInteractions(abusiveHostRules); verifyNoMoreInteractions(abusiveHostRules);
verifyNoMoreInteractions(recaptchaClient); verifyNoInteractions(recaptchaClient);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendRateLimitedPrefixAutoBlock() {
void testSendRateLimitedPrefixAutoBlock(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -974,21 +736,12 @@ class AccountControllerTest {
verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_PREFIX_HOST)); verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_PREFIX_HOST));
verifyNoMoreInteractions(abusiveHostRules); verifyNoMoreInteractions(abusiveHostRules);
verifyNoMoreInteractions(recaptchaClient); verifyNoInteractions(recaptchaClient);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendRateLimitedHostNoAutoBlock() {
void testSendRateLimitedHostNoAutoBlock(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1003,17 +756,13 @@ class AccountControllerTest {
verify(abusiveHostRules).isBlocked(eq(RATE_LIMITED_HOST2)); verify(abusiveHostRules).isBlocked(eq(RATE_LIMITED_HOST2));
verifyNoMoreInteractions(abusiveHostRules); verifyNoMoreInteractions(abusiveHostRules);
verifyNoMoreInteractions(recaptchaClient); verifyNoInteractions(recaptchaClient);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendMultipleHost() {
void testSendMultipleHost(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1028,16 +777,12 @@ class AccountControllerTest {
verify(abusiveHostRules, times(1)).isBlocked(eq(ABUSIVE_HOST)); verify(abusiveHostRules, times(1)).isBlocked(eq(ABUSIVE_HOST));
verifyNoMoreInteractions(abusiveHostRules); verifyNoMoreInteractions(abusiveHostRules);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendRestrictedHostOut() {
void testSendRestrictedHostOut(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
final String challenge = "challenge"; final String challenge = "challenge";
when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER)) when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER))
@ -1054,17 +799,15 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(402); assertThat(response.getStatus()).isEqualTo(402);
verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({
"+12025550123, true, true", "+12025550123, true",
"+12025550123, false, true", "+12505550199, false",
"+12505550199, true, false",
"+12505550199, false, false",
}) })
void testRestrictedRegion(final String number, final boolean enrolledInVerifyExperiment, final boolean expectSendCode) { void testRestrictedRegion(final String number, final boolean expectSendCode) throws NumberParseException {
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
@ -1073,15 +816,12 @@ class AccountControllerTest {
when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig); when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig);
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
final String challenge = "challenge"; final String challenge = "challenge";
when(pendingAccountsManager.getCodeForNumber(number)) when(pendingAccountsManager.getCodeForNumber(number))
.thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null))); .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
when(smsSender.deliverSmsVerificationWithTwilioVerify(any(), any(), any(), any())) when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(Optional.empty())); .thenReturn(CompletableFuture.completedFuture(new byte[16]));
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1094,31 +834,22 @@ class AccountControllerTest {
if (expectSendCode) { if (expectSendCode) {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) { final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(number, null);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(number), any(), any(), any()); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} else {
verify(smsSender).deliverSmsVerification(eq(number), any(), any());
}
} else { } else {
assertThat(response.getStatus()).isEqualTo(402); assertThat(response.getStatus()).isEqualTo(402);
verifyNoMoreInteractions(smsSender); verifyNoInteractions(registrationServiceClient);
} }
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testSendRestrictedIn() throws NumberParseException {
void testSendRestrictedIn(final boolean enrolledInVerifyExperiment) throws Exception {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
if (enrolledInVerifyExperiment) {
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
}
final String challenge = "challenge"; final String challenge = "challenge";
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null))); when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1130,40 +861,38 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
if (enrolledInVerifyExperiment) { final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(),
eq(Collections.emptyList()));
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
}
verifyNoMoreInteractions(smsSender); verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} }
@Test @Test
void testSendCodeTestDeviceNumber() throws Exception { void testSendCodeTestDeviceNumber() throws NumberParseException {
// no push code and a blocked host, but should evade captchas and skip smsSender final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(sessionId));
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", TEST_NUMBER)) .target(String.format("/v1/accounts/sms/code/%s", TEST_NUMBER))
.request() .request()
.header("X-Forwarded-For", ABUSIVE_HOST) .header("X-Forwarded-For", ABUSIVE_HOST)
.get(); .get();
ArgumentCaptor<StoredVerificationCode> captor = ArgumentCaptor.forClass(StoredVerificationCode.class);
final ArgumentCaptor<StoredVerificationCode> captor = ArgumentCaptor.forClass(StoredVerificationCode.class);
verify(pendingAccountsManager).store(eq(TEST_NUMBER), captor.capture()); verify(pendingAccountsManager).store(eq(TEST_NUMBER), captor.capture());
assertThat(captor.getValue().code()).isEqualTo(Integer.toString(TEST_VERIFICATION_CODE)); assertThat(captor.getValue().code()).isNull();
assertThat(captor.getValue().sessionId()).isEqualTo(sessionId);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verifyNoInteractions(smsSender);
// Even though no actual SMS will be sent, we leave that decision to the registration service
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(TEST_NUMBER, null);
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} }
@ParameterizedTest @Test
@ValueSource(booleans = {false, true}) void testVerifyCode() throws Exception {
void testVerifyCode(final boolean enrolledInVerifyExperiment) throws Exception {
if (enrolledInVerifyExperiment) {
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(
Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", "VerificationSid", null)));
}
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/code/%s", "1234")) .target(String.format("/v1/accounts/code/%s", "1234"))
.request() .request()
@ -1171,11 +900,6 @@ class AccountControllerTest {
.put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); .put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList()); verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
if (enrolledInVerifyExperiment) {
verify(smsSender).reportVerificationSucceeded(eq("VerificationSid"), any(), eq("registration"));
}
verifyNoInteractions(registrationServiceClient); verifyNoInteractions(registrationServiceClient);
} }
@ -1199,7 +923,6 @@ class AccountControllerTest {
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList()); verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT); verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT);
verifyNoInteractions(smsSender);
} }
@Test @Test
@ -1225,7 +948,7 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager); verifyNoInteractions(accountsManager);
} }
@Test @Test
@ -1240,7 +963,7 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager); verifyNoInteractions(accountsManager);
} }
@Test @Test
@ -1266,7 +989,6 @@ class AccountControllerTest {
verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT); verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT);
verifyNoInteractions(accountsManager); verifyNoInteractions(accountsManager);
verifyNoInteractions(smsSender);
} }
@Test @Test
@ -1320,7 +1042,7 @@ class AccountControllerTest {
assertThat(result.getUuid()).isNotNull(); assertThat(result.getUuid()).isNotNull();
verifyNoMoreInteractions(pinLimiter); verifyNoInteractions(pinLimiter);
} finally { } finally {
when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock); when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock);
} }
@ -1361,7 +1083,7 @@ class AccountControllerTest {
assertThat(failure.getBackupCredentials().getPassword().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue(); assertThat(failure.getBackupCredentials().getPassword().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue();
assertThat(failure.getTimeRemaining()).isGreaterThan(0); assertThat(failure.getTimeRemaining()).isGreaterThan(0);
verifyNoMoreInteractions(pinLimiter); verifyNoInteractions(pinLimiter);
} }
@Test @Test
@ -1411,55 +1133,14 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@Test
void testVerifyTestDeviceNumber() throws Exception {
when(pendingAccountsManager.getCodeForNumber(TEST_NUMBER)).thenReturn(Optional.of(
new StoredVerificationCode(Integer.toString(TEST_VERIFICATION_CODE), System.currentTimeMillis(), "push", null, null)));
final Response response = resources.getJerseyTest()
.target(String.format("/v1/accounts/code/%s", TEST_VERIFICATION_CODE))
.request()
.header("Authorization", AuthHelper.getProvisioningAuthHeader(TEST_NUMBER, "bar"))
.put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE));
verify(accountsManager).create(eq(TEST_NUMBER), eq("bar"), any(), any(), anyList());
assertThat(response.getStatus()).isEqualTo(200);
}
@Test @Test
void testChangePhoneNumber() throws Exception { void testChangePhoneNumber() throws Exception {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any(), any(), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(number);
assertThat(accountIdentityResponse.getPni()).isNotEqualTo(AuthHelper.VALID_PNI);
}
@Test
void testChangePhoneNumberWithRegistrationService() throws Exception {
final String number = "+18005559876";
final String code = "987654";
final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId))); new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any())) when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true)); .thenReturn(CompletableFuture.completedFuture(true));
@ -1557,26 +1238,6 @@ class AccountControllerTest {
void testChangePhoneNumberIncorrectCode() throws Exception { void testChangePhoneNumberIncorrectCode() throws Exception {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final Response response =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null, null, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any());
}
@Test
void testChangePhoneNumberIncorrectCodeWithRegistrationService() throws Exception {
final String number = "+18005559876";
final String code = "987654";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
@ -1603,9 +1264,13 @@ class AccountControllerTest {
void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception { void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(false); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(false);
@ -1633,9 +1298,13 @@ class AccountControllerTest {
void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception { void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@ -1664,9 +1333,13 @@ class AccountControllerTest {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
final String reglock = "setec-astronomy"; final String reglock = "setec-astronomy";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@ -1696,9 +1369,13 @@ class AccountControllerTest {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
final String reglock = "setec-astronomy"; final String reglock = "setec-astronomy";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@ -1728,6 +1405,7 @@ class AccountControllerTest {
final String number = "+18005559876"; final String number = "+18005559876";
final String code = "987654"; final String code = "987654";
final String pniIdentityKey = "changed-pni-identity-key"; final String pniIdentityKey = "changed-pni-identity-key";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
Device device2 = mock(Device.class); Device device2 = mock(Device.class);
when(device2.getId()).thenReturn(2L); when(device2.getId()).thenReturn(2L);
@ -1744,7 +1422,10 @@ class AccountControllerTest {
when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3)); when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3));
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
var deviceMessages = List.of( var deviceMessages = List.of(
new IncomingMessage(1, 2, 2, "content2"), new IncomingMessage(1, 2, 2, "content2"),
@ -2104,7 +1785,8 @@ class AccountControllerTest {
@ParameterizedTest @ParameterizedTest
@MethodSource @MethodSource
void testSignupCaptcha(final String message, final boolean enforced, final Set<String> countryCodes, final int expectedResponseStatusCode) { void testSignupCaptcha(final String message, final boolean enforced, final Set<String> countryCodes, final int expectedResponseStatusCode)
throws NumberParseException {
DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfigurationManager.getConfiguration()) when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration); .thenReturn(dynamicConfiguration);
@ -2114,6 +1796,9 @@ class AccountControllerTest {
when(dynamicConfiguration.getCaptchaConfiguration()) when(dynamicConfiguration.getCaptchaConfiguration())
.thenReturn(signupCaptchaConfig); .thenReturn(signupCaptchaConfig);
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
Response response = Response response =
resources.getJerseyTest() resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER)) .target(String.format("/v1/accounts/sms/code/%s", SENDER))
@ -2124,8 +1809,10 @@ class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode); assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode);
verify(smsSender, 200 == expectedResponseStatusCode ? times(1) : never()) final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
.deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
verify(registrationServiceClient, 200 == expectedResponseStatusCode ? times(1) : never())
.sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
} }
static Stream<Arguments> testSignupCaptcha() { static Stream<Arguments> testSignupCaptcha() {

View File

@ -1,89 +0,0 @@
package org.whispersystems.textsecuregcm.sms;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
class TwilioVerifyExperimentEnrollmentManagerTest {
private final ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class);
private final VoiceVerificationConfiguration voiceVerificationConfiguration = mock(VoiceVerificationConfiguration.class);
private TwilioVerifyExperimentEnrollmentManager manager;
private static final String NUMBER = "+15055551212";
private static final Optional<String> INELIGIBLE_CLIENT = Optional.of("android-2020-01");
private static final Optional<String> ELIGIBLE_CLIENT = Optional.of("anything");
private static final List<LanguageRange> LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL = LanguageRange.parse("am");
private static final List<LanguageRange> LANGUAGE_NOT_SUPPORTED_BY_SIGNAL_OR_TWILIO = LanguageRange.parse("xx");
private static final List<LanguageRange> LANGUAGE_SUPPORTED_BY_TWILIO = LanguageRange.parse("fr-CA");
@BeforeEach
void setup() {
when(voiceVerificationConfiguration.getLocales())
.thenReturn(Set.of("am", "en-US", "fr-CA"));
manager = new TwilioVerifyExperimentEnrollmentManager(
voiceVerificationConfiguration,
experimentEnrollmentManager);
}
@ParameterizedTest
@MethodSource
void testIsEnrolled(String message, boolean expected, Optional<String> clientType, String number,
List<LanguageRange> languageRanges, String transport, boolean managerResponse) {
when(experimentEnrollmentManager.isEnrolled(number, TwilioVerifyExperimentEnrollmentManager.EXPERIMENT_NAME))
.thenReturn(managerResponse);
assertEquals(expected, manager.isEnrolled(clientType, number, languageRanges, transport), message);
}
static Stream<Arguments> testIsEnrolled() {
return Stream.of(
Arguments.of("ineligible client", false, INELIGIBLE_CLIENT, NUMBER, Collections.emptyList(), "sms", true),
Arguments
.of("ineligible client", false, Optional.of("android-ng"), NUMBER, Collections.emptyList(), "sms", true),
Arguments
.of("client, language, and manager all agree on enrollment", true, ELIGIBLE_CLIENT, NUMBER,
LANGUAGE_SUPPORTED_BY_TWILIO,
"sms", true),
Arguments
.of("enrolled: ineligible language doesnt matter with sms", true, ELIGIBLE_CLIENT, NUMBER,
LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL, "sms",
true),
Arguments
.of("not enrolled: language only supported by Signal is preferred", false, ELIGIBLE_CLIENT, NUMBER, List.of(
LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL.get(0), LANGUAGE_SUPPORTED_BY_TWILIO.get(0)), "voice", true),
Arguments.of("enrolled: preferred language is supported", true, ELIGIBLE_CLIENT, NUMBER, List.of(
LANGUAGE_SUPPORTED_BY_TWILIO.get(0), LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL
.get(0)), "voice", true),
Arguments
.of("enrolled: preferred (and only) language is not supported by Signal or Twilio", true, ELIGIBLE_CLIENT,
NUMBER, LANGUAGE_NOT_SUPPORTED_BY_SIGNAL_OR_TWILIO, "voice", true),
Arguments.of("not enrolled: preferred language (and only) is only supported by Siganl", false, ELIGIBLE_CLIENT,
NUMBER, LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL, "voice", true)
);
}
}

View File

@ -1,248 +0,0 @@
/*
* Copyright 2021-2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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.core.WireMockConfiguration.wireMockConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
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 java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.util.ExecutorUtils;
@SuppressWarnings("OptionalGetWithoutIsPresent")
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 SERVICE_FRIENDLY_NAME = "SignalTest";
private static final String VERIFICATION_SID = "verification";
@RegisterExtension
private final WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
.build();
private TwilioVerifySender sender;
@BeforeEach
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:" + wireMock.getPort(), 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);
configuration.setVerifyServiceFriendlyName(SERVICE_FRIENDLY_NAME);
return configuration;
}
private void setupSuccessStubForVerify() {
wireMock.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\"}")));
}
@ParameterizedTest
@MethodSource
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());
wireMock.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&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME
+ "&CustomCode=123456" + (expectAppHash ? "&AppHash=" + ANDROID_APP_HASH : "")
)));
}
@SuppressWarnings("unused")
private static Stream<Arguments> deliverSmsVerificationWithVerify() {
return Stream.of(
// client, languageRange, expectAppHash, expectedLocale
Arguments.of("ios", "fr-CA, en", false, "fr"),
Arguments.of("android-2021-03", "zh-HK, it", true, "zh-HK"),
Arguments.of(null, null, false, null)
);
}
@ParameterizedTest
@MethodSource
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());
wireMock.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&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME
+ "&CustomCode=123456")));
}
@SuppressWarnings("unused")
private static Stream<Arguments> deliverVoxVerificationWithVerify() {
return Stream.of(
// languageRange, expectedLocale
Arguments.of("fr-CA, en", "fr"),
Arguments.of("zh-HK, it", "zh-HK"),
Arguments.of("en-CAA, en", "en"),
Arguments.of(null, null)
);
}
@Test
void testSmsFiveHundred() throws Exception {
wireMock.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();
wireMock.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&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME
+ "&CustomCode=123456")));
}
@Test
void testVoxFiveHundred() throws Exception {
wireMock.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();
wireMock.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&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME
+ "&CustomCode=123456")));
}
@Test
void reportVerificationSucceeded() throws Exception {
wireMock.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, null, "test").get();
assertThat(success).isTrue();
wireMock.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")));
}
@Test
void reportVerificationFailed() throws Exception {
wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
.withBody("{\"status\": 404, \"code\": 20404}")));
final Boolean success = sender.reportVerificationSucceeded(VERIFICATION_SID, null, "test").get();
assertThat(success).isFalse();
wireMock.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")));
}
}

View File

@ -1,42 +0,0 @@
package org.whispersystems.textsecuregcm.tests.sms;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
class SmsSenderTest {
private static final String NON_MEXICO_NUMBER = "+12345678901";
private static final String MEXICO_NON_MOBILE_NUMBER = "+52234567890";
private static final String MEXICO_MOBILE_NUMBER = "+52123456789";
private final TwilioSmsSender twilioSmsSender = mock(TwilioSmsSender.class);
private final SmsSender smsSender = new SmsSender(twilioSmsSender);
@Test
void testDeliverSmsVerificationNonMexico() {
smsSender.deliverSmsVerification(NON_MEXICO_NUMBER, Optional.empty(), "");
verify(twilioSmsSender, times(1))
.deliverSmsVerification(NON_MEXICO_NUMBER, Optional.empty(), "");
}
@Test
void testDeliverSmsVerificationMexicoNonMobile() {
smsSender.deliverSmsVerification(MEXICO_NON_MOBILE_NUMBER, Optional.empty(), "");
verify(twilioSmsSender, times(1))
.deliverSmsVerification("+521" + MEXICO_NON_MOBILE_NUMBER.substring("+52".length()), Optional.empty(), "");
}
@Test
void testDeliverSmsVerificationMexicoMobile() {
smsSender.deliverSmsVerification(MEXICO_MOBILE_NUMBER, Optional.empty(), "");
verify(twilioSmsSender, times(1))
.deliverSmsVerification(MEXICO_MOBILE_NUMBER, Optional.empty(), "");
}
}

View File

@ -1,291 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.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.matching;
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.core.WireMockConfiguration.wireMockConfig;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import java.util.List;
import java.util.Locale.LanguageRange;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTwilioConfiguration;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
class TwilioSmsSenderTest {
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";
@RegisterExtension
private final WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
.build();
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private TwilioSmsSender sender;
@BeforeEach
void setup() {
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration();
DynamicTwilioConfiguration dynamicTwilioConfiguration = new DynamicTwilioConfiguration();
dynamicConfiguration.setTwilioConfiguration(dynamicTwilioConfiguration);
dynamicTwilioConfiguration.setNumbers(List.of("+14151111111", "+14152222222"));
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
TwilioConfiguration configuration = createTwilioConfiguration();
sender = new TwilioSmsSender("http://localhost:" + wireMock.getPort(), "http://localhost:11111", configuration, dynamicConfigurationManager);
}
@Nonnull
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.setDefaultClientVerificationTexts(createTwlilioVerificationText(""));
configuration.setRegionalClientVerificationTexts(
Map.of("33", createTwlilioVerificationText("[33] "))
);
configuration.setAndroidAppHash("someHash");
return configuration;
}
private TwilioVerificationTextConfiguration createTwlilioVerificationText(final String prefix) {
TwilioVerificationTextConfiguration verificationTextConfiguration = new TwilioVerificationTextConfiguration();
verificationTextConfiguration.setIosText(prefix + "Verify on iOS: %1$s\n\nsomelink://verify/%1$s");
verificationTextConfiguration.setAndroidNgText(prefix + "<#> Verify on AndroidNg: %1$s\n\ncharacters");
verificationTextConfiguration.setAndroid202001Text(prefix + "Verify on Android202001: %1$s\n\nsomelink://verify/%1$s\n\ncharacters");
verificationTextConfiguration.setAndroid202103Text(prefix + "Verify on Android202103: %1$s\n\ncharacters");
verificationTextConfiguration.setGenericText(prefix + "Verify on whatever: %1$s");
return verificationTextConfiguration;
}
private void setupSuccessStubForSms() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"price\": -0.00750, \"status\": \"sent\"}")));
}
@Test
void testSendSms() {
setupSuccessStubForSms();
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters")));
}
@Test
void testSendSmsAndroid202001() {
setupSuccessStubForSms();
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-2020-01"), "123-456").join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=Verify+on+Android202001%3A+123-456%0A%0Asomelink%3A%2F%2Fverify%2F123-456%0A%0Acharacters")));
}
@Test
void testSendSmsAndroid202103() {
setupSuccessStubForSms();
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-2021-03"), "123456").join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=Verify+on+Android202103%3A+123456%0A%0Acharacters")));
}
@Test
void testSendSmsNanpaMessagingService() {
setupSuccessStubForSms();
TwilioConfiguration configuration = createTwilioConfiguration();
configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID);
TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMock.getPort(),
"http://localhost:11111", configuration, dynamicConfigurationManager);
assertThat(sender.deliverSmsVerification("+14153333333", Optional.of("ios"), "654-321").join()).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=Verify+on+iOS%3A+654-321%0A%0Asomelink%3A%2F%2Fverify%2F654-321")));
wireMock.resetRequests();
assertThat(sender.deliverSmsVerification("+447911123456", Optional.of("ios"), "654-321").join()).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B447911123456&Body=Verify+on+iOS%3A+654-321%0A%0Asomelink%3A%2F%2Fverify%2F654-321")));
}
@Test
void testSendVox() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"price\": -0.00750, \"status\": \"completed\"}")));
boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("en-US")).join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den-US")));
}
@Test
void testSendVoxMultipleLocales() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"price\": -0.00750, \"status\": \"completed\"}")));
boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("en-US;q=1, ar-US;q=0.9, fa-US;q=0.8, zh-Hans-US;q=0.7, ru-RU;q=0.6, zh-Hant-US;q=0.5")).join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den-US%26l%3Dar-US%26l%3Dfa-US%26l%3Dzh-US%26l%3Dru-RU%26l%3Dzh-US")));
}
@Test
void testSendSmsFiveHundred() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withStatus(500)
.withHeader("Content-Type", "application/json")
.withBody("{\"message\": \"Server error!\"}")));
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join();
assertThat(success).isFalse();
wireMock.verify(3, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters")));
}
@Test
void testSendVoxFiveHundred() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withStatus(500)
.withHeader("Content-Type", "application/json")
.withBody("{\"message\": \"Server error!\"}")));
boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("en-US")).join();
assertThat(success).isFalse();
wireMock.verify(3, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den-US")));
}
@Test
void testSendSmsNetworkFailure() {
TwilioConfiguration configuration = createTwilioConfiguration();
TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + 39873, "http://localhost:" + 39873, configuration, dynamicConfigurationManager);
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join();
assertThat(success).isFalse();
}
@Test
void testRetrySmsOnUnreachableErrorCodeIsTriedOnlyOnceWithoutSenderId() {
wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
.willReturn(aResponse()
.withStatus(400)
.withHeader("Content-Type", "application/json")
.withBody("{\"status\": 400, \"message\": \"is not currently reachable\", \"code\": 21612}")));
boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join();
assertThat(success).isFalse();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=nanpa_test_messaging_service_id&To=%2B14153333333&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters")));
}
@Test
void testSendSmsChina() {
setupSuccessStubForSms();
boolean success = sender.deliverSmsVerification("+861065529988", Optional.of("android-ng"), "123-456").join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B861065529988&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters%E2%80%88")));
}
@Test
void testSendSmsRegionalVerificationText() {
setupSuccessStubForSms();
boolean success = sender.deliverSmsVerification("+33655512673", Optional.of("android-ng"), "123-456").join();
assertThat(success).isTrue();
wireMock.verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B33655512673&Body=%5B33%5D+%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters")));
}
}