Decommission the old directory cache.

This commit is contained in:
Jon Chambers
2021-01-11 16:27:04 -05:00
committed by Jon Chambers
parent 9cd121c8f6
commit 71510a8199
20 changed files with 34 additions and 831 deletions

View File

@@ -6,75 +6,45 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.collect.ImmutableSet;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.DirectoryFeedbackRequest;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Collections;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
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.Mockito.*;
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 RateLimiter ipLimiter = mock(RateLimiter.class );
private final DirectoryManager directoryManager = mock(DirectoryManager.class );
private final ExternalServiceCredentialGenerator directoryCredentialsGenerator = mock(ExternalServiceCredentialGenerator.class);
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new DirectoryController(rateLimiters,
directoryManager,
directoryCredentialsGenerator))
.addResource(new DirectoryController(directoryCredentialsGenerator))
.build();
@Before
public void setup() throws Exception {
when(rateLimiters.getContactsLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getContactsIpLimiter()).thenReturn(ipLimiter);
when(directoryManager.get(anyListOf(byte[].class))).thenAnswer(new Answer<List<byte[]>>() {
@Override
public List<byte[]> answer(InvocationOnMock invocationOnMock) throws Throwable {
List<byte[]> query = (List<byte[]>) invocationOnMock.getArguments()[0];
List<byte[]> response = new LinkedList<>(query);
response.remove(0);
return response;
}
});
public void setup() {
when(directoryCredentialsGenerator.generateFor(eq(AuthHelper.VALID_NUMBER))).thenReturn(validCredentials);
}
@@ -85,65 +55,10 @@ public class DirectoryControllerTest {
.target("/v1/directory/feedback-v3/ok")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("test reason")), MediaType.APPLICATION_JSON_TYPE));
.put(Entity.json("{\"reason\": \"test reason\"}"));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testNotFoundFeedback() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/test-not-found")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("test reason")), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo()).isEqualTo(Status.NOT_FOUND);
}
@Test
public void testFeedbackEmptyRequest() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.json(""));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackNoReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.empty()), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackEmptyReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("")), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackTooLargeReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of(new String(new char[102400]))), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.CLIENT_ERROR);
}
@Test
public void testGetAuthToken() {
ExternalServiceCredentials token =
@@ -169,16 +84,7 @@ public class DirectoryControllerTest {
@Test
public void testContactIntersection() throws Exception {
List<String> tokens = new LinkedList<String>() {{
add(Base64.encodeBytes("foo".getBytes()));
add(Base64.encodeBytes("bar".getBytes()));
add(Base64.encodeBytes("baz".getBytes()));
}};
List<String> expectedResponse = new LinkedList<>(tokens);
expectedResponse.remove(0);
public void testContactIntersection() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/tokens/")
@@ -187,7 +93,7 @@ public class DirectoryControllerTest {
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER,
AuthHelper.VALID_PASSWORD))
.header("X-Forwarded-For", "192.168.1.1, 1.1.1.1")
.put(Entity.entity(new ClientContactTokens(tokens), MediaType.APPLICATION_JSON_TYPE));
.put(Entity.entity(Collections.emptyMap(), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(429);

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
public class ClientContactTest {
@Test
public void serializeToJSON() throws Exception {
byte[] token = Util.getContactToken("+14152222222");
ClientContact contact = new ClientContact(token, null, false, false);
ClientContact contactWithRelay = new ClientContact(token, "whisper", false, false);
ClientContact contactWithRelayVox = new ClientContact(token, "whisper", true, false);
ClientContact contactWithRelayVid = new ClientContact(token, "whisper", true, true);
assertThat("Basic Contact Serialization works",
asJson(contact),
is(equalTo(jsonFixture("fixtures/contact.json"))));
assertThat("Contact Relay Serialization works",
asJson(contactWithRelay),
is(equalTo(jsonFixture("fixtures/contact.relay.json"))));
assertThat("Contact Relay Vox Serializaton works",
asJson(contactWithRelayVox),
is(equalTo(jsonFixture("fixtures/contact.relay.voice.json"))));
assertThat("Contact Relay Video Serializaton works",
asJson(contactWithRelayVid),
is(equalTo(jsonFixture("fixtures/contact.relay.video.json"))));
}
@Test
public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", false, false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact));
}
}

View File

@@ -6,9 +6,7 @@
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -17,16 +15,6 @@ import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
public class PreKeyTest {
@Test
public void deserializeFromJSONV() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", false, false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact));
}
@Test
public void serializeToJSONV2() throws Exception {
PreKey preKey = new PreKey(1234, "test");

View File

@@ -13,7 +13,6 @@ import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
@@ -42,7 +41,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -54,7 +52,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid.toString()))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}");
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> account = accountsManager.get("+14152222222");
assertTrue(account.isPresent());
@@ -72,7 +70,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -83,7 +80,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid.toString()))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}");
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> account = accountsManager.get(uuid);
assertTrue(account.isPresent());
@@ -102,7 +99,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -114,7 +110,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null);
when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get("+14152222222");
assertTrue(retrieved.isPresent());
@@ -134,7 +130,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -146,7 +141,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get(uuid);
assertTrue(retrieved.isPresent());
@@ -166,7 +161,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -178,7 +172,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenThrow(new RedisException("Connection lost!"));
when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get("+14152222222");
assertTrue(retrieved.isPresent());
@@ -198,7 +192,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@@ -210,7 +203,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenThrow(new RedisException("Connection lost!"));
when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get(uuid);
assertTrue(retrieved.isPresent());

