Add TwilioVerifyExperimentEnrollmentManager

This commit is contained in:
Chris Eager 2021-03-24 17:24:19 -05:00 committed by Chris Eager
parent f68390e96f
commit bab5e5769b
2 changed files with 163 additions and 0 deletions

View File

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

View File

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