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:
parent
9d77f8dcd2
commit
08a70664f4
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user