Retire creation timestamp from device endpoints

This commit is contained in:
Katherine
2025-12-05 10:55:20 -05:00
committed by GitHub
parent b887d1f7c0
commit 31d6ac71a4
8 changed files with 72 additions and 306 deletions

View File

@@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq;
@@ -196,7 +197,6 @@ class DeviceControllerTest {
final Device refreshedDevice = mock(Device.class);
when(refreshedDevice.getId()).thenReturn(deviceId);
when(refreshedDevice.getName()).thenReturn(deviceName);
when(refreshedDevice.getCreated()).thenReturn(deviceCreated);
when(refreshedDevice.getLastSeen()).thenReturn(deviceLastSeen);
when(refreshedDevice.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId);
when(refreshedDevice.getCreatedAtCiphertext()).thenReturn(createdAtCiphertext);
@@ -215,7 +215,6 @@ class DeviceControllerTest {
assertEquals(1, deviceInfoList.devices().size());
assertEquals(deviceId, deviceInfoList.devices().getFirst().id());
assertArrayEquals(deviceName, deviceInfoList.devices().getFirst().name());
assertEquals(deviceCreated, deviceInfoList.devices().getFirst().created());
assertEquals(deviceLastSeen, deviceInfoList.devices().getFirst().lastSeen());
assertEquals(registrationId, deviceInfoList.devices().getFirst().registrationId());
assertArrayEquals(createdAtCiphertext, deviceInfoList.devices().getFirst().createdAtCiphertext());
@@ -962,7 +961,6 @@ class DeviceControllerTest {
final DeviceInfo deviceInfo = new DeviceInfo(Device.PRIMARY_ID,
"Device name ciphertext".getBytes(StandardCharsets.UTF_8),
System.currentTimeMillis(),
System.currentTimeMillis(),
1,
"timestamp ciphertext".getBytes(StandardCharsets.UTF_8));
@@ -985,7 +983,6 @@ class DeviceControllerTest {
final DeviceInfo retrievedDeviceInfo = response.readEntity(DeviceInfo.class);
assertEquals(deviceInfo.id(), retrievedDeviceInfo.id());
assertArrayEquals(deviceInfo.name(), retrievedDeviceInfo.name());
assertEquals(deviceInfo.created(), retrievedDeviceInfo.created());
assertEquals(deviceInfo.lastSeen(), retrievedDeviceInfo.lastSeen());
assertEquals(deviceInfo.registrationId(), retrievedDeviceInfo.registrationId());
assertArrayEquals(deviceInfo.createdAtCiphertext(), retrievedDeviceInfo.createdAtCiphertext());
@@ -1083,60 +1080,53 @@ class DeviceControllerTest {
}
}
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void recordTransferArchiveUploaded(final Optional<Instant> deviceCreated, final Optional<Integer> registrationId) {
@Test
void recordTransferArchiveUploaded() {
final byte deviceId = Device.PRIMARY_ID + 1;
final int registrationId = 123;
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, registrationId, transferArchive))
when(accountsManager.recordTransferArchiveUpload(account, deviceId, 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.map(Instant::toEpochMilli), registrationId, transferArchive),
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, registrationId, transferArchive),
MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(204, response.getStatus());
verify(accountsManager)
.recordTransferArchiveUpload(account, deviceId, deviceCreated, registrationId, transferArchive);
.recordTransferArchiveUpload(account, deviceId, 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;
final int registrationId = 123;
final Instant deviceCreated = Instant.now().truncatedTo(ChronoUnit.MILLIS);
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, Optional.of(deviceCreated), Optional.empty(), transferFailure))
when(accountsManager.recordTransferArchiveUpload(account, deviceId, registrationId, 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, Optional.of(deviceCreated.toEpochMilli()), Optional.empty(), transferFailure),
.put(Entity.entity(new TransferArchiveUploadedRequest(deviceId, registrationId, transferFailure),
MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(204, response.getStatus());
verify(accountsManager)
.recordTransferArchiveUpload(account, deviceId, Optional.of(deviceCreated), Optional.empty(), transferFailure);
.recordTransferArchiveUpload(account, deviceId, registrationId, transferFailure);
}
}
@@ -1154,7 +1144,7 @@ class DeviceControllerTest {
assertEquals(422, response.getStatus());
verify(accountsManager, never())
.recordTransferArchiveUpload(any(), anyByte(), any(), any(), any());
.recordTransferArchiveUpload(any(), anyByte(), anyInt(), any());
}
}
@@ -1164,22 +1154,16 @@ class DeviceControllerTest {
new RemoteAttachment(3, Base64.getUrlEncoder().encodeToString("archive".getBytes(StandardCharsets.UTF_8)));
return List.of(
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 device ID", new TransferArchiveUploadedRequest((byte) -1, 1, validTransferArchive)),
Arguments.argumentSet("Invalid registration ID - negative",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.empty(), Optional.of(-1), validTransferArchive)),
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, -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)),
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, 0x4000, validTransferArchive)),
Arguments.argumentSet("Missing CDN number",
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, Optional.of(System.currentTimeMillis()), Optional.empty(),
new TransferArchiveUploadedRequest(Device.PRIMARY_ID, 1,
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 TransferArchiveUploadedRequest(Device.PRIMARY_ID, 1,
new RemoteAttachment(3, "This is not a valid base64 string")))
);
}
@@ -1193,14 +1177,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, Optional.of(System.currentTimeMillis()), Optional.empty(),
.put(Entity.entity(new TransferArchiveUploadedRequest(Device.PRIMARY_ID, 1,
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(), any());
.recordTransferArchiveUpload(any(), anyByte(), anyInt(), any());
}
}

View File

