Discard old Twilio machinery and rely entirely on the stand-alone registration service
This commit is contained in:
parent
78f95e4859
commit
74d65b37a8
|
@ -62,29 +62,6 @@ dynamoDbTables:
|
|||
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
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfig
|
|||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
|
||||
|
@ -75,11 +74,6 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private DynamoDbTables dynamoDbTables;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private TwilioConfiguration twilio;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
|
@ -297,10 +291,6 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return webSocket;
|
||||
}
|
||||
|
||||
public TwilioConfiguration getTwilioConfiguration() {
|
||||
return twilio;
|
||||
}
|
||||
|
||||
public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() {
|
||||
return awsAttachments;
|
||||
}
|
||||
|
|
|
@ -163,9 +163,6 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
|||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||
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.storage.AbusiveHostRules;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountCleaner;
|
||||
|
@ -440,9 +437,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
|
||||
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
|
||||
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
|
||||
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
|
||||
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
|
@ -499,8 +493,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(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);
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
|
||||
|
@ -646,9 +638,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
// these should be common, but use @Auth DisabledPermittedAccount, which isn’t supported yet on websocket
|
||||
environment.jersey().register(
|
||||
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
||||
smsSender, registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
recaptchaClient, pushNotificationManager, verifyExperimentEnrollmentManager,
|
||||
changeNumberManager, backupCredentialsGenerator, experimentEnrollmentManager));
|
||||
registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
recaptchaClient, pushNotificationManager, changeNumberManager, backupCredentialsGenerator));
|
||||
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -29,10 +29,6 @@ public class DynamicConfiguration {
|
|||
@Valid
|
||||
private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
|
||||
|
@ -86,15 +82,6 @@ public class DynamicConfiguration {
|
|||
return payments;
|
||||
}
|
||||
|
||||
public DynamicTwilioConfiguration getTwilioConfiguration() {
|
||||
return twilio;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setTwilioConfiguration(DynamicTwilioConfiguration twilioConfiguration) {
|
||||
this.twilio = twilioConfiguration;
|
||||
}
|
||||
|
||||
public DynamicCaptchaConfiguration getCaptchaConfiguration() {
|
||||
return captcha;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -21,13 +21,9 @@ import io.micrometer.core.instrument.Tags;
|
|||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
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.UsernameRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameResponse;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
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.MessageTransport;
|
||||
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.Account;
|
||||
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.UsernameGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@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 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 CHALLENGE_PRESENT_TAG_NAME = "present";
|
||||
|
@ -157,7 +145,6 @@ public class AccountController {
|
|||
private final AccountsManager accounts;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final RegistrationServiceClient registrationServiceClient;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
|
@ -166,13 +153,8 @@ public class AccountController {
|
|||
private final PushNotificationManager pushNotificationManager;
|
||||
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
||||
|
||||
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
|
||||
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||
private final ChangeNumberManager changeNumberManager;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String REGISTRATION_SERVICE_EXPERIMENT_NAME = "registration-service";
|
||||
|
||||
@VisibleForTesting
|
||||
static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
|
||||
|
||||
|
@ -180,33 +162,27 @@ public class AccountController {
|
|||
AccountsManager accounts,
|
||||
AbusiveHostRules abusiveHostRules,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory,
|
||||
RegistrationServiceClient registrationServiceClient,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
TurnTokenGenerator turnTokenGenerator,
|
||||
Map<String, Integer> testDevices,
|
||||
RecaptchaClient recaptchaClient,
|
||||
PushNotificationManager pushNotificationManager,
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
|
||||
ChangeNumberManager changeNumberManager,
|
||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator,
|
||||
final ExperimentEnrollmentManager experimentEnrollmentManager)
|
||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.registrationServiceClient = registrationServiceClient;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.testDevices = testDevices;
|
||||
this.turnTokenGenerator = turnTokenGenerator;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
|
||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||
this.changeNumberManager = changeNumberManager;
|
||||
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -304,127 +280,6 @@ public class AccountController {
|
|||
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;
|
||||
|
||||
try {
|
||||
|
@ -461,6 +316,15 @@ public class AccountController {
|
|||
sessionId);
|
||||
|
||||
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
|
||||
|
@ -497,10 +361,6 @@ public class AccountController {
|
|||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
|
||||
.ifPresent(
|
||||
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "registration"));
|
||||
|
||||
Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
|
@ -552,23 +412,15 @@ public class AccountController {
|
|||
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
|
||||
final Optional<StoredVerificationCode> maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number);
|
||||
|
||||
final boolean codeVerified = maybeStoredVerificationCode.map(storedVerificationCode ->
|
||||
storedVerificationCode.sessionId() != null ?
|
||||
registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(),
|
||||
request.code(), REGISTRATION_RPC_TIMEOUT).join() :
|
||||
storedVerificationCode.isValid(request.code()))
|
||||
final boolean codeVerified = pendingAccounts.getCodeForNumber(number).map(storedVerificationCode ->
|
||||
registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(),
|
||||
request.code(), REGISTRATION_RPC_TIMEOUT).join())
|
||||
.orElse(false);
|
||||
|
||||
if (!codeVerified) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
|
||||
.ifPresent(
|
||||
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "changeNumber"));
|
||||
|
||||
final Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
|
@ -1039,17 +891,6 @@ public class AccountController {
|
|||
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() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] challenge = new byte[16];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 don‘t 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
void testParsePaymentsConfiguration() throws JsonProcessingException {
|
||||
{
|
||||
|
|
|
@ -21,6 +21,7 @@ import static org.mockito.Mockito.reset;
|
|||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
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.UsernameRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameResponse;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
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.MessageTransport;
|
||||
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.Account;
|
||||
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 TEST_NUMBER = "+14151111113";
|
||||
private static final Integer TEST_VERIFICATION_CODE = 123456;
|
||||
|
||||
private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class);
|
||||
private static AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
|
@ -158,7 +155,6 @@ class AccountControllerTest {
|
|||
private static RateLimiter usernameSetLimiter = mock(RateLimiter.class);
|
||||
private static RateLimiter usernameReserveLimiter = 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 TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
|
||||
private static Account senderPinAccount = mock(Account.class);
|
||||
|
@ -171,11 +167,6 @@ class AccountControllerTest {
|
|||
|
||||
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 static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
|
||||
|
||||
|
@ -194,17 +185,14 @@ class AccountControllerTest {
|
|||
accountsManager,
|
||||
abusiveHostRules,
|
||||
rateLimiters,
|
||||
smsSender,
|
||||
registrationServiceClient,
|
||||
dynamicConfigurationManager,
|
||||
turnTokenGenerator,
|
||||
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE),
|
||||
Map.of(TEST_NUMBER, 123456),
|
||||
recaptchaClient,
|
||||
pushNotificationManager,
|
||||
verifyExperimentEnrollmentManager,
|
||||
changeNumberManager,
|
||||
storageCredentialGenerator,
|
||||
experimentEnrollmentManager))
|
||||
storageCredentialGenerator))
|
||||
.build();
|
||||
|
||||
|
||||
|
@ -342,7 +330,6 @@ class AccountControllerTest {
|
|||
usernameSetLimiter,
|
||||
usernameReserveLimiter,
|
||||
usernameLookupLimiter,
|
||||
smsSender,
|
||||
registrationServiceClient,
|
||||
turnTokenGenerator,
|
||||
senderPinAccount,
|
||||
|
@ -351,8 +338,6 @@ class AccountControllerTest {
|
|||
senderTransfer,
|
||||
recaptchaClient,
|
||||
pushNotificationManager,
|
||||
verifyExperimentEnrollmentManager,
|
||||
experimentEnrollmentManager,
|
||||
changeNumberManager);
|
||||
|
||||
clearInvocations(AuthHelper.DISABLED_DEVICE);
|
||||
|
@ -455,7 +440,7 @@ class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.readEntity(String.class)).isBlank();
|
||||
|
||||
verifyNoMoreInteractions(pushNotificationManager);
|
||||
verifyNoInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -473,59 +458,17 @@ class AccountControllerTest {
|
|||
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
|
||||
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
|
||||
|
||||
verifyNoMoreInteractions(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));
|
||||
verifyNoInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendCodeViaRegistrationService() throws NumberParseException {
|
||||
void testSendCode() throws NumberParseException {
|
||||
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(sessionId));
|
||||
|
||||
when(experimentEnrollmentManager.isEnrolled(SENDER, AccountController.REGISTRATION_SERVICE_EXPERIMENT_NAME))
|
||||
.thenReturn(true);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
|
@ -543,8 +486,6 @@ class AccountControllerTest {
|
|||
verify(pendingAccountsManager).store(eq(SENDER), argThat(
|
||||
storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) &&
|
||||
"1234-push".equals(storedVerificationCode.pushCode())));
|
||||
|
||||
verifyNoInteractions(smsSender);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -560,7 +501,7 @@ class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.readEntity(String.class)).isBlank();
|
||||
|
||||
verify(smsSender, never()).deliverSmsVerification(any(), any(), any());
|
||||
verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -581,20 +522,14 @@ class AccountControllerTest {
|
|||
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
|
||||
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
|
||||
|
||||
verify(smsSender, never()).deliverSmsVerification(any(), any(), any());
|
||||
verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testSendCodeVoiceNoLocale(final boolean enrolledInVerifyExperiment) throws Exception {
|
||||
@Test
|
||||
public void testSendCodeVoiceNoLocale() throws NumberParseException {
|
||||
|
||||
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
|
||||
.thenReturn(enrolledInVerifyExperiment);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
|
||||
}
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -604,122 +539,18 @@ class AccountControllerTest {
|
|||
.header("X-Forwarded-For", NICE_HOST)
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Collections.emptyList()));
|
||||
}
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.VOICE, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testSendCodeVoiceSingleLocale(final boolean enrolledInVerifyExperiment) throws Exception {
|
||||
@Test
|
||||
void testSendCodeWithValidPreauth() throws NumberParseException {
|
||||
|
||||
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", "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")));
|
||||
}
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -731,17 +562,14 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(),
|
||||
eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString());
|
||||
}
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER_PREAUTH, null);
|
||||
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendCodeWithInvalidPreauth() throws Exception {
|
||||
void testSendCodeWithInvalidPreauth() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH))
|
||||
|
@ -752,12 +580,12 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
verifyNoInteractions(abusiveHostRules);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendCodeWithNoPreauth() throws Exception {
|
||||
void testSendCodeWithNoPreauth() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH))
|
||||
|
@ -767,22 +595,14 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(402);
|
||||
|
||||
verify(smsSender, never()).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString());
|
||||
verify(smsSender, never()).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(), anyList());
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendiOSCode() throws NumberParseException {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
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")));
|
||||
}
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -795,25 +615,15 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("ios")), anyString(),
|
||||
eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("ios")), anyString());
|
||||
}
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
|
||||
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.IOS, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendAndroidNgCode(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")));
|
||||
}
|
||||
@Test
|
||||
void testSendAndroidNgCode() throws NumberParseException {
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -826,25 +636,13 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("android-ng")), anyString(),
|
||||
eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("android-ng")), anyString());
|
||||
}
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
|
||||
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.ANDROID_WITHOUT_FCM, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
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")));
|
||||
}
|
||||
@Test
|
||||
void testSendAbusiveHost() {
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -857,20 +655,14 @@ class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(402);
|
||||
|
||||
verify(abusiveHostRules).isBlocked(eq(ABUSIVE_HOST));
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendAbusiveHostWithValidCaptcha(final boolean enrolledInVerifyExperiment) throws IOException {
|
||||
@Test
|
||||
void testSendAbusiveHostWithValidCaptcha() throws NumberParseException {
|
||||
|
||||
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
|
||||
.thenReturn(enrolledInVerifyExperiment);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid")));
|
||||
}
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -882,27 +674,16 @@ class AccountControllerTest {
|
|||
|
||||
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));
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(),
|
||||
eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
|
||||
}
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendAbusiveHostWithInvalidCaptcha(final boolean enrolledInVerifyExperiment) {
|
||||
@Test
|
||||
void testSendAbusiveHostWithInvalidCaptcha() {
|
||||
|
||||
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))
|
||||
|
@ -913,23 +694,13 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(402);
|
||||
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
verifyNoInteractions(abusiveHostRules);
|
||||
verify(recaptchaClient).verify(eq(INVALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST));
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
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")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendRateLimitedHostAutoBlock() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
|
@ -944,21 +715,12 @@ class AccountControllerTest {
|
|||
verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_IP_HOST));
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
|
||||
verifyNoMoreInteractions(recaptchaClient);
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(recaptchaClient);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
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")));
|
||||
}
|
||||
@Test
|
||||
void testSendRateLimitedPrefixAutoBlock() {
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -974,21 +736,12 @@ class AccountControllerTest {
|
|||
verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_PREFIX_HOST));
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
|
||||
verifyNoMoreInteractions(recaptchaClient);
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(recaptchaClient);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
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")));
|
||||
}
|
||||
@Test
|
||||
void testSendRateLimitedHostNoAutoBlock() {
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -1003,17 +756,13 @@ class AccountControllerTest {
|
|||
verify(abusiveHostRules).isBlocked(eq(RATE_LIMITED_HOST2));
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
|
||||
verifyNoMoreInteractions(recaptchaClient);
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(recaptchaClient);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendMultipleHost(final boolean enrolledInVerifyExperiment) {
|
||||
|
||||
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
|
||||
.thenReturn(enrolledInVerifyExperiment);
|
||||
@Test
|
||||
void testSendMultipleHost() {
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -1028,16 +777,12 @@ class AccountControllerTest {
|
|||
verify(abusiveHostRules, times(1)).isBlocked(eq(ABUSIVE_HOST));
|
||||
|
||||
verifyNoMoreInteractions(abusiveHostRules);
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendRestrictedHostOut(final boolean enrolledInVerifyExperiment) {
|
||||
|
||||
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
|
||||
.thenReturn(enrolledInVerifyExperiment);
|
||||
@Test
|
||||
void testSendRestrictedHostOut() {
|
||||
|
||||
final String challenge = "challenge";
|
||||
when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER))
|
||||
|
@ -1054,17 +799,15 @@ class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(402);
|
||||
|
||||
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"+12025550123, true, true",
|
||||
"+12025550123, false, true",
|
||||
"+12505550199, true, false",
|
||||
"+12505550199, false, false",
|
||||
"+12025550123, true",
|
||||
"+12505550199, 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);
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
|
||||
|
@ -1073,15 +816,12 @@ class AccountControllerTest {
|
|||
|
||||
when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig);
|
||||
|
||||
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
|
||||
.thenReturn(enrolledInVerifyExperiment);
|
||||
|
||||
final String challenge = "challenge";
|
||||
when(pendingAccountsManager.getCodeForNumber(number))
|
||||
.thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
|
||||
|
||||
when(smsSender.deliverSmsVerificationWithTwilioVerify(any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
|
@ -1094,31 +834,22 @@ class AccountControllerTest {
|
|||
if (expectSendCode) {
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(number), any(), any(), any());
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(number), any(), any());
|
||||
}
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(number, null);
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
} else {
|
||||
assertThat(response.getStatus()).isEqualTo(402);
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void testSendRestrictedIn(final boolean enrolledInVerifyExperiment) throws Exception {
|
||||
@Test
|
||||
void testSendRestrictedIn() throws NumberParseException {
|
||||
|
||||
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";
|
||||
|
||||
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 =
|
||||
resources.getJerseyTest()
|
||||
|
@ -1130,40 +861,38 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(),
|
||||
eq(Collections.emptyList()));
|
||||
} else {
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
|
||||
}
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
|
||||
|
||||
verifyNoMoreInteractions(smsSender);
|
||||
verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendCodeTestDeviceNumber() throws Exception {
|
||||
// no push code and a blocked host, but should evade captchas and skip smsSender
|
||||
void testSendCodeTestDeviceNumber() throws NumberParseException {
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(sessionId));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", TEST_NUMBER))
|
||||
.request()
|
||||
.header("X-Forwarded-For", ABUSIVE_HOST)
|
||||
.get();
|
||||
ArgumentCaptor<StoredVerificationCode> captor = ArgumentCaptor.forClass(StoredVerificationCode.class);
|
||||
|
||||
final ArgumentCaptor<StoredVerificationCode> captor = ArgumentCaptor.forClass(StoredVerificationCode.class);
|
||||
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);
|
||||
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
|
||||
@ValueSource(booleans = {false, true})
|
||||
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)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyCode() throws Exception {
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "1234"))
|
||||
.request()
|
||||
|
@ -1171,11 +900,6 @@ class AccountControllerTest {
|
|||
.put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
|
||||
|
||||
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).reportVerificationSucceeded(eq("VerificationSid"), any(), eq("registration"));
|
||||
}
|
||||
|
||||
verifyNoInteractions(registrationServiceClient);
|
||||
}
|
||||
|
||||
|
@ -1199,7 +923,6 @@ class AccountControllerTest {
|
|||
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
|
||||
|
||||
verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
verifyNoInteractions(smsSender);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1225,7 +948,7 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
verifyNoInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1240,7 +963,7 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
verifyNoInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1266,7 +989,6 @@ class AccountControllerTest {
|
|||
|
||||
verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
verifyNoInteractions(accountsManager);
|
||||
verifyNoInteractions(smsSender);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1320,7 +1042,7 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(result.getUuid()).isNotNull();
|
||||
|
||||
verifyNoMoreInteractions(pinLimiter);
|
||||
verifyNoInteractions(pinLimiter);
|
||||
} finally {
|
||||
when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock);
|
||||
}
|
||||
|
@ -1361,7 +1083,7 @@ class AccountControllerTest {
|
|||
assertThat(failure.getBackupCredentials().getPassword().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue();
|
||||
assertThat(failure.getTimeRemaining()).isGreaterThan(0);
|
||||
|
||||
verifyNoMoreInteractions(pinLimiter);
|
||||
verifyNoInteractions(pinLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1411,55 +1133,14 @@ class AccountControllerTest {
|
|||
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
|
||||
void testChangePhoneNumber() throws Exception {
|
||||
final String number = "+18005559876";
|
||||
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);
|
||||
|
||||
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()))
|
||||
.thenReturn(CompletableFuture.completedFuture(true));
|
||||
|
@ -1557,26 +1238,6 @@ class AccountControllerTest {
|
|||
void testChangePhoneNumberIncorrectCode() throws Exception {
|
||||
final String number = "+18005559876";
|
||||
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);
|
||||
|
||||
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
|
||||
|
@ -1603,9 +1264,13 @@ class AccountControllerTest {
|
|||
void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception {
|
||||
final String number = "+18005559876";
|
||||
final String code = "987654";
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
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);
|
||||
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(false);
|
||||
|
@ -1633,9 +1298,13 @@ class AccountControllerTest {
|
|||
void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception {
|
||||
final String number = "+18005559876";
|
||||
final String code = "987654";
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
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);
|
||||
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
|
||||
|
@ -1664,9 +1333,13 @@ class AccountControllerTest {
|
|||
final String number = "+18005559876";
|
||||
final String code = "987654";
|
||||
final String reglock = "setec-astronomy";
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
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);
|
||||
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
|
||||
|
@ -1696,9 +1369,13 @@ class AccountControllerTest {
|
|||
final String number = "+18005559876";
|
||||
final String code = "987654";
|
||||
final String reglock = "setec-astronomy";
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
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);
|
||||
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
|
||||
|
@ -1728,6 +1405,7 @@ class AccountControllerTest {
|
|||
final String number = "+18005559876";
|
||||
final String code = "987654";
|
||||
final String pniIdentityKey = "changed-pni-identity-key";
|
||||
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Device device2 = mock(Device.class);
|
||||
when(device2.getId()).thenReturn(2L);
|
||||
|
@ -1744,7 +1422,10 @@ class AccountControllerTest {
|
|||
when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3));
|
||||
|
||||
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(
|
||||
new IncomingMessage(1, 2, 2, "content2"),
|
||||
|
@ -2104,7 +1785,8 @@ class AccountControllerTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@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);
|
||||
when(dynamicConfigurationManager.getConfiguration())
|
||||
.thenReturn(dynamicConfiguration);
|
||||
|
@ -2114,6 +1796,9 @@ class AccountControllerTest {
|
|||
when(dynamicConfiguration.getCaptchaConfiguration())
|
||||
.thenReturn(signupCaptchaConfig);
|
||||
|
||||
when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(new byte[16]));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
|
@ -2124,8 +1809,10 @@ class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode);
|
||||
|
||||
verify(smsSender, 200 == expectedResponseStatusCode ? times(1) : never())
|
||||
.deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
|
||||
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
|
||||
|
||||
verify(registrationServiceClient, 200 == expectedResponseStatusCode ? times(1) : never())
|
||||
.sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
|
||||
}
|
||||
|
||||
static Stream<Arguments> testSignupCaptcha() {
|
||||
|
|
|
@ -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 doesn’t 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)
|
||||
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
}
|
|
@ -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(), "");
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue