mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 08:08:06 +01:00
Make account deletion an asynchronous operation
This commit is contained in:
@@ -37,6 +37,7 @@ import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.Invocation;
|
||||
@@ -815,7 +816,7 @@ class AccountControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteAccount() throws InterruptedException {
|
||||
void testDeleteAccount() {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/me")
|
||||
@@ -828,18 +829,18 @@ class AccountControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteAccountInterrupted() throws InterruptedException {
|
||||
doThrow(InterruptedException.class).when(accountsManager).delete(any(), any());
|
||||
void testDeleteAccountException() {
|
||||
when(accountsManager.delete(any(), any())).thenReturn(CompletableFuture.failedFuture(new RuntimeException("OH NO")));
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/me")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete();
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target("/v1/accounts/me")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.delete()) {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(500);
|
||||
verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST);
|
||||
assertThat(response.getStatus()).isEqualTo(500);
|
||||
verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -15,10 +15,8 @@ import static org.mockito.Mockito.when;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason;
|
||||
@@ -35,11 +33,10 @@ class AccountCleanerTest {
|
||||
private final Device undeletedDisabledDevice = mock(Device.class );
|
||||
private final Device undeletedEnabledDevice = mock(Device.class );
|
||||
|
||||
private ExecutorService deletionExecutor;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(accountsManager.delete(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
when(deletedDisabledDevice.isEnabled()).thenReturn(false);
|
||||
when(deletedDisabledDevice.getGcmId()).thenReturn(null);
|
||||
when(deletedDisabledDevice.getApnId()).thenReturn(null);
|
||||
@@ -66,19 +63,11 @@ class AccountCleanerTest {
|
||||
when(undeletedEnabledAccount.getNumber()).thenReturn("+14153333333");
|
||||
when(undeletedEnabledAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(179));
|
||||
when(undeletedEnabledAccount.getUuid()).thenReturn(UUID.randomUUID());
|
||||
|
||||
deletionExecutor = Executors.newFixedThreadPool(2);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws InterruptedException {
|
||||
deletionExecutor.shutdown();
|
||||
deletionExecutor.awaitTermination(2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAccounts() throws InterruptedException {
|
||||
AccountCleaner accountCleaner = new AccountCleaner(accountsManager, deletionExecutor);
|
||||
void testAccounts() {
|
||||
AccountCleaner accountCleaner = new AccountCleaner(accountsManager);
|
||||
accountCleaner.onCrawlStart();
|
||||
accountCleaner.timeAndProcessCrawlChunk(Optional.empty(),
|
||||
Arrays.asList(deletedDisabledAccount, undeletedDisabledAccount, undeletedEnabledAccount));
|
||||
|
||||
@@ -12,12 +12,18 @@ import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AccountLockManagerTest {
|
||||
|
||||
private AmazonDynamoDBLockClient lockClient;
|
||||
private ExecutorService executor;
|
||||
|
||||
private AccountLockManager accountLockManager;
|
||||
|
||||
@@ -30,10 +36,19 @@ class AccountLockManagerTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lockClient = mock(AmazonDynamoDBLockClient.class);
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
accountLockManager = new AccountLockManager(lockClient);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws InterruptedException {
|
||||
executor.shutdown();
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
executor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLock() throws InterruptedException {
|
||||
accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {});
|
||||
@@ -59,4 +74,34 @@ class AccountLockManagerTest {
|
||||
assertThrows(IllegalArgumentException.class, () -> accountLockManager.withLock(Collections.emptyList(), () -> {}));
|
||||
verify(task, never()).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLockAsync() throws InterruptedException {
|
||||
accountLockManager.withLockAsync(List.of(FIRST_NUMBER, SECOND_NUMBER),
|
||||
() -> CompletableFuture.completedFuture(null), executor).join();
|
||||
|
||||
verify(lockClient, times(2)).acquireLock(any());
|
||||
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLockAsyncTaskThrowsException() throws InterruptedException {
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> accountLockManager.withLockAsync(List.of(FIRST_NUMBER, SECOND_NUMBER),
|
||||
() -> CompletableFuture.failedFuture(new RuntimeException()), executor).join());
|
||||
|
||||
verify(lockClient, times(2)).acquireLock(any());
|
||||
verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLockAsyncEmptyList() {
|
||||
final Runnable task = mock(Runnable.class);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> accountLockManager.withLockAsync(Collections.emptyList(),
|
||||
() -> CompletableFuture.completedFuture(null), executor));
|
||||
|
||||
verify(task, never()).run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -58,6 +62,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
static final RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
|
||||
private ClientPresenceManager clientPresenceManager;
|
||||
private ExecutorService accountLockExecutor;
|
||||
|
||||
private AccountsManager accountsManager;
|
||||
|
||||
@@ -81,6 +86,8 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
Tables.DELETED_ACCOUNTS.tableName(),
|
||||
SCAN_PAGE_SIZE);
|
||||
|
||||
accountLockExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getDynamoDbClient(),
|
||||
Tables.DELETED_ACCOUNTS_LOCK.tableName());
|
||||
|
||||
@@ -104,6 +111,15 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final ProfilesManager profilesManager = mock(ProfilesManager.class);
|
||||
when(profilesManager.deleteAll(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
|
||||
mock(RegistrationRecoveryPasswordsManager.class);
|
||||
|
||||
when(registrationRecoveryPasswordsManager.removeForNumber(any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
phoneNumberIdentifiers,
|
||||
@@ -111,17 +127,26 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
accountLockManager,
|
||||
keysManager,
|
||||
messagesManager,
|
||||
mock(ProfilesManager.class),
|
||||
profilesManager,
|
||||
secureStorageClient,
|
||||
secureBackupClient,
|
||||
svr2Client,
|
||||
clientPresenceManager,
|
||||
mock(ExperimentEnrollmentManager.class),
|
||||
mock(RegistrationRecoveryPasswordsManager.class),
|
||||
registrationRecoveryPasswordsManager,
|
||||
accountLockExecutor,
|
||||
mock(Clock.class));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws InterruptedException {
|
||||
accountLockExecutor.shutdown();
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
accountLockExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChangeNumber() throws InterruptedException, MismatchedDevicesException {
|
||||
final String originalNumber = "+18005551111";
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -104,6 +105,13 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
return null;
|
||||
}).when(accountLockManager).withLock(any(), any());
|
||||
|
||||
when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> {
|
||||
final Supplier<CompletableFuture<?>> taskSupplier = invocation.getArgument(1);
|
||||
taskSupplier.get().join();
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
|
||||
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
|
||||
.thenAnswer((Answer<UUID>) invocation -> UUID.randomUUID());
|
||||
@@ -122,6 +130,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
mock(ClientPresenceManager.class),
|
||||
mock(ExperimentEnrollmentManager.class),
|
||||
mock(RegistrationRecoveryPasswordsManager.class),
|
||||
mock(Executor.class),
|
||||
mock(Clock.class)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
@@ -46,7 +45,9 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -140,6 +141,7 @@ class AccountsManagerTest {
|
||||
when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK"));
|
||||
|
||||
when(accounts.updateAsync(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(accounts.delete(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
final Account account = invocation.getArgument(0, Account.class);
|
||||
@@ -188,8 +190,21 @@ class AccountsManagerTest {
|
||||
return null;
|
||||
}).when(accountLockManager).withLock(any(), any());
|
||||
|
||||
when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> {
|
||||
final Supplier<CompletableFuture<?>> taskSupplier = invocation.getArgument(1);
|
||||
taskSupplier.get().join();
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
|
||||
mock(RegistrationRecoveryPasswordsManager.class);
|
||||
|
||||
when(registrationRecoveryPasswordsManager.removeForNumber(anyString())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
when(keysManager.delete(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(profilesManager.deleteAll(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
@@ -207,7 +222,8 @@ class AccountsManagerTest {
|
||||
svr2Client,
|
||||
clientPresenceManager,
|
||||
enrollmentManager,
|
||||
mock(RegistrationRecoveryPasswordsManager.class),
|
||||
registrationRecoveryPasswordsManager,
|
||||
mock(Executor.class),
|
||||
mock(Clock.class));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -106,6 +109,13 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
return null;
|
||||
}).when(accountLockManager).withLock(any(), any());
|
||||
|
||||
when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> {
|
||||
final Supplier<CompletableFuture<?>> taskSupplier = invocation.getArgument(1);
|
||||
taskSupplier.get().join();
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers =
|
||||
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName());
|
||||
|
||||
@@ -126,6 +136,7 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
mock(ClientPresenceManager.class),
|
||||
experimentEnrollmentManager,
|
||||
mock(RegistrationRecoveryPasswordsManager.class),
|
||||
mock(Executor.class),
|
||||
mock(Clock.class));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -180,6 +181,7 @@ class AccountsTest {
|
||||
mock(ClientPresenceManager.class),
|
||||
mock(ExperimentEnrollmentManager.class),
|
||||
mock(RegistrationRecoveryPasswordsManager.class),
|
||||
mock(Executor.class),
|
||||
mock(Clock.class));
|
||||
|
||||
final Account account = nextRandomAccount();
|
||||
@@ -246,7 +248,7 @@ class AccountsTest {
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
|
||||
accounts.delete(originalUuid);
|
||||
accounts.delete(originalUuid).join();
|
||||
assertThat(accounts.findRecentlyDeletedAccountIdentifier(account.getNumber())).hasValue(originalUuid);
|
||||
|
||||
freshUser = accounts.create(account);
|
||||
@@ -577,7 +579,7 @@ class AccountsTest {
|
||||
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isPresent();
|
||||
assertThat(accounts.getByAccountIdentifier(retainedAccount.getUuid())).isPresent();
|
||||
|
||||
accounts.delete(deletedAccount.getUuid());
|
||||
accounts.delete(deletedAccount.getUuid()).join();
|
||||
|
||||
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isNotPresent();
|
||||
assertThat(accounts.findRecentlyDeletedAccountIdentifier(deletedAccount.getNumber())).hasValue(deletedAccount.getUuid());
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ProfilesTest {
|
||||
void testDeleteReset() throws InvalidInputException {
|
||||
profiles.set(ACI, validProfile);
|
||||
|
||||
profiles.deleteAll(ACI);
|
||||
profiles.deleteAll(ACI).join();
|
||||
|
||||
final String version = "someVersion";
|
||||
final byte[] name = ProfileTestHelper.generateRandomByteArray(81);
|
||||
@@ -242,7 +242,7 @@ public class ProfilesTest {
|
||||
profiles.set(ACI, profileOne);
|
||||
profiles.set(ACI, profileTwo);
|
||||
|
||||
profiles.deleteAll(ACI);
|
||||
profiles.deleteAll(ACI).join();
|
||||
|
||||
Optional<VersionedProfile> retrieved = profiles.get(ACI, versionOne);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user