@@ -29,7 +29,6 @@ import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
@@ -91,13 +90,10 @@ public class AccountsManagerDeviceTransferIntegrationTest {
accountsManager.stop();
}
@ParameterizedTest
@MethodSource
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
void waitForTransferArchive(
final Optional<Long> recordUploadDeviceCreated,
final Optional<Integer> recordUploadRegistrationId) {
@Test
void waitForTransferArchive() {
final UUID accountIdentifier = UUID.randomUUID();
final int registrationId = 123;
final byte deviceId = Device.PRIMARY_ID;
final RemoteAttachment transferArchive =
@@ -105,8 +101,7 @@ public class AccountsManagerDeviceTransferIntegrationTest {
final Device device = mock(Device.class);
when(device.getId()).thenReturn(deviceId);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId);
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
@@ -119,106 +114,62 @@ public class AccountsManagerDeviceTransferIntegrationTest {
assertEquals(Optional.empty(), displacedFuture.join());
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli), recordUploadRegistrationId, transferArchive).join();
accountsManager.recordTransferArchiveUpload(account, deviceId, registrationId, transferArchive).join();
assertEquals(Optional.of(transferArchive), activeFuture.join());
}
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) {
@Test
void waitForTransferArchiveAlreadyAdded() {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
final int registrationId = 123;
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(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId);
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli), recordUploadRegistrationId, transferArchive).join();
accountsManager.recordTransferArchiveUpload(account, deviceId, registrationId, transferArchive).join();
assertEquals(Optional.of(transferArchive),
accountsManager.waitForTransferArchive(account, device, Duration.ofSeconds(5)).join());
}
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) {
@Test
void waitForErrorTransferArchive() {
final UUID accountIdentifier = UUID.randomUUID();
final byte deviceId = Device.PRIMARY_ID;
final int registrationId = 123;
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(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId);
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
accountsManager.recordTransferArchiveUpload(account, deviceId, recordUploadDeviceCreated.map(Instant::ofEpochMilli),
recordUploadRegistrationId, transferArchiveError).join();
accountsManager.recordTransferArchiveUpload(account, deviceId,
registrationId, transferArchiveError).join();
assertEquals(Optional.of(transferArchiveError),
accountsManager.waitForTransferArchive(account, device, Duration.ofSeconds(5)).join());
}
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) {
@Test
void waitForTransferArchiveTimeout() {
final UUID accountIdentifier = UUID.randomUUID();
final Device device = mock(Device.class);
when(device.getCreated()).thenReturn(recordUploadDeviceCreated.orElse(System.currentTimeMillis()));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(recordUploadRegistrationId.orElse(1));
when(device.getRegistrationId(IdentityType.ACI)).thenReturn(123);
final Account account = mock(Account.class);
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
@@ -227,16 +178,6 @@ 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

@@ -1497,61 +1497,6 @@ 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;

View File

@@ -472,7 +472,6 @@ public class AddRemoveDeviceIntegrationTest {
final DeviceInfo deviceInfo = maybeDeviceInfo.get();
assertEquals(updatedAccountAndDevice.second().getId(), deviceInfo.id());
assertEquals(updatedAccountAndDevice.second().getCreated(), deviceInfo.created());
assertEquals(updatedAccountAndDevice.second().getRegistrationId(IdentityType.ACI), deviceInfo.registrationId());
assertNotNull(deviceInfo.createdAtCiphertext());
}
@@ -521,7 +520,6 @@ public class AddRemoveDeviceIntegrationTest {
final DeviceInfo deviceInfo = maybeDeviceInfo.get();
assertEquals(updatedAccountAndDevice.second().getId(), deviceInfo.id());
assertEquals(updatedAccountAndDevice.second().getCreated(), deviceInfo.created());
assertEquals(updatedAccountAndDevice.second().getRegistrationId(IdentityType.ACI), deviceInfo.registrationId());
assertNotNull(deviceInfo.createdAtCiphertext());
}
@@ -546,13 +544,12 @@ public class AddRemoveDeviceIntegrationTest {
@ParameterizedTest
@CsvSource({
"10_000,1000,,false", // no pending messages
"10_000,1000,1000,true", // pending message at device creation
"10_000,1000,5999,true", // pending message right before device creation + fudge factor
"10_000,1000,6000,false", // pending message at device creation + fudge factor
"3000,5000,4000,false", // pending message after current time but before device creation
"10_000,,false", // no pending messages
"10_000,9999,true", // pending message right before now
"10_000,10_000,false", // pending message at now
"10_000,10_001,false", // pending message after now
})
void waitForMessageFetch(long currentTime, long deviceCreation, Long oldestMessage, boolean shouldWait)
void waitForMessageFetch(long currentTime, Long oldestMessage, boolean shouldWait)
throws InterruptedException {
final String number = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("US"),
@@ -564,7 +561,6 @@ public class AddRemoveDeviceIntegrationTest {
final String linkDeviceToken = accountsManager.generateLinkDeviceToken(UUID.randomUUID());
final String linkDeviceTokenIdentifier = AccountsManager.getLinkDeviceTokenIdentifier(linkDeviceToken);
clock.pin(Instant.ofEpochMilli(deviceCreation));
final Pair<Account, Device> updatedAccountAndDevice =
accountsManager.addDevice(account, new DeviceSpec(
"device-name".getBytes(StandardCharsets.UTF_8),
@@ -583,8 +579,6 @@ public class AddRemoveDeviceIntegrationTest {
linkDeviceToken)
.join();
assertEquals(updatedAccountAndDevice.second().getCreated(), deviceCreation);
when(messagesManager.getEarliestUndeliveredTimestampForDevice(account.getUuid(), account.getPrimaryDevice()))
.thenReturn(CompletableFuture.completedFuture(Optional.ofNullable(oldestMessage).map(Instant::ofEpochMilli)));