Add TwilioVerifyExperimentEnrollmentManager
This commit is contained in:
parent
f68390e96f
commit
bab5e5769b
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 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)
|
||||
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue