0
0
mirror of https://github.com/signalapp/Signal-Server.git synced 2024-09-20 12:02:18 +02:00

Support for getting/setting remote config variables

This commit is contained in:
Moxie Marlinspike 2019-12-13 14:57:57 -08:00
parent 9d77f8dcd2
commit 08a70664f4
14 changed files with 722 additions and 0 deletions

View File

@ -171,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private ZkConfig zkConfig;
@Valid
@NotNull
@JsonProperty
private RemoteConfigConfiguration remoteConfig;
private Map<String, String> transparentDataIndex = new HashMap<>();
public RecaptchaConfiguration getRecaptchaConfiguration() {
@ -299,4 +304,7 @@ public class WhisperServerConfiguration extends Configuration {
return zkConfig;
}
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
return remoteConfig;
}
}

View File

@ -179,6 +179,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
Keys keys = new Keys(keysDatabase);
Messages messages = new Messages(messageDatabase);
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
RemoteConfigs remoteConfigs = new RemoteConfigs(accountDatabase);
RedisClientFactory cacheClientFactory = new RedisClientFactory("main_cache", config.getCacheConfiguration().getUrl(), config.getCacheConfiguration().getReplicaUrls(), config.getCacheConfiguration().getCircuitBreakerConfiguration());
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
@ -199,6 +200,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.of(deadLetterHandler));
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
@ -245,6 +247,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(pushSender);
environment.lifecycle().manage(messagesCache);
environment.lifecycle().manage(accountDatabaseCrawler);
environment.lifecycle().manage(remoteConfigsManager);
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
@ -263,6 +266,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, cdnPolicyGenerator, cdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled);
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
RemoteConfigController remoteConfigController = new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens());
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
@ -285,6 +289,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(messageController);
environment.jersey().register(profileController);
environment.jersey().register(stickerController);
environment.jersey().register(remoteConfigController);
///
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config.getWebSocketConfiguration(), 90000);
@ -295,6 +300,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.jersey().register(profileController);
webSocketEnvironment.jersey().register(attachmentControllerV1);
webSocketEnvironment.jersey().register(attachmentControllerV2);
webSocketEnvironment.jersey().register(remoteConfigController);
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, webSocketEnvironment.getRequestLog(), 60000);
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));

View File

@ -0,0 +1,18 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import java.util.LinkedList;
import java.util.List;
public class RemoteConfigConfiguration {
@JsonProperty
@NotNull
private List<String> authorizedTokens = new LinkedList<>();
public List<String> getAuthorizedTokens() {
return authorizedTokens;
}
}

View File

@ -0,0 +1,99 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.util.Conversions;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.stream.Collectors;
import io.dropwizard.auth.Auth;
@Path("/v1/config")
public class RemoteConfigController {
private final RemoteConfigsManager remoteConfigsManager;
private final List<String> configAuthTokens;
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, List<String> configAuthTokens) {
this.remoteConfigsManager = remoteConfigsManager;
this.configAuthTokens = configAuthTokens;
}
@Timed
@GET
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public UserRemoteConfigList getAll(@Auth Account account) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] number = account.getNumber().getBytes();
return new UserRemoteConfigList(remoteConfigsManager.getAll().stream().map(config -> new UserRemoteConfig(config.getName(),
isInBucket(digest, number,
config.getName().getBytes(),
config.getPercentage())))
.collect(Collectors.toList()));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void set(@HeaderParam("Config-Token") String configToken, @Valid RemoteConfig config) {
if (!isAuthorized(configToken)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
remoteConfigsManager.set(config);
}
@Timed
@DELETE
@Path("/{name}")
public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) {
if (!isAuthorized(configToken)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
remoteConfigsManager.delete(name);
}
@VisibleForTesting
public static boolean isInBucket(MessageDigest digest, byte[] user, byte[] configName, int configPercentage) {
digest.update(user);
byte[] hash = digest.digest(configName);
int bucket = (int)(Math.abs(Conversions.byteArrayToLong(hash)) % 100);
return bucket < configPercentage;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isAuthorized(String configToken) {
return configAuthTokens.stream().anyMatch(authorized -> MessageDigest.isEqual(authorized.getBytes(), configToken.getBytes()));
}
}

View File

@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class UserRemoteConfig {
@JsonProperty
private String name;
@JsonProperty
private boolean enabled;
public UserRemoteConfig() {}
public UserRemoteConfig(String name, boolean enabled) {
this.name = name;
this.enabled = enabled;
}
public String getName() {
return name;
}
public boolean isEnabled() {
return enabled;
}
}

View File

@ -0,0 +1,21 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class UserRemoteConfigList {
@JsonProperty
private List<UserRemoteConfig> config;
public UserRemoteConfigList() {}
public UserRemoteConfigList(List<UserRemoteConfig> config) {
this.config = config;
}
public List<UserRemoteConfig> getConfig() {
return config;
}
}

View File

@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class RemoteConfig {
@JsonProperty
@Pattern(regexp = "[A-Za-z0-9\\.]+")
private String name;
@JsonProperty
@NotNull
@Min(0)
@Max(100)
private int percentage;
public RemoteConfig() {}
public RemoteConfig(String name, int percentage) {
this.name = name;
this.percentage = percentage;
}
public int getPercentage() {
return percentage;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,70 @@
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jdbi.v3.core.transaction.TransactionIsolationLevel;
import org.whispersystems.textsecuregcm.storage.mappers.AccountRowMapper;
import org.whispersystems.textsecuregcm.storage.mappers.RemoteConfigRowMapper;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.codahale.metrics.MetricRegistry.name;
public class RemoteConfigs {
public static final String ID = "id";
public static final String NAME = "name";
public static final String PERCENTAGE = "percentage";
private static final ObjectMapper mapper = SystemMapper.getMapper();
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Timer setTimer = metricRegistry.timer(name(Accounts.class, "set" ));
private final Timer getAllTimer = metricRegistry.timer(name(Accounts.class, "getAll"));
private final Timer deleteTimer = metricRegistry.timer(name(Accounts.class, "delete"));
private final FaultTolerantDatabase database;
public RemoteConfigs(FaultTolerantDatabase database) {
this.database = database;
this.database.getDatabase().registerRowMapper(new RemoteConfigRowMapper());
}
public void set(RemoteConfig remoteConfig) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = setTimer.time()) {
handle.createUpdate("INSERT INTO remote_config (" + NAME + ", " + PERCENTAGE + ") VALUES (:name, :percentage) ON CONFLICT(" + NAME + ") DO UPDATE SET " + PERCENTAGE + " = EXCLUDED." + PERCENTAGE)
.bind("name", remoteConfig.getName())
.bind("percentage", remoteConfig.getPercentage())
.execute();
}
}));
}
public List<RemoteConfig> getAll() {
return database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context ignored = getAllTimer.time()) {
return handle.createQuery("SELECT * FROM remote_config")
.mapTo(RemoteConfig.class)
.list();
}
}));
}
public void delete(String name) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = deleteTimer.time()) {
handle.createUpdate("DELETE FROM remote_config WHERE " + NAME + " = :name")
.bind("name", name)
.execute();
}
}));
}
}

View File

@ -0,0 +1,67 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import io.dropwizard.lifecycle.Managed;
public class RemoteConfigsManager implements Managed {
private final Logger logger = LoggerFactory.getLogger(RemoteConfigsManager.class);
private final RemoteConfigs remoteConfigs;
private final long sleepInterval;
private AtomicReference<List<RemoteConfig>> cachedConfigs = new AtomicReference<>(new LinkedList<>());
public RemoteConfigsManager(RemoteConfigs remoteConfigs) {
this(remoteConfigs, TimeUnit.SECONDS.toMillis(10));
}
@VisibleForTesting
public RemoteConfigsManager(RemoteConfigs remoteConfigs, long sleepInterval) {
this.remoteConfigs = remoteConfigs;
this.sleepInterval = sleepInterval;
}
@Override
public void start() {
this.cachedConfigs.set(remoteConfigs.getAll());
new Thread(() -> {
while (true) {
try {
this.cachedConfigs.set(remoteConfigs.getAll());
} catch (Throwable t) {
logger.warn("Error updating remote configs cache", t);
}
Util.sleep(sleepInterval);
}
}).start();
}
public List<RemoteConfig> getAll() {
return cachedConfigs.get();
}
public void set(RemoteConfig config) {
remoteConfigs.set(config);
}
public void delete(String name) {
remoteConfigs.delete(name);
}
@Override
public void stop() throws Exception {
}
}

View File

@ -0,0 +1,18 @@
package org.whispersystems.textsecuregcm.storage.mappers;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
import java.sql.ResultSet;
import java.sql.SQLException;
public class RemoteConfigRowMapper implements RowMapper<RemoteConfig> {
@Override
public RemoteConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
return new RemoteConfig(rs.getString(RemoteConfigs.NAME), rs.getInt(RemoteConfigs.PERCENTAGE));
}
}

View File

