Contact Discovery Service

This commit is contained in:
Brian Acton
2018-09-18 11:17:12 -07:00
parent 15cf010e44
commit 10575d80ad
30 changed files with 1439 additions and 148 deletions

View File

@@ -18,6 +18,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sqs.ContactDiscoveryQueueSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
@@ -49,6 +50,7 @@ public class AccountControllerTest {
private RateLimiter rateLimiter = mock(RateLimiter.class );
private RateLimiter pinLimiter = mock(RateLimiter.class );
private SmsSender smsSender = mock(SmsSender.class );
private ContactDiscoveryQueueSender cdsSender = mock(ContactDiscoveryQueueSender.class);
private MessagesManager storedMessages = mock(MessagesManager.class );
private TimeProvider timeProvider = mock(TimeProvider.class );
private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
@@ -65,6 +67,7 @@ public class AccountControllerTest {
accountsManager,
rateLimiters,
smsSender,
cdsSender,
storedMessages,
turnTokenGenerator,
new HashMap<>()))
@@ -137,6 +140,7 @@ public class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager, times(1)).create(isA(Account.class));
verify(cdsSender, times(1)).addRegisteredUser(eq(SENDER));
}
@Test
@@ -282,4 +286,4 @@ public class AccountControllerTest {
}
}

View File

@@ -29,10 +29,8 @@ import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.sqs.ContactDiscoveryQueueSender;
import org.whispersystems.textsecuregcm.storage.*;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -55,10 +53,11 @@ public class DeviceControllerTest {
public DumbVerificationDeviceController(PendingDevicesManager pendingDevices,
AccountsManager accounts,
MessagesManager messages,
ContactDiscoveryQueueSender cdsSender,
RateLimiters rateLimiters,
Map<String, Integer> deviceConfiguration)
{
super(pendingDevices, accounts, messages, rateLimiters, deviceConfiguration);
super(pendingDevices, accounts, messages, cdsSender, rateLimiters, deviceConfiguration);
}
@Override
@@ -70,10 +69,12 @@ public class DeviceControllerTest {
private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private MessagesManager messagesManager = mock(MessagesManager.class);
private ContactDiscoveryQueueSender cdsSender = mock(ContactDiscoveryQueueSender.class);
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
private Account account = mock(Account.class );
private Account maxedAccount = mock(Account.class);
private Device masterDevice = mock(Device.class);
private Map<String, Integer> deviceConfiguration = new HashMap<String, Integer>() {{
@@ -88,6 +89,7 @@ public class DeviceControllerTest {
.addResource(new DumbVerificationDeviceController(pendingDevicesManager,
accountsManager,
messagesManager,
cdsSender,
rateLimiters,
deviceConfiguration))
.build();
@@ -101,9 +103,13 @@ public class DeviceControllerTest {
when(rateLimiters.getAllocateDeviceLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter);
when(masterDevice.getId()).thenReturn(1L);
when(account.getNextDeviceId()).thenReturn(42L);
when(account.getNumber()).thenReturn(AuthHelper.VALID_NUMBER);
// when(maxedAccount.getActiveDeviceCount()).thenReturn(6);
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(masterDevice));
when(account.isActive()).thenReturn(false);
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis())));
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(new StoredVerificationCode("1112223", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31))));
@@ -195,4 +201,16 @@ public class DeviceControllerTest {
assertEquals(response.getStatus(), 422);
verifyNoMoreInteractions(messagesManager);
}
@Test
public void removeDeviceTest() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/devices/12345")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete();
assertEquals(204, response.getStatus());
verify(cdsSender).deleteRegisteredUser(eq(AuthHelper.VALID_NUMBER));
}
}

View File

