mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 19:48:01 +01:00
Add archive listing
This commit is contained in:
@@ -9,11 +9,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.Status;
|
||||
@@ -36,6 +38,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
@@ -55,8 +58,7 @@ public class BackupManagerTest {
|
||||
|
||||
@RegisterExtension
|
||||
public static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS,
|
||||
DynamoDbExtensionSchema.Tables.BACKUP_MEDIA);
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS);
|
||||
|
||||
private final TestClock testClock = TestClock.now();
|
||||
private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(testClock);
|
||||
@@ -66,15 +68,18 @@ public class BackupManagerTest {
|
||||
private final UUID aci = UUID.randomUUID();
|
||||
|
||||
private BackupManager backupManager;
|
||||
private BackupsDb backupsDb;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
reset(tusCredentialGenerator);
|
||||
testClock.unpin();
|
||||
this.backupsDb = new BackupsDb(
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS.tableName(),
|
||||
testClock);
|
||||
this.backupManager = new BackupManager(
|
||||
new BackupsDb(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS.tableName(), DynamoDbExtensionSchema.Tables.BACKUP_MEDIA.tableName(),
|
||||
testClock),
|
||||
backupsDb,
|
||||
backupAuthTestUtil.params,
|
||||
tusCredentialGenerator,
|
||||
remoteStorageManager,
|
||||
@@ -256,25 +261,23 @@ public class BackupManagerTest {
|
||||
.thenReturn(new MessageBackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
|
||||
when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
final MediaEncryptionParameters encryptionParams = new MediaEncryptionParameters(
|
||||
TestRandomUtil.nextBytes(32),
|
||||
TestRandomUtil.nextBytes(32),
|
||||
TestRandomUtil.nextBytes(16));
|
||||
|
||||
final BackupManager.StorageDescriptor copied = backupManager.copyToBackup(
|
||||
backupUser, 3, "abc", 100, mock(MediaEncryptionParameters.class),
|
||||
"def".getBytes(StandardCharsets.UTF_8)).join();
|
||||
backupUser, 3, "abc", 100, encryptionParams, "def".getBytes(StandardCharsets.UTF_8)).join();
|
||||
|
||||
assertThat(copied.cdn()).isEqualTo(3);
|
||||
assertThat(copied.key()).isEqualTo("def".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final Map<String, AttributeValue> backup = getBackupItem(backupUser);
|
||||
final long bytesUsed = AttributeValues.getLong(backup, BackupsDb.ATTR_MEDIA_BYTES_USED, 0L);
|
||||
assertThat(bytesUsed).isEqualTo(100);
|
||||
assertThat(bytesUsed).isEqualTo(encryptionParams.outputSize(100));
|
||||
|
||||
final long mediaCount = AttributeValues.getLong(backup, BackupsDb.ATTR_MEDIA_COUNT, 0L);
|
||||
assertThat(mediaCount).isEqualTo(1);
|
||||
|
||||
final Map<String, AttributeValue> mediaItem = getBackupMediaItem(backupUser,
|
||||
"def".getBytes(StandardCharsets.UTF_8));
|
||||
final long mediaLength = AttributeValues.getLong(mediaItem, BackupsDb.ATTR_LENGTH, 0L);
|
||||
assertThat(mediaLength).isEqualTo(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -292,12 +295,89 @@ public class BackupManagerTest {
|
||||
mock(MediaEncryptionParameters.class),
|
||||
"def".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
// usage should be rolled back after a known copy failure
|
||||
final Map<String, AttributeValue> backup = getBackupItem(backupUser);
|
||||
assertThat(AttributeValues.getLong(backup, BackupsDb.ATTR_MEDIA_BYTES_USED, -1L)).isEqualTo(0L);
|
||||
assertThat(AttributeValues.getLong(backup, BackupsDb.ATTR_MEDIA_COUNT, -1L)).isEqualTo(0L);
|
||||
}
|
||||
|
||||
final Map<String, AttributeValue> media = getBackupMediaItem(backupUser, "def".getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(media).isEmpty();
|
||||
@Test
|
||||
public void quotaEnforcementNoRecalculation() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
verifyNoInteractions(remoteStorageManager);
|
||||
|
||||
// set the backupsDb to be out of quota at t=0
|
||||
testClock.pin(Instant.ofEpochSecond(1));
|
||||
backupsDb.setMediaUsage(backupUser, new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES, 1000)).join();
|
||||
// check still within staleness bound (t=0 + 1 day - 1 sec)
|
||||
testClock.pin(Instant.ofEpochSecond(0)
|
||||
.plus(BackupManager.MAX_QUOTA_STALENESS)
|
||||
.minus(Duration.ofSeconds(1)));
|
||||
assertThat(backupManager.canStoreMedia(backupUser, 10).join()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quotaEnforcementRecalculation() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
final String backupMediaPrefix = "%s/%s/".formatted(
|
||||
BackupManager.encodeBackupIdForCdn(backupUser),
|
||||
BackupManager.MEDIA_DIRECTORY_NAME);
|
||||
|
||||
// on recalculation, say there's actually 10 bytes left
|
||||
when(remoteStorageManager.calculateBytesUsed(eq(backupMediaPrefix)))
|
||||
.thenReturn(
|
||||
CompletableFuture.completedFuture(new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - 10, 1000)));
|
||||
|
||||
// set the backupsDb to be out of quota at t=0
|
||||
testClock.pin(Instant.ofEpochSecond(0));
|
||||
backupsDb.setMediaUsage(backupUser, new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES, 1000)).join();
|
||||
testClock.pin(Instant.ofEpochSecond(0).plus(BackupManager.MAX_QUOTA_STALENESS));
|
||||
assertThat(backupManager.canStoreMedia(backupUser, 10).join()).isTrue();
|
||||
|
||||
// backupsDb should have the new value
|
||||
final BackupsDb.TimestampedUsageInfo info = backupsDb.getMediaUsage(backupUser).join();
|
||||
assertThat(info.lastRecalculationTime()).isEqualTo(
|
||||
Instant.ofEpochSecond(0).plus(BackupManager.MAX_QUOTA_STALENESS));
|
||||
assertThat(info.usageInfo().bytesUsed()).isEqualTo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - 10);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"true, 10, 10, true",
|
||||
"true, 10, 11, false",
|
||||
"true, 0, 1, false",
|
||||
"true, 0, 0, true",
|
||||
"false, 10, 10, true",
|
||||
"false, 10, 11, false",
|
||||
"false, 0, 1, false",
|
||||
"false, 0, 0, true",
|
||||
})
|
||||
public void quotaEnforcement(
|
||||
boolean recalculation,
|
||||
final long spaceLeft,
|
||||
final long mediaToAddSize,
|
||||
boolean shouldAccept) {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
final String backupMediaPrefix = "%s/%s/".formatted(
|
||||
BackupManager.encodeBackupIdForCdn(backupUser),
|
||||
BackupManager.MEDIA_DIRECTORY_NAME);
|
||||
|
||||
// set the backupsDb to be out of quota at t=0
|
||||
testClock.pin(Instant.ofEpochSecond(0));
|
||||
backupsDb.setMediaUsage(backupUser, new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - spaceLeft, 1000))
|
||||
.join();
|
||||
|
||||
if (recalculation) {
|
||||
testClock.pin(Instant.ofEpochSecond(0).plus(BackupManager.MAX_QUOTA_STALENESS).plus(Duration.ofSeconds(1)));
|
||||
when(remoteStorageManager.calculateBytesUsed(eq(backupMediaPrefix)))
|
||||
.thenReturn(CompletableFuture.completedFuture(
|
||||
new UsageInfo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES - spaceLeft, 1000)));
|
||||
}
|
||||
assertThat(backupManager.canStoreMedia(backupUser, mediaToAddSize).join()).isEqualTo(shouldAccept);
|
||||
if (recalculation && !shouldAccept) {
|
||||
// should have recalculated if we exceeded quota
|
||||
verify(remoteStorageManager, times(1)).calculateBytesUsed(anyString());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, AttributeValue> getBackupItem(final AuthenticatedBackupUser backupUser) {
|
||||
@@ -308,17 +388,6 @@ public class BackupManagerTest {
|
||||
.item();
|
||||
}
|
||||
|
||||
private Map<String, AttributeValue> getBackupMediaItem(final AuthenticatedBackupUser backupUser,
|
||||
final byte[] mediaId) {
|
||||
return DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem(GetItemRequest.builder()
|
||||
.tableName(DynamoDbExtensionSchema.Tables.BACKUP_MEDIA.tableName())
|
||||
.key(Map.of(
|
||||
BackupsDb.KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser.backupId())),
|
||||
BackupsDb.KEY_MEDIA_ID, AttributeValues.b(mediaId)))
|
||||
.build())
|
||||
.item();
|
||||
}
|
||||
|
||||
private void checkExpectedExpirations(
|
||||
final Instant expectedExpiration,
|
||||
final @Nullable Instant expectedMediaExpiration,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
public class BackupMediaEncrypterTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, 1, 2, 15, 16, 17, 63, 64, 65, 1023, 1024, 1025})
|
||||
public void sizeCalc() {
|
||||
final MediaEncryptionParameters params = new MediaEncryptionParameters(
|
||||
TestRandomUtil.nextBytes(32),
|
||||
TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(16));
|
||||
final BackupMediaEncrypter encrypter = new BackupMediaEncrypter(params);
|
||||
assertThat(params.outputSize(1)).isEqualTo(encrypter.outputSize(1));
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,13 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
|
||||
@@ -27,8 +30,7 @@ public class BackupsDbTest {
|
||||
|
||||
@RegisterExtension
|
||||
public static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS,
|
||||
DynamoDbExtensionSchema.Tables.BACKUP_MEDIA);
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS);
|
||||
|
||||
private final TestClock testClock = TestClock.now();
|
||||
private BackupsDb backupsDb;
|
||||
@@ -37,26 +39,10 @@ public class BackupsDbTest {
|
||||
public void setup() {
|
||||
testClock.unpin();
|
||||
backupsDb = new BackupsDb(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS.tableName(), DynamoDbExtensionSchema.Tables.BACKUP_MEDIA.tableName(),
|
||||
DynamoDbExtensionSchema.Tables.BACKUPS.tableName(),
|
||||
testClock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trackMediaIdempotent() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
this.backupsDb.trackMedia(backupUser, "abc".getBytes(StandardCharsets.UTF_8), 100).join();
|
||||
assertDoesNotThrow(() ->
|
||||
this.backupsDb.trackMedia(backupUser, "abc".getBytes(StandardCharsets.UTF_8), 100).join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trackMediaLengthChange() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
this.backupsDb.trackMedia(backupUser, "abc".getBytes(StandardCharsets.UTF_8), 100).join();
|
||||
CompletableFutureTestUtil.assertFailsWithCause(InvalidLengthException.class,
|
||||
this.backupsDb.trackMedia(backupUser, "abc".getBytes(StandardCharsets.UTF_8), 99));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trackMediaStats() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
@@ -64,27 +50,33 @@ public class BackupsDbTest {
|
||||
backupsDb.addMessageBackup(backupUser).join();
|
||||
int total = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
this.backupsDb.trackMedia(backupUser, Integer.toString(i).getBytes(StandardCharsets.UTF_8), i).join();
|
||||
this.backupsDb.trackMedia(backupUser, i).join();
|
||||
total += i;
|
||||
final BackupsDb.BackupDescription description = this.backupsDb.describeBackup(backupUser).join();
|
||||
assertThat(description.mediaUsedSpace().get()).isEqualTo(total);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
this.backupsDb.untrackMedia(backupUser, Integer.toString(i).getBytes(StandardCharsets.UTF_8), i).join();
|
||||
this.backupsDb.trackMedia(backupUser, -i).join();
|
||||
total -= i;
|
||||
final BackupsDb.BackupDescription description = this.backupsDb.describeBackup(backupUser).join();
|
||||
assertThat(description.mediaUsedSpace().get()).isEqualTo(total);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static byte[] hashedBackupId(final byte[] backupId) {
|
||||
try {
|
||||
return Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(backupId), 16);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void setUsage(boolean mediaAlreadyExists) {
|
||||
testClock.pin(Instant.ofEpochSecond(5));
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
|
||||
if (mediaAlreadyExists) {
|
||||
this.backupsDb.trackMedia(backupUser, 10).join();
|
||||
}
|
||||
backupsDb.setMediaUsage(backupUser, new UsageInfo( 113, 17)).join();
|
||||
final BackupsDb.TimestampedUsageInfo info = backupsDb.getMediaUsage(backupUser).join();
|
||||
assertThat(info.lastRecalculationTime()).isEqualTo(Instant.ofEpochSecond(5));
|
||||
assertThat(info.usageInfo().bytesUsed()).isEqualTo(113L);
|
||||
assertThat(info.usageInfo().numObjects()).isEqualTo(17L);
|
||||
}
|
||||
|
||||
private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupTier backupTier) {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
@@ -19,6 +23,8 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -33,9 +39,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.configuration.Cdn3StorageManagerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
@@ -62,7 +70,8 @@ public class Cdn3RemoteStorageManagerTest {
|
||||
Executors.newSingleThreadScheduledExecutor(),
|
||||
new CircuitBreakerConfiguration(),
|
||||
new RetryConfiguration(),
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(),
|
||||
new Cdn3StorageManagerConfiguration(wireMock.url("storage-manager/"), "clientId", "clientSecret"));
|
||||
|
||||
wireMock.stubFor(get(urlEqualTo("/cdn2/source/small"))
|
||||
.willReturn(aResponse()
|
||||
@@ -125,7 +134,9 @@ public class Cdn3RemoteStorageManagerTest {
|
||||
.toCompletableFuture().join();
|
||||
|
||||
final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody();
|
||||
assertThat(destBody.length).isEqualTo(new BackupMediaEncrypter(params).outputSize(LARGE.length()));
|
||||
assertThat(destBody.length)
|
||||
.isEqualTo(new BackupMediaEncrypter(params).outputSize(LARGE.length()))
|
||||
.isEqualTo(params.outputSize(LARGE.length()));
|
||||
assertThat(new String(decrypt(destBody), StandardCharsets.UTF_8)).isEqualTo(LARGE);
|
||||
}
|
||||
|
||||
@@ -176,4 +187,57 @@ public class Cdn3RemoteStorageManagerTest {
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY, "AES"), new IvParameterSpec(IV));
|
||||
return cipher.doFinal(encrypted, IV.length, encrypted.length - IV.length - mac.getMacLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void list() throws JsonProcessingException {
|
||||
wireMock.stubFor(get(urlPathEqualTo("/storage-manager/backups/"))
|
||||
.withQueryParam("prefix", equalTo("abc/"))
|
||||
.withQueryParam("limit", equalTo("3"))
|
||||
.withHeader(Cdn3RemoteStorageManager.CLIENT_ID_HEADER, equalTo("clientId"))
|
||||
.withHeader(Cdn3RemoteStorageManager.CLIENT_SECRET_HEADER, equalTo("clientSecret"))
|
||||
.willReturn(aResponse()
|
||||
.withBody(SystemMapper.jsonMapper().writeValueAsString(new Cdn3RemoteStorageManager.Cdn3ListResponse(
|
||||
List.of(
|
||||
new Cdn3RemoteStorageManager.Cdn3ListResponse.Entry("abc/x/y", 3),
|
||||
new Cdn3RemoteStorageManager.Cdn3ListResponse.Entry("abc/y", 4),
|
||||
new Cdn3RemoteStorageManager.Cdn3ListResponse.Entry("abc/z", 5)
|
||||
), "cursor")))));
|
||||
final RemoteStorageManager.ListResult result = remoteStorageManager
|
||||
.list("abc/", Optional.empty(), 3)
|
||||
.toCompletableFuture().join();
|
||||
assertThat(result.cursor()).get().isEqualTo("cursor");
|
||||
assertThat(result.objects()).hasSize(3);
|
||||
|
||||
// should strip the common prefix
|
||||
assertThat(result.objects()).isEqualTo(List.of(
|
||||
new RemoteStorageManager.ListResult.Entry("x/y", 3),
|
||||
new RemoteStorageManager.ListResult.Entry("y", 4),
|
||||
new RemoteStorageManager.ListResult.Entry("z", 5)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prefixMissing() throws JsonProcessingException {
|
||||
wireMock.stubFor(get(urlPathEqualTo("/storage-manager/backups/"))
|
||||
.willReturn(aResponse()
|
||||
.withBody(SystemMapper.jsonMapper().writeValueAsString(new Cdn3RemoteStorageManager.Cdn3ListResponse(
|
||||
List.of(new Cdn3RemoteStorageManager.Cdn3ListResponse.Entry("x", 3)),
|
||||
"cursor")))));
|
||||
CompletableFutureTestUtil.assertFailsWithCause(IOException.class,
|
||||
remoteStorageManager.list("abc/", Optional.empty(), 3).toCompletableFuture());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usage() throws JsonProcessingException {
|
||||
wireMock.stubFor(get(urlPathEqualTo("/storage-manager/usage"))
|
||||
.withQueryParam("prefix", equalTo("abc/"))
|
||||
.willReturn(aResponse()
|
||||
.withBody(SystemMapper.jsonMapper().writeValueAsString(new Cdn3RemoteStorageManager.UsageResponse(
|
||||
17,
|
||||
113)))));
|
||||
final UsageInfo result = remoteStorageManager.calculateBytesUsed("abc/")
|
||||
.toCompletableFuture()
|
||||
.join();
|
||||
assertThat(result.numObjects()).isEqualTo(17);
|
||||
assertThat(result.bytesUsed()).isEqualTo(113);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junitpioneer.jupiter.cartesian.CartesianTest;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
@@ -438,4 +439,43 @@ public class ArchiveControllerTest {
|
||||
).toList())));
|
||||
assertThat(response.getStatus()).isEqualTo(413);
|
||||
}
|
||||
|
||||
@CartesianTest
|
||||
public void list(
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
|
||||
throws VerificationFailedException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupTier.MEDIA, backupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(
|
||||
new AuthenticatedBackupUser(presentation.getBackupId(), BackupTier.MEDIA)));
|
||||
|
||||
final byte[] mediaId = TestRandomUtil.nextBytes(15);
|
||||
final Optional<String> expectedCursor = cursorProvided ? Optional.of("myCursor") : Optional.empty();
|
||||
final Optional<String> returnedCursor = cursorReturned ? Optional.of("newCursor") : Optional.empty();
|
||||
|
||||
when(backupManager.list(any(), eq(expectedCursor), eq(17)))
|
||||
.thenReturn(CompletableFuture.completedFuture(new BackupManager.ListMediaResult(
|
||||
List.of(new BackupManager.StorageDescriptorWithLength(1, mediaId, 100)),
|
||||
returnedCursor
|
||||
)));
|
||||
|
||||
WebTarget target = resources.getJerseyTest()
|
||||
.target("v1/archives/media/")
|
||||
.queryParam("limit", 17);
|
||||
if (cursorProvided) {
|
||||
target = target.queryParam("cursor", "myCursor");
|
||||
}
|
||||
final ArchiveController.ListResponse response = target
|
||||
.request()
|
||||
.header("X-Signal-ZK-Auth", Base64.getEncoder().encodeToString(presentation.serialize()))
|
||||
.header("X-Signal-ZK-Auth-Signature", "aaa")
|
||||
.get(ArchiveController.ListResponse.class);
|
||||
|
||||
assertThat(response.storedMediaObjects()).hasSize(1);
|
||||
assertThat(response.storedMediaObjects().get(0).objectLength()).isEqualTo(100);
|
||||
assertThat(response.storedMediaObjects().get(0).mediaId()).isEqualTo(mediaId);
|
||||
assertThat(response.cursor()).isEqualTo(returnedCursor.orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,18 +57,6 @@ public final class DynamoDbExtensionSchema {
|
||||
.attributeType(ScalarAttributeType.B).build()),
|
||||
Collections.emptyList(), Collections.emptyList()),
|
||||
|
||||
BACKUP_MEDIA("backups_media_test",
|
||||
BackupsDb.KEY_BACKUP_ID_HASH,
|
||||
BackupsDb.KEY_MEDIA_ID,
|
||||
List.of(
|
||||
AttributeDefinition.builder()
|
||||
.attributeName(BackupsDb.KEY_BACKUP_ID_HASH)
|
||||
.attributeType(ScalarAttributeType.B).build(),
|
||||
AttributeDefinition.builder()
|
||||
.attributeName(BackupsDb.KEY_MEDIA_ID)
|
||||
.attributeType(ScalarAttributeType.B).build()),
|
||||
Collections.emptyList(), Collections.emptyList()),
|
||||
|
||||
CLIENT_RELEASES("client_releases_test",
|
||||
ClientReleases.ATTR_PLATFORM,
|
||||
ClientReleases.ATTR_VERSION,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpUtilsTest {
|
||||
|
||||
@Test
|
||||
public void queryParameterStringPreservesOrder() {
|
||||
final String result = HttpUtils.queryParamString(List.of(
|
||||
Map.entry("a", "aval"),
|
||||
Map.entry("b", "bval1"),
|
||||
Map.entry("b", "bval2")
|
||||
));
|
||||
// https://url.spec.whatwg.org/#example-constructing-urlsearchparams allows multiple parameters with the same key
|
||||
// https://url.spec.whatwg.org/#example-searchparams-sort implies that the relative order of values for parameters
|
||||
// with the same key must be preserved
|
||||
assertThat(result).isEqualTo("?a=aval&b=bval1&b=bval2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParameterStringEncodesUnsafeChars() {
|
||||
final String result = HttpUtils.queryParamString(List.of(Map.entry("&k?e=y/!", "=v/a?l&u;e")));
|
||||
assertThat(result).isEqualTo("?%26k%3Fe%3Dy%2F%21=%3Dv%2Fa%3Fl%26u%3Be");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user