@ -273,4 +273,24 @@
</createIndex>
</changeSet>
<changeSet id="12" author="moxie">
<createTable tableName="remote_config">
<column name="id" type="bigint" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="text">
<constraints nullable="false" unique="true"/>
</column>
<column name="percentage" type="int">
<constraints nullable="false"/>
</column>
<column name="uuids" type="text []">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,207 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.Mockito.*;
public class RemoteConfigControllerTest {
private RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
private List<String> remoteConfigsAuth = new LinkedList<>() {{
add("foo");
add("bar");
}};
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper())
.addResource(new RemoteConfigController(remoteConfigsManager, remoteConfigsAuth))
.build();
@Before
public void setup() throws Exception {
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
add(new RemoteConfig("android.stickers", 25));
add(new RemoteConfig("ios.stickers", 50));
add(new RemoteConfig("always.true", 100));
}});
}
@Test
public void testRetrieveConfig() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(UserRemoteConfigList.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig().size()).isEqualTo(3);
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);
}
@Test
public void testRetrieveConfigUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testSetConfig() {
Response response = resources.getJerseyTest()
.target("/v1/config")
.request()
.header("Config-Token", "foo")
.put(Entity.entity(new RemoteConfig("android.stickers", 88), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(204);
ArgumentCaptor<RemoteConfig> captor = ArgumentCaptor.forClass(RemoteConfig.class);
verify(remoteConfigsManager, times(1)).set(captor.capture());
assertThat(captor.getValue().getName()).isEqualTo("android.stickers");
assertThat(captor.getValue().getPercentage()).isEqualTo(88);
}
@Test
public void testSetConfigUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config")
.request()
.header("Config-Token", "baz")
.put(Entity.entity(new RemoteConfig("android.stickers", 88), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testSetConfigBadName() {
Response response = resources.getJerseyTest()
.target("/v1/config")
.request()
.header("Config-Token", "foo")
.put(Entity.entity(new RemoteConfig("android-stickers", 88), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testSetConfigEmptyName() {
Response response = resources.getJerseyTest()
.target("/v1/config")
.request()
.header("Config-Token", "foo")
.put(Entity.entity(new RemoteConfig("", 88), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(422);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testDelete() {
Response response = resources.getJerseyTest()
.target("/v1/config/android.stickers")
.request()
.header("Config-Token", "foo")
.delete();
assertThat(response.getStatus()).isEqualTo(204);
verify(remoteConfigsManager, times(1)).delete("android.stickers");
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testDeleteUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config/android.stickers")
.request()
.header("Config-Token", "baz")
.delete();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
public void testMath() throws NoSuchAlgorithmException {
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
Map<String, Integer> enabledMap = new HashMap<>();
MessageDigest digest = MessageDigest.getInstance("SHA1");
int iterations = 100000;
for (int i=0;i<iterations;i++) {
for (RemoteConfig config : remoteConfigList) {
int count = enabledMap.getOrDefault(config.getName(), 0);
int random = new SecureRandom().nextInt(iterations);
if (RemoteConfigController.isInBucket(digest, ("+121322" + String.format("%05d", random)).getBytes(), config.getName().getBytes(), config.getPercentage())) {
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));
}
}
}

View File

@ -0,0 +1,52 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
import com.opentable.db.postgres.junit.PreparedDbRule;
import org.jdbi.v3.core.Jdbi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import java.util.List;
import static org.assertj.core.api.Java6Assertions.assertThat;
public class RemoteConfigsManagerTest {
@Rule
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private RemoteConfigsManager remoteConfigs;
@Before
public void setup() {
RemoteConfigs remoteConfigs = new RemoteConfigs(new FaultTolerantDatabase("remote_configs-test", Jdbi.create(db.getTestDatabase()), new CircuitBreakerConfiguration()));
this.remoteConfigs = new RemoteConfigsManager(remoteConfigs, 500);
this.remoteConfigs.start();
}
@Test
public void testUpdate() throws InterruptedException {
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
Thread.sleep(501);
List<RemoteConfig> results = remoteConfigs.getAll();
assertThat(results.size()).isEqualTo(2);
assertThat(results.get(0).getName()).isEqualTo("android.stickers");
assertThat(results.get(0).getPercentage()).isEqualTo(50);
assertThat(results.get(1).getName()).isEqualTo("ios.stickers");
assertThat(results.get(1).getPercentage()).isEqualTo(75);
}
}

View File

@ -0,0 +1,73 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
import com.opentable.db.postgres.junit.PreparedDbRule;
import org.jdbi.v3.core.Jdbi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
import java.sql.SQLException;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class RemoteConfigsTest {
@Rule
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private RemoteConfigs remoteConfigs;
@Before
public void setup() {
this.remoteConfigs = new RemoteConfigs(new FaultTolerantDatabase("remote_configs-test", Jdbi.create(db.getTestDatabase()), new CircuitBreakerConfiguration()));
}
@Test
public void testStore() throws SQLException {
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
List<RemoteConfig> configs = remoteConfigs.getAll();
assertThat(configs.size()).isEqualTo(1);
assertThat(configs.get(0).getName()).isEqualTo("android.stickers");
assertThat(configs.get(0).getPercentage()).isEqualTo(50);
}
@Test
public void testUpdate() throws SQLException {
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
List<RemoteConfig> configs = remoteConfigs.getAll();
assertThat(configs.size()).isEqualTo(2);
assertThat(configs.get(0).getName()).isEqualTo("android.stickers");
assertThat(configs.get(0).getPercentage()).isEqualTo(50);
assertThat(configs.get(1).getName()).isEqualTo("ios.stickers");
assertThat(configs.get(1).getPercentage()).isEqualTo(75);
}
@Test
public void testDelete() {
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
remoteConfigs.delete("android.stickers");
List<RemoteConfig> configs = remoteConfigs.getAll();
assertThat(configs.size()).isEqualTo(1);
assertThat(configs.get(0).getName()).isEqualTo("ios.stickers");
assertThat(configs.get(0).getPercentage()).isEqualTo(75);
}
}