@@ -1,5 +1,6 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
@@ -7,6 +8,10 @@ import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentials;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryClientConfiguration;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
@@ -24,15 +29,19 @@ import java.util.List;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyListOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DirectoryControllerTest {
private final RateLimiters rateLimiters = mock(RateLimiters.class );
private final RateLimiter rateLimiter = mock(RateLimiter.class );
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
private final RateLimiters rateLimiters = mock(RateLimiters.class);
private final RateLimiter rateLimiter = mock(RateLimiter.class);
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
private final DirectoryCredentialsGenerator directoryCredentialsGenerator = mock(DirectoryCredentialsGenerator.class);
private final DirectoryCredentials validCredentials = new DirectoryCredentials("username", "password");
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
@@ -40,7 +49,8 @@ public class DirectoryControllerTest {
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new DirectoryController(rateLimiters,
directoryManager))
directoryManager,
directoryCredentialsGenerator))
.build();
@@ -56,6 +66,19 @@ public class DirectoryControllerTest {
return response;
}
});
when(directoryCredentialsGenerator.generateFor(eq(AuthHelper.VALID_NUMBER))).thenReturn(validCredentials);
}
@Test
public void testGetAuthToken() {
DirectoryCredentials token =
resources.getJerseyTest()
.target("/v1/directory/auth")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(DirectoryCredentials.class);
assertThat(token.getUsername()).isEqualTo(validCredentials.getUsername());
assertThat(token.getPassword()).isEqualTo(validCredentials.getPassword());
}
@Test

View File

