Use registration ID or creation timestamp in the transfer archive flow

This commit is contained in:
Katherine
2025-07-30 15:32:49 -04:00
committed by GitHub
parent 30774bbc40
commit db4c71368c
6 changed files with 308 additions and 87 deletions

View File

@@ -1082,31 +1082,39 @@ class DeviceControllerTest {
}
}
@Test
void recordTransferArchiveUploaded() {
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void recordTransferArchiveUploaded(final Optional<Instant> deviceCreated, final Optional<Integer> registrationId) {
final byte deviceId = Device.PRIMARY_ID + 1;
final Instant deviceCreated = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final RemoteAttachment transferArchive =
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)));
when(rateLimiter.validateAsync(AuthHelper.VALID_UUID)).thenReturn(CompletableFuture.completedFuture(null));
when(accountsManager.recordTransferArchiveUpload(account, deviceId, deviceCreated, transferArchive))
when(accountsManager.recordTransferArchiveUpload(account, deviceId, deviceCreated, registrationId, transferArchive))
.thenReturn(CompletableFuture.completedFuture(null));
try (final Response response = resources.getJerseyTest()
.target("/v1/devices/transfer_archive")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, deviceCreated.toEpochMilli(), transferArchive),
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, deviceCreated.map(Instant::toEpochMilli), registrationId, transferArchive),
MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(204, response.getStatus());
verify(accountsManager)
.recordTransferArchiveUpload(account, deviceId, deviceCreated, transferArchive);
.recordTransferArchiveUpload(account, deviceId, deviceCreated, registrationId, transferArchive);
}
}
private static List<Arguments> recordTransferArchiveUploaded() {
return List.of(
Arguments.of(Optional.empty(), Optional.of(123)),
Arguments.of(Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)), Optional.empty())
);
}
@Test
void recordTransferArchiveFailed() {
final byte deviceId = Device.PRIMARY_ID + 1;
@@ -1114,20 +1122,20 @@ class DeviceControllerTest {
final RemoteAttachmentError transferFailure = new RemoteAttachmentError(RemoteAttachmentError.ErrorType.CONTINUE_WITHOUT_UPLOAD);
when(rateLimiter.validateAsync(AuthHelper.VALID_UUID)).thenReturn(CompletableFuture.completedFuture(null));
when(accountsManager.recordTransferArchiveUpload(account, deviceId, deviceCreated, transferFailure))
when(accountsManager.recordTransferArchiveUpload(account, deviceId, Optional.of(deviceCreated), Optional.empty(), transferFailure))
.thenReturn(CompletableFuture.completedFuture(null));
try (final Response response = resources.getJerseyTest()
.target("/v1/devices/transfer_archive")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, deviceCreated.toEpochMilli(), transferFailure),
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, Optional.of(deviceCreated.toEpochMilli()), Optional.empty(), transferFailure),
MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(204, response.getStatus());
verify(accountsManager)
.recordTransferArchiveUpload(account, deviceId, deviceCreated, transferFailure);
.recordTransferArchiveUpload(account, deviceId, Optional.of(deviceCreated), Optional.empty(), transferFailure);
}
}
@@ -1145,29 +1153,33 @@ class DeviceControllerTest {
assertEquals(422, response.getStatus());
verify(accountsManager, never())
.recordTransferArchiveUpload(any(), anyByte(), any(), any());
.recordTransferArchiveUpload(any(), anyByte(), any(), any(), any());
}
}
@SuppressWarnings("DataFlowIssue")
private static List<TransferArchiveUploadedRequest> recordTransferArchiveUploadedBadRequest() {
private static List<Arguments> recordTransferArchiveUploadedBadRequest() {
final RemoteAttachment validTransferArchive =
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("archive".getBytes(StandardCharsets.UTF_8)));
return List.of(
// Invalid device ID
new TransferArchiveUploadedRequest((byte) -1, System.currentTimeMillis(), validTransferArchive),
// Invalid "created at" timestamp
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, -1, validTransferArchive),
// Missing CDN number
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, System.currentTimeMillis(),
new RemoteAttachment(null, Base64.getUrlEncoder().encodeToString("archive".getBytes(StandardCharsets.UTF_8)))),
// Bad attachment key
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, System.currentTimeMillis(),
new RemoteAttachment(3, "This is not a valid base64 string"))
Arguments.argumentSet("Invalid device ID", new TransferArchiveUploadedRequest((byte) -1, Optional.of(System.currentTimeMillis()), Optional.empty(), validTransferArchive)),
Arguments.argumentSet("Invalid \"created at\" timestamp",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of((long) -1), Optional.empty(), validTransferArchive)),
Arguments.argumentSet("Invalid registration ID - negative",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.empty(), Optional.of(-1), validTransferArchive)),
Arguments.argumentSet("Invalid registration ID - too large",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.empty(), Optional.of(0x4000), validTransferArchive)),
Arguments.argumentSet("Exactly one of \"created at\" timestamp and registration ID must be present - neither provided",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.empty(), Optional.empty(), validTransferArchive)),
Arguments.argumentSet("Exactly one of \"created at\" timestamp and registration ID must be present - both provided",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of(System.currentTimeMillis()), Optional.of(123), validTransferArchive)),
Arguments.argumentSet("Missing CDN number",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of(System.currentTimeMillis()), Optional.empty(),
new RemoteAttachment(null, Base64.getUrlEncoder().encodeToString("archive".getBytes(StandardCharsets.UTF_8))))),
Arguments.argumentSet("Bad attachment key",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of(System.currentTimeMillis()), Optional.empty(),
new RemoteAttachment(3, "This is not a valid base64 string")))
);
}
@@ -1180,14 +1192,14 @@ class DeviceControllerTest {
.target("/v1/devices/transfer_archive")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new TransferArchiveUploadedRequest(Device.PRIMARY_ID, System.currentTimeMillis(),
.put(Entity.entity(new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of(System.currentTimeMillis()), Optional.empty(),
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)))),
MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(429, response.getStatus());
verify(accountsManager, never())
.recordTransferArchiveUpload(any(), anyByte(), any(), any());
.recordTransferArchiveUpload(any(), anyByte(), any(), any(), any());
}
}