View File

@@ -8,16 +8,12 @@ package org.whispersystems.textsecuregcm.tests.storage;
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.AccountDatabaseCrawlerRestartException;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Arrays;
import java.util.Optional;
@@ -25,7 +21,6 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
public class DirectoryReconcilerTest {
@@ -39,10 +34,8 @@ public class DirectoryReconcilerTest {
private final Account activeAccount = mock(Account.class);
private final Account inactiveAccount = mock(Account.class);
private final Account undiscoverableAccount = mock(Account.class);
private final BatchOperationHandle batchOperationHandle = mock(BatchOperationHandle.class);
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
private final DirectoryReconciliationClient reconciliationClient = mock(DirectoryReconciliationClient.class);
private final DirectoryReconciler directoryReconciler = new DirectoryReconciler("test", true, reconciliationClient, directoryManager);
private final DirectoryReconciler directoryReconciler = new DirectoryReconciler("test", reconciliationClient);
private final DirectoryReconciliationResponse successResponse = new DirectoryReconciliationResponse(DirectoryReconciliationResponse.Status.OK);
@@ -60,7 +53,6 @@ public class DirectoryReconcilerTest {
when(undiscoverableAccount.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER);
when(undiscoverableAccount.isEnabled()).thenReturn(true);
when(undiscoverableAccount.isDiscoverableByPhoneNumber()).thenReturn(false);
when(directoryManager.startBatchOperation()).thenReturn(batchOperationHandle);
}
@Test
@@ -68,38 +60,12 @@ public class DirectoryReconcilerTest {
when(reconciliationClient.sendChunk(any())).thenReturn(successResponse);
directoryReconciler.timeAndProcessCrawlChunk(Optional.of(VALID_UUID), Arrays.asList(activeAccount, inactiveAccount, undiscoverableAccount));
verify(activeAccount, atLeastOnce()).getUuid();
verify(activeAccount, atLeastOnce()).getNumber();
verify(activeAccount, atLeastOnce()).isEnabled();
verify(activeAccount, atLeastOnce()).isDiscoverableByPhoneNumber();
verify(inactiveAccount, atLeastOnce()).getNumber();
verify(inactiveAccount, atLeastOnce()).isEnabled();
verify(undiscoverableAccount, atLeastOnce()).getUuid();
verify(undiscoverableAccount, atLeastOnce()).getNumber();
verify(undiscoverableAccount, atLeastOnce()).isEnabled();
verify(undiscoverableAccount, atLeastOnce()).isDiscoverableByPhoneNumber();
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromUuid()).isEqualTo(VALID_UUID);
assertThat(request.getValue().getToUuid()).isEqualTo(UNDISCOVERABLE_UUID);
assertThat(request.getValue().getUsers()).isEqualTo(Arrays.asList(new DirectoryReconciliationRequest.User(VALID_UUID, VALID_NUMBERRR)));
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_NUMBERRR));
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(UNDISCOVERABLE_NUMBER));
verify(directoryManager, times(1)).stopBatchOperation(eq(batchOperationHandle));
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBERRR));
verifyNoMoreInteractions(activeAccount);
verifyNoMoreInteractions(inactiveAccount);
verifyNoMoreInteractions(batchOperationHandle);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
}
}