@@ -0,0 +1,242 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.google.common.base.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationCache;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Arrays;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class DirectoryReconcilerTest {
private static final String VALID_NUMBER = "valid";
private static final String INACTIVE_NUMBER = "inactive";
private static final long ACCOUNT_COUNT = 0L;
private static final long INTERVAL_MS = 30_000L;
private final Account account = mock(Account.class);
private final Account inactiveAccount = mock(Account.class);
private final Accounts accounts = mock(Accounts.class);
private final BatchOperationHandle batchOperationHandle = mock(BatchOperationHandle.class);
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
private final DirectoryReconciliationClient reconciliationClient = mock(DirectoryReconciliationClient.class);
private final DirectoryReconciliationCache reconciliationCache = mock(DirectoryReconciliationCache.class);
private final DirectoryReconciler directoryReconciler = new DirectoryReconciler(reconciliationClient, reconciliationCache, directoryManager, accounts);
private final DirectoryReconciliationResponse successResponse = new DirectoryReconciliationResponse(DirectoryReconciliationResponse.Status.OK);
private final DirectoryReconciliationResponse notFoundResponse = new DirectoryReconciliationResponse(DirectoryReconciliationResponse.Status.MISSING);
@Before
public void setup() {
when(account.getNumber()).thenReturn(VALID_NUMBER);
when(account.isActive()).thenReturn(true);
when(account.isVideoSupported()).thenReturn(true);
when(account.isVoiceSupported()).thenReturn(true);
when(inactiveAccount.getNumber()).thenReturn(INACTIVE_NUMBER);
when(inactiveAccount.isActive()).thenReturn(false);
when(directoryManager.startBatchOperation()).thenReturn(batchOperationHandle);
when(accounts.getAllFrom(anyInt())).thenReturn(Arrays.asList(account, inactiveAccount));
when(accounts.getAllFrom(eq(VALID_NUMBER), anyInt())).thenReturn(Arrays.asList(inactiveAccount));
when(accounts.getAllFrom(eq(INACTIVE_NUMBER), anyInt())).thenReturn(Collections.emptyList());
when(accounts.getCount()).thenReturn(ACCOUNT_COUNT);
when(reconciliationClient.sendChunk(any())).thenReturn(successResponse);
when(reconciliationCache.getLastNumber()).thenReturn(Optional.absent());
when(reconciliationCache.claimActiveWork(any(), anyLong())).thenReturn(true);
when(reconciliationCache.isAccelerated()).thenReturn(false);
}
@Test
public void testGetUncachedAccountCount() {
when(reconciliationCache.getCachedAccountCount()).thenReturn(Optional.absent());
long accountCount = directoryReconciler.getAccountCount();
assertThat(accountCount).isEqualTo(ACCOUNT_COUNT);
verify(accounts, times(1)).getCount();
verify(reconciliationCache, times(1)).getCachedAccountCount();
verify(reconciliationCache, times(1)).setCachedAccountCount(eq(ACCOUNT_COUNT));
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
@Test
public void testGetCachedAccountCount() {
when(reconciliationCache.getCachedAccountCount()).thenReturn(Optional.of(ACCOUNT_COUNT));
long accountCount = directoryReconciler.getAccountCount();
assertThat(accountCount).isEqualTo(ACCOUNT_COUNT);
verify(reconciliationCache, times(1)).getCachedAccountCount();
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
@Test
public void testValid() {
long delayMs = directoryReconciler.doPeriodicWork(INTERVAL_MS);
assertThat(delayMs).isLessThanOrEqualTo(INTERVAL_MS);
verify(accounts, times(1)).getAllFrom(anyInt());
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromNumber()).isNull();
assertThat(request.getValue().getToNumber()).isEqualTo(INACTIVE_NUMBER);
assertThat(request.getValue().getNumbers()).isEqualTo(Arrays.asList(VALID_NUMBER));
ArgumentCaptor<ClientContact> addedContact = ArgumentCaptor.forClass(ClientContact.class);
verify(directoryManager, times(1)).startBatchOperation();
verify(directoryManager, times(1)).add(eq(batchOperationHandle), addedContact.capture());
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(INACTIVE_NUMBER));
verify(directoryManager, times(1)).stopBatchOperation(eq(batchOperationHandle));
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBER));
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.of(INACTIVE_NUMBER)));
verify(reconciliationCache, times(1)).isAccelerated();
verify(reconciliationCache, times(2)).claimActiveWork(any(), anyLong());
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
@Test
public void testInProgress() {
when(reconciliationCache.getLastNumber()).thenReturn(Optional.of(VALID_NUMBER));
long delayMs = directoryReconciler.doPeriodicWork(INTERVAL_MS);
assertThat(delayMs).isLessThanOrEqualTo(INTERVAL_MS);
verify(accounts, times(1)).getAllFrom(eq(VALID_NUMBER), anyInt());
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromNumber()).isEqualTo(VALID_NUMBER);
assertThat(request.getValue().getToNumber()).isEqualTo(INACTIVE_NUMBER);
assertThat(request.getValue().getNumbers()).isEqualTo(Collections.emptyList());
verify(directoryManager, times(1)).startBatchOperation();
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(INACTIVE_NUMBER));
verify(directoryManager, times(1)).stopBatchOperation(eq(batchOperationHandle));
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.of(INACTIVE_NUMBER)));
verify(reconciliationCache, times(1)).isAccelerated();
verify(reconciliationCache, times(2)).claimActiveWork(any(), anyLong());
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
@Test
public void testLastChunk() {
when(reconciliationCache.getLastNumber()).thenReturn(Optional.of(INACTIVE_NUMBER));
long delayMs = directoryReconciler.doPeriodicWork(INTERVAL_MS);
assertThat(delayMs).isLessThanOrEqualTo(INTERVAL_MS);
verify(accounts, times(1)).getAllFrom(eq(INACTIVE_NUMBER), anyInt());
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromNumber()).isEqualTo(INACTIVE_NUMBER);
assertThat(request.getValue().getToNumber()).isNull();
assertThat(request.getValue().getNumbers()).isEqualTo(Collections.emptyList());
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
verify(reconciliationCache, times(1)).clearAccelerate();
verify(reconciliationCache, times(1)).isAccelerated();
verify(reconciliationCache, times(2)).claimActiveWork(any(), anyLong());
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
@Test
public void testNotFound() {
when(reconciliationClient.sendChunk(any())).thenReturn(notFoundResponse);
long delayMs = directoryReconciler.doPeriodicWork(INTERVAL_MS);
assertThat(delayMs).isLessThanOrEqualTo(INTERVAL_MS);
verify(accounts, times(1)).getAllFrom(anyInt());
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromNumber()).isNull();
assertThat(request.getValue().getToNumber()).isEqualTo(INACTIVE_NUMBER);
assertThat(request.getValue().getNumbers()).isEqualTo(Arrays.asList(VALID_NUMBER));
ArgumentCaptor<ClientContact> addedContact = ArgumentCaptor.forClass(ClientContact.class);
verify(directoryManager, times(1)).startBatchOperation();
verify(directoryManager, times(1)).add(eq(batchOperationHandle), addedContact.capture());
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(INACTIVE_NUMBER));
verify(directoryManager, times(1)).stopBatchOperation(eq(batchOperationHandle));
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBER));
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
verify(reconciliationCache, times(1)).clearAccelerate();
verify(reconciliationCache, times(1)).claimActiveWork(any(), anyLong());
verifyNoMoreInteractions(accounts);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
verifyNoMoreInteractions(reconciliationCache);
}
}