View File

@@ -11,6 +11,9 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.auth.DisconnectionRequestManager;
import org.whispersystems.textsecuregcm.entities.RemoteAttachmentError;
import org.whispersystems.textsecuregcm.entities.RestoreAccountRequest;
@@ -28,6 +31,7 @@ import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -87,18 +91,22 @@ public class AccountsManagerDeviceTransferIntegrationTest {
accountsManager.stop();
}
@Test
void waitForTransferArchive() {
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void waitForTransferArchive(
final Optional<Long> recordUploadDeviceCreated,
final Optional<Integer> recordUploadRegistrationId) {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
final long deviceCreated = System.currentTimeMillis();
final RemoteAttachment transferArchive =
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("transfer-archive".getBytes(StandardCharsets.UTF_8)));
final Device device = mock(Device.class);
when(device.getId()).thenReturn(deviceId);
when(device.getCreated()).thenReturn(deviceCreated);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
@@ -111,66 +119,106 @@ public class AccountsManagerDeviceTransferIntegrationTest {
assertEquals(Optional.empty(), displacedFuture.join());
accountsManager.recordTransferArchiveUpload(account, deviceId, Instant.ofEpochMilli(deviceCreated), transferArchive).join();
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli), recordUploadRegistrationId, transferArchive).join();
assertEquals(Optional.of(transferArchive), activeFuture.join());
}
@Test
void waitForTransferArchiveAlreadyAdded() {
private static List<Arguments> waitForTransferArchive() {
final long deviceCreated = System.currentTimeMillis();
final int registrationId = 123;
return List.of(
Arguments.of(Optional.empty(), Optional.of(registrationId)),
Arguments.of(Optional.of(deviceCreated), Optional.empty())
);
}
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void waitForTransferArchiveAlreadyAdded(
final Optional<Long> recordUploadDeviceCreated,
final Optional<Integer> recordUploadRegistrationId) {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
final long deviceCreated = System.currentTimeMillis();
final RemoteAttachment transferArchive =
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("transfer-archive".getBytes(StandardCharsets.UTF_8)));
final Device device = mock(Device.class);
when(device.getId()).thenReturn(deviceId);
when(device.getCreated()).thenReturn(deviceCreated);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
accountsManager.recordTransferArchiveUpload(account, deviceId, Instant.ofEpochMilli(deviceCreated), transferArchive).join();
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli), recordUploadRegistrationId, transferArchive).join();
assertEquals(Optional.of(transferArchive),
accountsManager.waitForTransferArchive(account, device, Duration.ofSeconds(5)).join());
}
@Test
void waitForErrorTransferArchive() {
private static List<Arguments> waitForTransferArchiveAlreadyAdded() {
final long deviceCreated = System.currentTimeMillis();
final int registrationId = 123;
return List.of(
Arguments.of(Optional.empty(), Optional.of(registrationId)),
Arguments.of(Optional.of(deviceCreated), Optional.empty())
);
}
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void waitForErrorTransferArchive(
final Optional<Long> recordUploadDeviceCreated,
final Optional<Integer> recordUploadRegistrationId) {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
final long deviceCreated = System.currentTimeMillis();
final RemoteAttachmentError transferArchiveError =
new RemoteAttachmentError(RemoteAttachmentError.ErrorType.CONTINUE_WITHOUT_UPLOAD);
final Device device = mock(Device.class);
when(device.getId()).thenReturn(deviceId);
when(device.getCreated()).thenReturn(deviceCreated);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
accountsManager
.recordTransferArchiveUpload(account, deviceId, Instant.ofEpochMilli(deviceCreated), transferArchiveError)
.join();
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli),
recordUploadRegistrationId, transferArchiveError).join();
assertEquals(Optional.of(transferArchiveError),
accountsManager.waitForTransferArchive(account, device, Duration.ofSeconds(5)).join());
}
@Test
void waitForTransferArchiveTimeout() {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
private static List<Arguments> waitForErrorTransferArchive() {
final long deviceCreated = System.currentTimeMillis();
final int registrationId = 123;
return List.of(
Arguments.of(Optional.empty(), Optional.of(registrationId)),
Arguments.of(Optional.of(deviceCreated), Optional.empty())
);
}
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void waitForTransferArchiveTimeout(
final Optional<Long> recordUploadDeviceCreated,
final Optional<Integer> recordUploadRegistrationId) {
final UUID accountIdentifier = UUID.randomUUID();
final Device device = mock(Device.class);
when(device.getId()).thenReturn(deviceId);
when(device.getCreated()).thenReturn(deviceCreated);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
@@ -179,6 +227,16 @@ public class AccountsManagerDeviceTransferIntegrationTest {
accountsManager.waitForTransferArchive(account, device, Duration.ofMillis(1)).join());
}
private static List<Arguments> waitForTransferArchiveTimeout() {
final long deviceCreated = System.currentTimeMillis();
final int registrationId = 123;
return List.of(
Arguments.of(Optional.empty(), Optional.of(registrationId)),
Arguments.of(Optional.of(deviceCreated), Optional.empty())
);
}
@Test
void waitForRestoreAccountRequest() {
final String token = RandomStringUtils.secure().nextAlphanumeric(16);

View File

@@ -57,6 +57,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -82,6 +83,8 @@ import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
import org.whispersystems.textsecuregcm.entities.TransferArchiveResult;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
@@ -1498,6 +1501,61 @@ class AccountsManagerTest {
}
}
@Test
void testFirstSuccessfulTransferArchiveCompletableFutureOneTimeout() {
// First future times out, second one completes successfully
final RemoteAttachment transferArchive = new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)));
final CompletableFuture<Optional<TransferArchiveResult>> timeoutFuture = new CompletableFuture<>();
timeoutFuture.completeOnTimeout(Optional.empty(), 50, TimeUnit.MILLISECONDS);
final CompletableFuture<Optional<TransferArchiveResult>> successfulFuture = new CompletableFuture<>();
final CompletableFuture<Optional<TransferArchiveResult>> result =
AccountsManager.firstSuccessfulTransferArchiveFuture(List.of(timeoutFuture, successfulFuture));
CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS)
.execute(() -> successfulFuture.complete(Optional.of(transferArchive)));
final Optional<TransferArchiveResult> maybeTransferArchive = result.join();
assertTrue(maybeTransferArchive.isPresent());
assertEquals(transferArchive, maybeTransferArchive.get());
}
@Test
void testFirstSuccessfulTransferArchiveCompletableFutureBothTimeout() {
// Both futures time out
final CompletableFuture<Optional<TransferArchiveResult>> firstTimeoutFuture = new CompletableFuture<>();
firstTimeoutFuture.completeOnTimeout(Optional.empty(), 10, TimeUnit.MILLISECONDS);
final CompletableFuture<Optional<TransferArchiveResult>> secondTimeoutFuture = new CompletableFuture<>();
secondTimeoutFuture.completeOnTimeout(Optional.empty(), 10, TimeUnit.MILLISECONDS);
final CompletableFuture<Optional<TransferArchiveResult>> result =
AccountsManager.firstSuccessfulTransferArchiveFuture(List.of(firstTimeoutFuture, secondTimeoutFuture));
assertTrue(result.join().isEmpty());
}
@Test
void testFirstSuccessfulTransferArchiveCompletableFuture() {
// First future completes successfully, second one times out
final RemoteAttachment transferArchive = new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)));
final CompletableFuture<Optional<TransferArchiveResult>> successfulFuture = new CompletableFuture<>();
final CompletableFuture<Optional<TransferArchiveResult>> timeoutFuture = new CompletableFuture<>();
timeoutFuture.completeOnTimeout(Optional.empty(), 50, TimeUnit.MILLISECONDS);
final CompletableFuture<Optional<TransferArchiveResult>> result =
AccountsManager.firstSuccessfulTransferArchiveFuture(List.of(successfulFuture, timeoutFuture));
successfulFuture.complete(Optional.of(transferArchive));
final Optional<TransferArchiveResult> maybeTransferArchive = result.join();
assertTrue(maybeTransferArchive.isPresent());
assertEquals(transferArchive, maybeTransferArchive.get());
}
private static List<Arguments> validateCompleteDeviceList() {
final byte deviceId = Device.PRIMARY_ID;
final byte extraDeviceId = deviceId + 1;