Add API endpoints for waiting for account restoration requests
This commit is contained in:
parent
5c4cafcb6f
commit
324913d2da
|
@ -29,6 +29,7 @@ import javax.annotation.Nullable;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.Max;
|
import javax.validation.constraints.Max;
|
||||||
import javax.validation.constraints.Min;
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
@ -56,6 +57,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RestoreAccountRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.LinkDeviceResponse;
|
import org.whispersystems.textsecuregcm.entities.LinkDeviceResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest;
|
import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKeySignatureValidator;
|
import org.whispersystems.textsecuregcm.entities.PreKeySignatureValidator;
|
||||||
|
@ -64,6 +66,7 @@ import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
||||||
import org.whispersystems.textsecuregcm.entities.SetPublicKeyRequest;
|
import org.whispersystems.textsecuregcm.entities.SetPublicKeyRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.TransferArchiveUploadedRequest;
|
import org.whispersystems.textsecuregcm.entities.TransferArchiveUploadedRequest;
|
||||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
|
@ -437,6 +440,70 @@ public class DeviceController {
|
||||||
return isDowngrade;
|
return isDowngrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/restore_account/{token}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Signals that a new device is requesting restoration of account data by some method",
|
||||||
|
description = """
|
||||||
|
Signals that a new device is requesting restoration of account data by some method. Devices waiting via the
|
||||||
|
"wait for 'restore account' request" endpoint will be notified that the request has been issued.
|
||||||
|
""")
|
||||||
|
@ApiResponse(responseCode = "204", description = "Success")
|
||||||
|
@ApiResponse(responseCode = "422", description = "The request object could not be parsed or was otherwise invalid")
|
||||||
|
@ApiResponse(responseCode = "429", description = "Rate-limited; try again after the prescribed delay")
|
||||||
|
@RateLimitedByIp(RateLimiters.For.RECORD_DEVICE_TRANSFER_REQUEST)
|
||||||
|
public CompletionStage<Void> recordRestoreAccountRequest(
|
||||||
|
@PathParam("token")
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 64)
|
||||||
|
@Schema(description = "A randomly-generated token identifying the request for device-to-device transfer.",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
maximum = "64") final String token,
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
final RestoreAccountRequest restoreAccountRequest) {
|
||||||
|
|
||||||
|
return accounts.recordRestoreAccountRequest(token, restoreAccountRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/restore_account/{token}")
|
||||||
|
@Operation(summary = "Wait for 'restore account' request")
|
||||||
|
@ApiResponse(responseCode = "200", description = "A 'restore account' request was received for the given token",
|
||||||
|
content = @Content(schema = @Schema(implementation = RestoreAccountRequest.class)))
|
||||||
|
@ApiResponse(responseCode = "204", description = "No 'restore account' request for the given token was received before the call completed; clients may repeat the call to continue waiting")
|
||||||
|
@ApiResponse(responseCode = "400", description = "The given token or timeout was invalid")
|
||||||
|
@ApiResponse(responseCode = "429", description = "Rate-limited; try again after the prescribed delay")
|
||||||
|
@RateLimitedByIp(RateLimiters.For.WAIT_FOR_DEVICE_TRANSFER_REQUEST)
|
||||||
|
public CompletionStage<Response> waitForDeviceTransferRequest(
|
||||||
|
@PathParam("token")
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 64)
|
||||||
|
@Schema(description = "A randomly-generated token identifying the request for device-to-device transfer.",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
maximum = "64") final String token,
|
||||||
|
|
||||||
|
@QueryParam("timeout")
|
||||||
|
@DefaultValue("30")
|
||||||
|
@Min(1)
|
||||||
|
@Max(3600)
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
|
minimum = "1",
|
||||||
|
maximum = "3600",
|
||||||
|
description = """
|
||||||
|
The amount of time (in seconds) to wait for a response. If a transfer archive for the authenticated
|
||||||
|
device is not available within the given amount of time, this endpoint will return a status of HTTP/204.
|
||||||
|
""") final int timeoutSeconds) {
|
||||||
|
|
||||||
|
return accounts.waitForRestoreAccountRequest(token, Duration.ofSeconds(timeoutSeconds))
|
||||||
|
.thenApply(maybeRequestReceived -> maybeRequestReceived
|
||||||
|
.map(restoreAccountRequest -> Response.status(Response.Status.OK).entity(restoreAccountRequest).build())
|
||||||
|
.orElseGet(() -> Response.status(Response.Status.NO_CONTENT).build()));
|
||||||
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Schema(description = """
|
||||||
|
Represents a request from a new device to restore account data by some method.
|
||||||
|
""")
|
||||||
|
public record RestoreAccountRequest(
|
||||||
|
@NotNull
|
||||||
|
@Schema(description = "The method by which the new device has requested account data restoration")
|
||||||
|
Method method) {
|
||||||
|
|
||||||
|
public enum Method {
|
||||||
|
@Schema(description = "Restore account data from a remote message history backup")
|
||||||
|
REMOTE_BACKUP,
|
||||||
|
|
||||||
|
@Schema(description = "Restore account data from a local backup archive")
|
||||||
|
LOCAL_BACKUP,
|
||||||
|
|
||||||
|
@Schema(description = "Restore account data via direct device-to-device transfer")
|
||||||
|
DEVICE_TRANSFER,
|
||||||
|
|
||||||
|
@Schema(description = "Do not restore account data")
|
||||||
|
DECLINE,
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,8 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
|
||||||
WAIT_FOR_LINKED_DEVICE("waitForLinkedDevice", true, new RateLimiterConfig(10, Duration.ofSeconds(30))),
|
WAIT_FOR_LINKED_DEVICE("waitForLinkedDevice", true, new RateLimiterConfig(10, Duration.ofSeconds(30))),
|
||||||
UPLOAD_TRANSFER_ARCHIVE("uploadTransferArchive", true, new RateLimiterConfig(10, Duration.ofMinutes(1))),
|
UPLOAD_TRANSFER_ARCHIVE("uploadTransferArchive", true, new RateLimiterConfig(10, Duration.ofMinutes(1))),
|
||||||
WAIT_FOR_TRANSFER_ARCHIVE("waitForTransferArchive", true, new RateLimiterConfig(10, Duration.ofSeconds(30))),
|
WAIT_FOR_TRANSFER_ARCHIVE("waitForTransferArchive", true, new RateLimiterConfig(10, Duration.ofSeconds(30))),
|
||||||
|
RECORD_DEVICE_TRANSFER_REQUEST("recordDeviceTransferRequest", true, new RateLimiterConfig(10, Duration.ofMillis(100))),
|
||||||
|
WAIT_FOR_DEVICE_TRANSFER_REQUEST("waitForDeviceTransferRequest", true, new RateLimiterConfig(10, Duration.ofMillis(100))),
|
||||||
;
|
;
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
|
@ -68,6 +68,7 @@ import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfigurati
|
||||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RestoreAccountRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
||||||
|
@ -142,6 +143,9 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||||
private final Map<TimestampedDeviceIdentifier, CompletableFuture<Optional<RemoteAttachment>>> waitForTransferArchiveFuturesByDeviceIdentifier =
|
private final Map<TimestampedDeviceIdentifier, CompletableFuture<Optional<RemoteAttachment>>> waitForTransferArchiveFuturesByDeviceIdentifier =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Map<String, CompletableFuture<Optional<RestoreAccountRequest>>> waitForRestoreAccountRequestFuturesByToken =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final int SHA256_HASH_LENGTH = getSha256MessageDigest().getDigestLength();
|
private static final int SHA256_HASH_LENGTH = getSha256MessageDigest().getDigestLength();
|
||||||
|
|
||||||
private static final Duration RECENTLY_ADDED_DEVICE_TTL = Duration.ofHours(1);
|
private static final Duration RECENTLY_ADDED_DEVICE_TTL = Duration.ofHours(1);
|
||||||
|
@ -152,6 +156,10 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||||
private static final String TRANSFER_ARCHIVE_PREFIX = "transfer_archive::";
|
private static final String TRANSFER_ARCHIVE_PREFIX = "transfer_archive::";
|
||||||
private static final String TRANSFER_ARCHIVE_KEYSPACE_PATTERN = "__keyspace@0__:" + TRANSFER_ARCHIVE_PREFIX + "*";
|
private static final String TRANSFER_ARCHIVE_KEYSPACE_PATTERN = "__keyspace@0__:" + TRANSFER_ARCHIVE_PREFIX + "*";
|
||||||
|
|
||||||
|
private static final Duration RESTORE_ACCOUNT_REQUEST_TTL = Duration.ofHours(1);
|
||||||
|
private static final String RESTORE_ACCOUNT_REQUEST_PREFIX = "restore_account::";
|
||||||
|
private static final String RESTORE_ACCOUNT_REQUEST_KEYSPACE_PATTERN = "__keyspace@0__:" + RESTORE_ACCOUNT_REQUEST_PREFIX + "*";
|
||||||
|
|
||||||
private static final ObjectWriter ACCOUNT_REDIS_JSON_WRITER = SystemMapper.jsonMapper()
|
private static final ObjectWriter ACCOUNT_REDIS_JSON_WRITER = SystemMapper.jsonMapper()
|
||||||
.writer(SystemMapper.excludingField(Account.class, List.of("uuid")));
|
.writer(SystemMapper.excludingField(Account.class, List.of("uuid")));
|
||||||
|
|
||||||
|
@ -238,7 +246,8 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||||
public void start() {
|
public void start() {
|
||||||
pubSubConnection.usePubSubConnection(connection -> {
|
pubSubConnection.usePubSubConnection(connection -> {
|
||||||
connection.addListener(this);
|
connection.addListener(this);
|
||||||
connection.sync().psubscribe(LINKED_DEVICE_KEYSPACE_PATTERN, TRANSFER_ARCHIVE_KEYSPACE_PATTERN);
|
connection.sync().psubscribe(LINKED_DEVICE_KEYSPACE_PATTERN, TRANSFER_ARCHIVE_KEYSPACE_PATTERN,
|
||||||
|
RESTORE_ACCOUNT_REQUEST_KEYSPACE_PATTERN);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1496,6 +1505,44 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||||
":" + destinationDeviceCreationTimestamp.toEpochMilli();
|
":" + destinationDeviceCreationTimestamp.toEpochMilli();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Optional<RestoreAccountRequest>> waitForRestoreAccountRequest(final String token, final Duration timeout) {
|
||||||
|
return waitForPubSubKey(waitForRestoreAccountRequestFuturesByToken,
|
||||||
|
token,
|
||||||
|
getRestoreAccountRequestKey(token),
|
||||||
|
timeout,
|
||||||
|
this::handleRestoreAccountRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> recordRestoreAccountRequest(final String token, final RestoreAccountRequest restoreAccountRequest) {
|
||||||
|
final String key = getRestoreAccountRequestKey(token);
|
||||||
|
|
||||||
|
final String requestJson;
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestJson = SystemMapper.jsonMapper().writeValueAsString(restoreAccountRequest);
|
||||||
|
} catch (final JsonProcessingException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubSubRedisClient.withConnection(connection ->
|
||||||
|
connection.async().set(key, requestJson, SetArgs.Builder.ex(RESTORE_ACCOUNT_REQUEST_TTL)))
|
||||||
|
.thenRun(Util.NOOP)
|
||||||
|
.toCompletableFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRestoreAccountRequest(final CompletableFuture<Optional<RestoreAccountRequest>> future, final String transferRequestJson) {
|
||||||
|
try {
|
||||||
|
future.complete(Optional.of(SystemMapper.jsonMapper().readValue(transferRequestJson, RestoreAccountRequest.class)));
|
||||||
|
} catch (final JsonProcessingException e) {
|
||||||
|
logger.error("Could not parse device transfer request JSON", e);
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getRestoreAccountRequestKey(final String token) {
|
||||||
|
return RESTORE_ACCOUNT_REQUEST_PREFIX + token;
|
||||||
|
}
|
||||||
|
|
||||||
private <K, T> CompletableFuture<Optional<T>> waitForPubSubKey(final Map<K, CompletableFuture<Optional<T>>> futureMap,
|
private <K, T> CompletableFuture<Optional<T>> waitForPubSubKey(final Map<K, CompletableFuture<Optional<T>>> futureMap,
|
||||||
final K mapKey,
|
final K mapKey,
|
||||||
final String redisKey,
|
final String redisKey,
|
||||||
|
@ -1564,6 +1611,14 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||||
} catch (final IllegalArgumentException e) {
|
} catch (final IllegalArgumentException e) {
|
||||||
logger.error("Could not parse timestamped device identifier", e);
|
logger.error("Could not parse timestamped device identifier", e);
|
||||||
}
|
}
|
||||||
|
} else if (RESTORE_ACCOUNT_REQUEST_KEYSPACE_PATTERN.equalsIgnoreCase(pattern) && "set".equalsIgnoreCase(message)) {
|
||||||
|
// The `- 1` here compensates for the '*' in the pattern
|
||||||
|
final String token = channel.substring(RESTORE_ACCOUNT_REQUEST_KEYSPACE_PATTERN.length() - 1);
|
||||||
|
|
||||||
|
Optional.ofNullable(waitForRestoreAccountRequestFuturesByToken.remove(token))
|
||||||
|
.ifPresent(future -> pubSubRedisClient.withConnection(connection -> connection.async().get(
|
||||||
|
getRestoreAccountRequestKey(token)))
|
||||||
|
.thenAccept(requestJson -> handleRestoreAccountRequest(future, requestJson)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
@ -61,6 +62,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RestoreAccountRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.LinkDeviceResponse;
|
import org.whispersystems.textsecuregcm.entities.LinkDeviceResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||||
|
@ -1019,7 +1021,7 @@ class DeviceControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@ValueSource(ints = {0, -1, 3601})
|
||||||
void waitForLinkedDeviceBadTimeout(final int timeoutSeconds) {
|
void waitForLinkedDeviceBadTimeout(final int timeoutSeconds) {
|
||||||
final String tokenIdentifier = Base64.getUrlEncoder().withoutPadding().encodeToString(new byte[32]);
|
final String tokenIdentifier = Base64.getUrlEncoder().withoutPadding().encodeToString(new byte[32]);
|
||||||
|
|
||||||
|
@ -1034,10 +1036,6 @@ class DeviceControllerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Integer> waitForLinkedDeviceBadTimeout() {
|
|
||||||
return List.of(0, -1, 3601);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void waitForLinkedDeviceBadTokenIdentifierLength(final String tokenIdentifier) {
|
void waitForLinkedDeviceBadTokenIdentifierLength(final String tokenIdentifier) {
|
||||||
|
@ -1194,7 +1192,7 @@ class DeviceControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@ValueSource(ints = {0, -1, 3601})
|
||||||
void waitForTransferArchiveBadTimeout(final int timeoutSeconds) {
|
void waitForTransferArchiveBadTimeout(final int timeoutSeconds) {
|
||||||
try (final Response response = resources.getJerseyTest()
|
try (final Response response = resources.getJerseyTest()
|
||||||
.target("/v1/devices/transfer_archive/")
|
.target("/v1/devices/transfer_archive/")
|
||||||
|
@ -1207,10 +1205,6 @@ class DeviceControllerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Integer> waitForTransferArchiveBadTimeout() {
|
|
||||||
return List.of(0, -1, 3601);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void waitForTransferArchiveRateLimited() {
|
void waitForTransferArchiveRateLimited() {
|
||||||
when(rateLimiter.validateAsync(anyString()))
|
when(rateLimiter.validateAsync(anyString()))
|
||||||
|
@ -1225,4 +1219,101 @@ class DeviceControllerTest {
|
||||||
assertEquals(429, response.getStatus());
|
assertEquals(429, response.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordRestoreAccountRequest() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest =
|
||||||
|
new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP);
|
||||||
|
|
||||||
|
when(accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.request()
|
||||||
|
.put(Entity.json(restoreAccountRequest))) {
|
||||||
|
|
||||||
|
assertEquals(204, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordRestoreAccountRequestBadToken() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(128);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest =
|
||||||
|
new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP);
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.request()
|
||||||
|
.put(Entity.json(restoreAccountRequest))) {
|
||||||
|
|
||||||
|
assertEquals(400, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordRestoreAccountRequestInvalidRequest() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest = new RestoreAccountRequest(null);
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.request()
|
||||||
|
.put(Entity.json(restoreAccountRequest))) {
|
||||||
|
|
||||||
|
assertEquals(422, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitForDeviceTransferRequest() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest =
|
||||||
|
new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP);
|
||||||
|
|
||||||
|
when(accountsManager.waitForRestoreAccountRequest(eq(token), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(restoreAccountRequest)));
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.request()
|
||||||
|
.get()) {
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
assertEquals(restoreAccountRequest, response.readEntity(RestoreAccountRequest.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitForDeviceTransferRequestNoRequestIssued() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
|
||||||
|
when(accountsManager.waitForRestoreAccountRequest(eq(token), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.request()
|
||||||
|
.get()) {
|
||||||
|
|
||||||
|
assertEquals(204, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = {0, -1, 3601})
|
||||||
|
void waitForDeviceTransferRequestBadTimeout(final int timeoutSeconds) {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/devices/restore_account/" + token)
|
||||||
|
.queryParam("timeout", timeoutSeconds)
|
||||||
|
.request()
|
||||||
|
.get()) {
|
||||||
|
|
||||||
|
assertEquals(400, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.storage;
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.RestoreAccountRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
||||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
|
@ -34,7 +36,7 @@ import static org.mockito.Mockito.when;
|
||||||
// ThreadMode.SEPARATE_THREAD protects against hangs in the remote Redis calls, as this mode allows the test code to be
|
// ThreadMode.SEPARATE_THREAD protects against hangs in the remote Redis calls, as this mode allows the test code to be
|
||||||
// preempted by the timeout check
|
// preempted by the timeout check
|
||||||
@Timeout(value = 5, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
@Timeout(value = 5, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||||
public class AccountsManagerTransferArchiveIntegrationTest {
|
public class AccountsManagerDeviceTransferIntegrationTest {
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
static final RedisServerExtension PUBSUB_SERVER_EXTENSION = RedisServerExtension.builder().build();
|
static final RedisServerExtension PUBSUB_SERVER_EXTENSION = RedisServerExtension.builder().build();
|
||||||
|
@ -144,4 +146,41 @@ public class AccountsManagerTransferArchiveIntegrationTest {
|
||||||
assertEquals(Optional.empty(),
|
assertEquals(Optional.empty(),
|
||||||
accountsManager.waitForTransferArchive(account, device, Duration.ofMillis(1)).join());
|
accountsManager.waitForTransferArchive(account, device, Duration.ofMillis(1)).join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitForRestoreAccountRequest() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest =
|
||||||
|
new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER);
|
||||||
|
|
||||||
|
final CompletableFuture<Optional<RestoreAccountRequest>> displacedFuture =
|
||||||
|
accountsManager.waitForRestoreAccountRequest(token, Duration.ofSeconds(5));
|
||||||
|
|
||||||
|
final CompletableFuture<Optional<RestoreAccountRequest>> activeFuture =
|
||||||
|
accountsManager.waitForRestoreAccountRequest(token, Duration.ofSeconds(5));
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), displacedFuture.join());
|
||||||
|
|
||||||
|
accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest).join();
|
||||||
|
|
||||||
|
assertEquals(Optional.of(restoreAccountRequest), activeFuture.join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitForRestoreAccountRequestAlreadyRequested() {
|
||||||
|
final String token = RandomStringUtils.randomAlphanumeric(16);
|
||||||
|
final RestoreAccountRequest restoreAccountRequest =
|
||||||
|
new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER);
|
||||||
|
|
||||||
|
accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest).join();
|
||||||
|
|
||||||
|
assertEquals(Optional.of(restoreAccountRequest),
|
||||||
|
accountsManager.waitForRestoreAccountRequest(token, Duration.ofSeconds(5)).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void waitForRestoreAccountRequestTimeout() {
|
||||||
|
assertEquals(Optional.empty(),
|
||||||
|
accountsManager.waitForRestoreAccountRequest(RandomStringUtils.randomAlphanumeric(16), Duration.ofMillis(1)).join());
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue