Add archive listing

This commit is contained in:
ravi-signal
2024-01-08 13:54:57 -06:00
committed by GitHub
parent 460dc6224c
commit b6ecfc7131
21 changed files with 798 additions and 258 deletions

View File

@@ -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,

View File

@@ -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));
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -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");
}
}