implement /v2/config API (#2764)

This commit is contained in:
Jonathan Klabunde Tomer
2025-07-16 15:29:31 -07:00
committed by GitHub
parent 6116830da9
commit 5c21aa2ad4
6 changed files with 570 additions and 150 deletions

View File

@@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
@@ -16,27 +17,27 @@ import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.data.Offset;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.params.IntRangeSource;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.entities.RemoteConfigurationResponse;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
@@ -63,20 +64,18 @@ class RemoteConfigControllerTest {
@BeforeEach
void setup() throws Exception {
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null,
null, null));
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
}});
when(remoteConfigsManager.getAll()).thenReturn(
List.of(
new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null, null, null),
new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null),
new RemoteConfig("always.true", 100, Set.of(), null, null, null),
new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null),
new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null),
new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null),
new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null),
new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null),
new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"),
new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null)));
}
@AfterEach
@@ -86,117 +85,72 @@ class RemoteConfigControllerTest {
@Test
void testRetrieveConfig() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
RemoteConfigurationResponse configuration = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(UserRemoteConfigList.class);
.get(RemoteConfigurationResponse.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
}
@Test
void testServerEpochTime() {
Object serverEpochTime = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Map.class)
.get("serverEpochTime");
assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat))
.extracting(Number::longValue)
.isEqualTo(PINNED_EPOCH_SECONDS);
assertThat(configuration.config()).hasSize(11);
assertThat(configuration.config()).containsKeys("android.stickers", "ios.stickers", "linked.config.0", "linked.config.1", "unlinked.config");
assertThat(configuration.config()).contains(
entry("always.true", "true"),
entry("only.special", "true"),
entry("value.always.true", "bar"),
entry("value.only.special", "xyz"),
entry("value.always.false", "red"),
entry("global.maxGroupSize", "42"));
}
@Test
void testRetrieveConfigNotSpecial() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
RemoteConfigurationResponse configuration = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
.get(UserRemoteConfigList.class);
.get(RemoteConfigurationResponse.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
assertThat(configuration.config()).hasSize(11);
assertThat(configuration.config()).containsKeys("android.stickers", "ios.stickers", "linked.config.0", "linked.config.1", "unlinked.config");
assertThat(configuration.config()).contains(
entry("always.true", "true"),
entry("only.special", "false"),
entry("value.always.true", "bar"),
entry("value.only.special", "abc"),
entry("value.always.false", "red"),
entry("global.maxGroupSize", "42"));
}
@Test
void testHashKeyLinkedConfigs() {
boolean allUnlinkedConfigsMatched = true;
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
RemoteConfigurationResponse configuration = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", testAccount.getAuthHeader())
.get(UserRemoteConfigList.class);
.get(RemoteConfigurationResponse.class);
assertThat(configuration.getConfig()).hasSize(11);
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
assertThat(configuration.config().get("linked.config.0")).isEqualTo(configuration.config().get("linked.config.1"));
allUnlinkedConfigsMatched &= (configuration.config().get("linked.config.0") == configuration.config().get("unlinked.config"));
}
// with 20 test accounts, 1 in 2^20 chance that this fails when it shouldn't, but
// AuthHelper#generateTestAccounts uses a constant random seed that doesn't fail as of the time
// of this writing; if this starts failing for no apparent reason, it's likely that we've
// changed the order of the sequence of random numbers used during test initialization in such
// a way that we've accidentally picked an unlucky set of accounts here
assertThat(allUnlinkedConfigsMatched).isFalse();
}
@Test
void testRetrieveConfigUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config/")
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
.get();
@@ -207,32 +161,101 @@ class RemoteConfigControllerTest {
}
@Test
void testMath() throws NoSuchAlgorithmException {
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
Map<String, Integer> enabledMap = new HashMap<>();
MessageDigest digest = MessageDigest.getInstance("SHA1");
int iterations = 100000;
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
void testRetrieveConfigUnchanged() {
Response response = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header("User-Agent", "Signal-Android/7.6.2 Android/34 libsignal/0.46.0")
.get();
for (int i=0;i<iterations;i++) {
for (RemoteConfig config : remoteConfigList) {
int count = enabledMap.getOrDefault(config.getName(), 0);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getLength()).isNotEqualTo(0);
final EntityTag etag = response.getEntityTag();
assertThat(etag).isNotNull();
if (RemoteConfigController.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
count++;
}
response = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header("User-Agent", "Signal-Android/7.6.2 Android/34 libsignal/0.46.0")
.header("If-None-Match", etag)
.get();
enabledMap.put(config.getName(), count);
assertThat(response.getStatus()).isEqualTo(304);
assertThat(response.getLength()).isLessThanOrEqualTo(0); // could be -1 for absent or explicit 0
}
@Test
void testRetrieveConfigChanged() {
Response response = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, AuthHelper.VALID_PASSWORD_3_PRIMARY))
.header("User-Agent", "Signal-Android/7.6.2 Android/34 libsignal/0.46.0")
.get();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getLength()).isNotEqualTo(0);
final EntityTag etag = response.getEntityTag();
assertThat(etag).isNotNull();
final List<RemoteConfig> configs = new ArrayList<>(remoteConfigsManager.getAll());
configs.add(new RemoteConfig("android.new.config", 100, Set.of(), null, null, null));
when(remoteConfigsManager.getAll()).thenReturn(configs);
response = resources.getJerseyTest()
.target("/v2/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header("User-Agent", "Signal-Android/7.6.2 Android/34 libsignal/0.46.0")
.header("If-None-Match", etag)
.get();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getLength()).isNotEqualTo(0);
}
@ParameterizedTest
@IntRangeSource(from = 1, to = 99)
void testMath(int percentage) throws NoSuchAlgorithmException {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
final int iterations = 10000;
int enabledCount = 0;
for (int i = 0; i < iterations; i++) {
if (RemoteConfigController.isInBucket(digest, AuthHelper.getRandomUUID(random), "test".getBytes(), percentage, Set.of())) {
enabledCount++;
}
}
for (RemoteConfig config : remoteConfigList) {
double targetNumber = iterations * (config.getPercentage() / 100.0);
double variance = targetNumber * 0.01;
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
(int) (targetNumber + variance));
}
// https://en.wikipedia.org/wiki/Binomial_distribution#Expected_value_and_variance
final double expectedCount = iterations * percentage / 100.0;
final double stdev = Math.sqrt(expectedCount * (1 - percentage / 100.0));
// 3 standard deviations = 99.73% chance of success for one bucket, 23.5%
// chance of any failure in 99 buckets; if this starts failing after a
// change, run it again with a few different random seeds to make sure it
// fails only about on about one seed in four
assertThat((double) enabledCount).isCloseTo(expectedCount, Offset.offset(3 * stdev));
}
@ParameterizedTest
@ValueSource(ints = {0, 100})
void testMathExactForZeroOrOneHundred(int percentage) throws NoSuchAlgorithmException {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final Random random = new Random();
final int iterations = 10000;
int enabledCount = 0;
for (int i = 0; i < iterations; i++) {
if (RemoteConfigController.isInBucket(digest, AuthHelper.getRandomUUID(random), "test".getBytes(), percentage, Set.of())) {
enabledCount++;
}
}
assertThat(enabledCount).isEqualTo(iterations * percentage / 100);
}
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import jakarta.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.TestClock;
@ExtendWith(DropwizardExtensionsSupport.class)
class RemoteConfigControllerV1Test {
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
private static final long PINNED_EPOCH_SECONDS = 1701287216L;
private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS));
private static final ResourceExtension resources = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper())
.addResource(new RemoteConfigControllerV1(remoteConfigsManager, Map.of("maxGroupSize", "42"), TEST_CLOCK))
.build();
@BeforeEach
void setup() throws Exception {
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null,
null, null));
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
}});
}
@AfterEach
void teardown() {
reset(remoteConfigsManager);
}
@Test
void testRetrieveConfig() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(UserRemoteConfigList.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
}
@Test
void testServerEpochTime() {
Object serverEpochTime = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Map.class)
.get("serverEpochTime");
assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat))
.extracting(Number::longValue)
.isEqualTo(PINNED_EPOCH_SECONDS);
}
@Test
void testRetrieveConfigNotSpecial() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
.get(UserRemoteConfigList.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
}
@Test
void testHashKeyLinkedConfigs() {
boolean allUnlinkedConfigsMatched = true;
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", testAccount.getAuthHeader())
.get(UserRemoteConfigList.class);
assertThat(configuration.getConfig()).hasSize(11);
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
}
assertThat(allUnlinkedConfigsMatched).isFalse();
}
@Test
void testRetrieveConfigUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
void testMath() throws NoSuchAlgorithmException {
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
Map<String, Integer> enabledMap = new HashMap<>();
MessageDigest digest = MessageDigest.getInstance("SHA256");
int iterations = 100000;
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
for (int i=0;i<iterations;i++) {
for (RemoteConfig config : remoteConfigList) {
int count = enabledMap.getOrDefault(config.getName(), 0);
if (RemoteConfigControllerV1.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
count++;
}
enabledMap.put(config.getName(), count);
}
}
for (RemoteConfig config : remoteConfigList) {
double targetNumber = iterations * (config.getPercentage() / 100.0);
double variance = targetNumber * 0.01;
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
(int) (targetNumber + variance));
}
}
}