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