Convert backup services to use new error model

This commit is contained in:
ravi-signal
2026-01-23 15:25:15 -05:00
committed by GitHub
parent 12d9637f21
commit 5b1d4ce95e
28 changed files with 1105 additions and 764 deletions

View File

@@ -6,7 +6,6 @@
package org.whispersystems.textsecuregcm.backup;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
@@ -21,8 +20,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -107,7 +104,7 @@ public class BackupAuthManagerTest {
}
@Test
void commitBackupId() throws RateLimitExceededException {
void commitBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
final BackupAuthManager authManager = create();
final Account account = mock(Account.class);
@@ -158,15 +155,13 @@ public class BackupAuthManagerTest {
linkedDevice(),
Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)),
Optional.of(backupAuthTestUtil.getRequest(mediaBackupKey, aci)));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(commit)
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.PERMISSION_DENIED);
assertThatExceptionOfType(BackupPermissionException.class)
.isThrownBy(commit);
}
@CartesianTest
void paidTierCredentialViaConfiguration(@CartesianTest.Enum final BackupCredentialType credentialType)
throws VerificationFailedException {
throws VerificationFailedException, BackupNotFoundException {
final BackupAuthManager authManager = create(BackupLevel.PAID, rateLimiter(aci, false, false));
final byte[] backupKey = switch (credentialType) {
@@ -196,7 +191,7 @@ public class BackupAuthManagerTest {
@CartesianTest
void getBackupAuthCredentials(@CartesianTest.Enum final BackupLevel backupLevel,
@CartesianTest.Enum final BackupCredentialType credentialType) {
@CartesianTest.Enum final BackupCredentialType credentialType) throws BackupNotFoundException {
final BackupAuthManager authManager = create();
@@ -217,16 +212,14 @@ public class BackupAuthManagerTest {
final Account account = new MockAccountBuilder().build();
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() ->
authManager.getBackupAuthCredentials(account, credentialType, range(Duration.ofDays(1))))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.NOT_FOUND);
assertThatExceptionOfType(BackupNotFoundException.class)
.isThrownBy(() -> authManager.getBackupAuthCredentials(account, credentialType, range(Duration.ofDays(1))));
}
@CartesianTest
void getReceiptCredentials(@CartesianTest.Enum final BackupLevel backupLevel,
@CartesianTest.Enum final BackupCredentialType credentialType) throws VerificationFailedException {
@CartesianTest.Enum final BackupCredentialType credentialType)
throws VerificationFailedException, BackupNotFoundException {
final BackupAuthManager authManager = create();
final byte[] backupKey = switch (credentialType) {
@@ -259,7 +252,7 @@ public class BackupAuthManagerTest {
}
@Test
void expiringBackupPayment() throws VerificationFailedException {
void expiringBackupPayment() throws VerificationFailedException, BackupNotFoundException {
clock.pin(Instant.ofEpochSecond(1));
final Instant day4 = Instant.EPOCH.plus(Duration.ofDays(4));
@@ -292,7 +285,7 @@ public class BackupAuthManagerTest {
}
@Test
void expiredBackupPayment() {
void expiredBackupPayment() throws BackupNotFoundException {
final Instant day1 = Instant.EPOCH.plus(Duration.ofDays(1));
final Instant day2 = Instant.EPOCH.plus(Duration.ofDays(2));
final Instant day3 = Instant.EPOCH.plus(Duration.ofDays(3));
@@ -335,7 +328,8 @@ public class BackupAuthManagerTest {
@Test
void redeemReceipt() throws InvalidInputException, VerificationFailedException {
void redeemReceipt()
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
final BackupAuthManager authManager = create();
final Account account = new MockAccountBuilder()
@@ -358,15 +352,13 @@ public class BackupAuthManagerTest {
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
.thenReturn(CompletableFuture.completedFuture(true));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() ->
authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.ABORTED);
assertThatExceptionOfType(BackupMissingIdCommitmentException.class)
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)));
}
@Test
void mergeRedemptions() throws InvalidInputException, VerificationFailedException {
void mergeRedemptions()
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
final Instant newExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
final Instant existingExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1)).plus(Duration.ofSeconds(1));
@@ -396,10 +388,8 @@ public class BackupAuthManagerTest {
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
clock.pin(expirationTime.plus(Duration.ofSeconds(1)));
final BackupAuthManager authManager = create();
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(3, expirationTime)))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
assertThatExceptionOfType(BackupBadReceiptException.class)
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(3, expirationTime)));
verifyNoInteractions(accountsManager);
verifyNoInteractions(redeemedReceiptsManager);
}
@@ -410,11 +400,8 @@ public class BackupAuthManagerTest {
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
clock.pin(expirationTime.plus(Duration.ofSeconds(1)));
final BackupAuthManager authManager = create();
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() ->
authManager.redeemReceipt(mock(Account.class), receiptPresentation(level, expirationTime)))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
assertThatExceptionOfType(BackupBadReceiptException.class)
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(level, expirationTime)));
verifyNoInteractions(accountsManager);
verifyNoInteractions(redeemedReceiptsManager);
}
@@ -423,10 +410,8 @@ public class BackupAuthManagerTest {
void redeemInvalidPresentation() throws InvalidInputException, VerificationFailedException {
final BackupAuthManager authManager = create();
final ReceiptCredentialPresentation invalid = receiptPresentation(ServerSecretParams.generate(), 3L, Instant.EPOCH);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), invalid))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
assertThatExceptionOfType(BackupBadReceiptException.class)
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), invalid));
verifyNoInteractions(accountsManager);
verifyNoInteractions(redeemedReceiptsManager);
}
@@ -444,10 +429,8 @@ public class BackupAuthManagerTest {
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
.thenReturn(CompletableFuture.completedFuture(false));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
assertThatExceptionOfType(BackupBadReceiptException.class)
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)));
verifyNoInteractions(accountsManager);
}
@@ -544,10 +527,8 @@ public class BackupAuthManagerTest {
authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential);
if (messageChange == CredentialChangeType.NO_UPDATE && mediaChange == CredentialChangeType.NO_UPDATE) {
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(commit)
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
assertThatExceptionOfType(BackupInvalidArgumentException.class)
.isThrownBy(commit);
} else if (expectRateLimit) {
assertThatExceptionOfType(RateLimitExceededException.class).isThrownBy(commit);
} else {

View File

@@ -14,6 +14,7 @@ import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.Assertions;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
@@ -76,6 +77,10 @@ public class BackupAuthTestUtil {
});
final RedemptionRange redemptionRange;
redemptionRange = RedemptionRange.inclusive(clock, redemptionStart, redemptionEnd);
return issuer.getBackupAuthCredentials(account, credentialType, redemptionRange);
try {
return issuer.getBackupAuthCredentials(account, credentialType, redemptionRange);
} catch (BackupNotFoundException e) {
return Assertions.fail("Backup credential request not found even though we set one");
}
}
}

View File

@@ -24,8 +24,6 @@ import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
import io.dropwizard.util.DataSize;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -184,11 +182,7 @@ public class BackupManagerTest {
() -> BackupManager.checkBackupLevel(backupUser, requiredLevel);
if (expectException) {
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(checkBackupLevel)
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.PERMISSION_DENIED);
assertThatExceptionOfType(BackupPermissionException.class).isThrownBy(checkBackupLevel);
} else {
assertThatNoException().isThrownBy(checkBackupLevel);
}
@@ -212,11 +206,7 @@ public class BackupManagerTest {
() -> BackupManager.checkBackupCredentialType(backupUser, requiredCredentialType);
if (expectException) {
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(checkCredentialType)
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.UNAUTHENTICATED);
assertThatExceptionOfType(BackupWrongCredentialTypeException.class).isThrownBy(checkCredentialType);
} else {
assertThatNoException().isThrownBy(checkCredentialType);
}
@@ -224,7 +214,7 @@ public class BackupManagerTest {
@ParameterizedTest
@EnumSource
public void createBackup(final BackupLevel backupLevel) {
public void createBackup(final BackupLevel backupLevel) throws BackupException {
final Instant now = Instant.ofEpochSecond(Duration.ofDays(1).getSeconds());
testClock.pin(now);
@@ -253,9 +243,8 @@ public class BackupManagerTest {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, backupLevel);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.createMessageBackupUploadDescriptor(backupUser))
.matches(exception -> exception.getStatus().getCode() == Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
.isThrownBy(() -> backupManager.createMessageBackupUploadDescriptor(backupUser));
}
@Test
@@ -270,26 +259,20 @@ public class BackupManagerTest {
@Test
public void createTemporaryMediaAttachmentWrongTier() {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.PERMISSION_DENIED);
assertThatExceptionOfType(BackupPermissionException.class)
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser));
}
@Test
public void createTemporaryMediaAttachmentWrongCredentialType() {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.UNAUTHENTICATED);
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser));
}
@ParameterizedTest
@EnumSource
public void ttlRefresh(final BackupLevel backupLevel) {
public void ttlRefresh(final BackupLevel backupLevel) throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
@@ -311,7 +294,7 @@ public class BackupManagerTest {
@ParameterizedTest
@EnumSource
public void createBackupRefreshesTtl(final BackupLevel backupLevel) {
public void createBackupRefreshesTtl(final BackupLevel backupLevel) throws BackupException {
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
final Instant tnext = tstart.plus(Duration.ofDays(1));
@@ -340,19 +323,16 @@ public class BackupManagerTest {
final ECKeyPair keyPair = ECKeyPair.generate();
// haven't set a public key yet, but should fail before hitting the database anyway
assertThatExceptionOfType(StatusRuntimeException.class)
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(
invalidPresentation,
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
null))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
null));
}
@Test
public void invalidPresentationCorrectSignature() throws VerificationFailedException {
public void invalidPresentationCorrectSignature() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.FREE, backupKey, aci);
final BackupAuthCredentialPresentation invalidPresentation = backupAuthTestUtil.getPresentation(
@@ -365,14 +345,11 @@ public class BackupManagerTest {
keyPair.getPrivateKey().calculateSignature(presentation.serialize()),
keyPair.getPublicKey());
assertThatExceptionOfType(StatusRuntimeException.class)
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(
invalidPresentation,
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
null))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
null));
}
@Test
@@ -384,15 +361,12 @@ public class BackupManagerTest {
final byte[] signature = keyPair.getPrivateKey().calculateSignature(presentation.serialize());
// haven't set a public key yet
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, signature, null))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, signature, null));
}
@Test
public void mismatchedPublicKey() throws VerificationFailedException {
public void mismatchedPublicKey() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.FREE, backupKey, aci);
@@ -404,18 +378,15 @@ public class BackupManagerTest {
backupManager.setPublicKey(presentation, signature1, keyPair1.getPublicKey());
// shouldn't be able to set a different public key
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() ->
backupManager.setPublicKey(presentation, signature2, keyPair2.getPublicKey()))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.setPublicKey(presentation, signature2, keyPair2.getPublicKey()));
// should be able to set the same public key again (noop)
backupManager.setPublicKey(presentation, signature1, keyPair1.getPublicKey());
}
@Test
public void signatureValidation() throws VerificationFailedException {
public void signatureValidation() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.FREE, backupKey, aci);
@@ -427,19 +398,14 @@ public class BackupManagerTest {
wrongSignature[1] += 1;
// shouldn't be able to set a public key with an invalid signature
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.setPublicKey(presentation, wrongSignature, keyPair.getPublicKey()))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.setPublicKey(presentation, wrongSignature, keyPair.getPublicKey()));
backupManager.setPublicKey(presentation, signature, keyPair.getPublicKey());
// shouldn't be able to authenticate with an invalid signature
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, wrongSignature, null))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, wrongSignature, null));
// correct signature
final AuthenticatedBackupUser user = backupManager.authenticateBackupUser(presentation, signature, null);
@@ -448,7 +414,7 @@ public class BackupManagerTest {
}
@Test
public void credentialExpiration() throws VerificationFailedException {
public void credentialExpiration() throws VerificationFailedException, BackupException {
// credential for 1 day after epoch
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(1)));
@@ -468,15 +434,12 @@ public class BackupManagerTest {
// should be rejected the day after that
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(3)));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null));
}
@Test
public void copySuccess() {
public void copySuccess() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final CopyResult copied = copy(backupUser);
@@ -493,7 +456,7 @@ public class BackupManagerTest {
}
@Test
public void copyUsageCheckpoints() throws InterruptedException {
public void copyUsageCheckpoints() throws InterruptedException, BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
backupsDb.setMediaUsage(backupUser, new UsageInfo(0, 0)).join();
@@ -512,7 +475,7 @@ public class BackupManagerTest {
.thenReturn(slow);
final ArrayBlockingQueue<CopyResult> copyResults = new ArrayBlockingQueue<>(100);
final CompletableFuture<Void> future = backupManager
.copyToBackup(backupUser, toCopy)
.copyToBackup(backupManager.getCopyQuota(backupUser, toCopy))
.doOnNext(copyResults::add).then().toFuture();
for (int i = 0; i < slowIndex; i++) {
@@ -540,7 +503,7 @@ public class BackupManagerTest {
}
@Test
public void copyFailure() {
public void copyFailure() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
assertThat(copyError(backupUser, new SourceObjectNotFoundException()).outcome())
.isEqualTo(CopyResult.Outcome.SOURCE_NOT_FOUND);
@@ -552,7 +515,7 @@ public class BackupManagerTest {
}
@Test
public void copyPartialSuccess() {
public void copyPartialSuccess() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final List<CopyParameters> toCopy = List.of(
new CopyParameters(3, "success", 100, COPY_ENCRYPTION_PARAM, TestRandomUtil.nextBytes(15)),
@@ -568,7 +531,7 @@ public class BackupManagerTest {
when(remoteStorageManager.copy(eq(3), eq("badlength"), eq(300), any(), any()))
.thenReturn(CompletableFuture.failedFuture(new InvalidLengthException("")));
final List<CopyResult> results = backupManager.copyToBackup(backupUser, toCopy)
final List<CopyResult> results = backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, toCopy))
.collectList().block();
assertThat(results).hasSize(3);
@@ -587,15 +550,11 @@ public class BackupManagerTest {
public void copyWrongCredentialType() {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> copy(backupUser))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.UNAUTHENTICATED);
assertThatExceptionOfType(BackupWrongCredentialTypeException.class).isThrownBy(() -> copy(backupUser));
}
@Test
public void quotaEnforcementNoRecalculation() {
public void quotaEnforcementNoRecalculation() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
verifyNoInteractions(remoteStorageManager);
@@ -612,7 +571,7 @@ public class BackupManagerTest {
}
@Test
public void quotaEnforcementRecalculation() {
public void quotaEnforcementRecalculation() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
@@ -642,7 +601,7 @@ public class BackupManagerTest {
public void quotaEnforcement(
@CartesianTest.Values(booleans = {true, false}) boolean hasSpaceBeforeRecalc,
@CartesianTest.Values(booleans = {true, false}) boolean hasSpaceAfterRecalc,
@CartesianTest.Values(booleans = {true, false}) boolean doesReaclc) {
@CartesianTest.Values(booleans = {true, false}) boolean doesReaclc) throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
@@ -701,7 +660,7 @@ public class BackupManagerTest {
@ParameterizedTest
@ValueSource(strings = {"", "cursor"})
public void list(final String cursorVal) {
public void list(final String cursorVal) throws BackupException {
final Optional<String> cursor = Optional.of(cursorVal).filter(StringUtils::isNotBlank);
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
@@ -724,7 +683,7 @@ public class BackupManagerTest {
}
@Test
public void deleteEntireBackup() {
public void deleteEntireBackup() throws BackupException {
final AuthenticatedBackupUser original = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
testClock.pin(Instant.ofEpochSecond(10));
@@ -762,7 +721,7 @@ public class BackupManagerTest {
}
@Test
public void delete() {
public void delete() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final byte[] mediaId = TestRandomUtil.nextBytes(16);
final String backupMediaKey = "%s/%s/%s".formatted(
@@ -786,10 +745,8 @@ public class BackupManagerTest {
public void deleteWrongCredentialType() {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
final byte[] mediaId = TestRandomUtil.nextBytes(16);
assertThatThrownBy(() ->
backupManager.deleteMedia(backupUser, List.of(new BackupManager.StorageDescriptor(5, mediaId))).then().block())
.isInstanceOf(StatusRuntimeException.class)
.matches(e -> ((StatusRuntimeException) e).getStatus().getCode() == Status.UNAUTHENTICATED.getCode());
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
.isThrownBy(() -> backupManager.deleteMedia(backupUser, List.of(new BackupManager.StorageDescriptor(5, mediaId))).then().block());
}
@Test
@@ -797,14 +754,12 @@ public class BackupManagerTest {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final BackupManager.StorageDescriptor sd = new BackupManager.StorageDescriptor(4, TestRandomUtil.nextBytes(15));
when(remoteStorageManager.cdnNumber()).thenReturn(5);
assertThatThrownBy(() ->
backupManager.deleteMedia(backupUser, List.of(sd)).then().block())
.isInstanceOf(StatusRuntimeException.class)
.matches(e -> ((StatusRuntimeException) e).getStatus().getCode() == Status.INVALID_ARGUMENT.getCode());
assertThatThrownBy(() -> backupManager.deleteMedia(backupUser, List.of(sd)).then().toFuture().join())
.hasCauseInstanceOf(BackupInvalidArgumentException.class);
}
@Test
public void deleteUsageCheckpoints() throws InterruptedException {
public void deleteUsageCheckpoints() throws InterruptedException, BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA,
BackupLevel.PAID);
@@ -849,7 +804,7 @@ public class BackupManagerTest {
}
@Test
public void deletePartialFailure() {
public void deletePartialFailure() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final List<BackupManager.StorageDescriptor> descriptors = new ArrayList<>();
@@ -887,7 +842,7 @@ public class BackupManagerTest {
}
@Test
public void alreadyDeleted() {
public void alreadyDeleted() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
final byte[] mediaId = TestRandomUtil.nextBytes(16);
final String backupMediaKey = "%s/%s/%s".formatted(
@@ -907,7 +862,7 @@ public class BackupManagerTest {
}
@Test
public void listExpiredBackups() {
public void listExpiredBackups() throws BackupException {
final List<AuthenticatedBackupUser> backupUsers = IntStream.range(0, 10)
.mapToObj(_ -> backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID))
.toList();
@@ -943,7 +898,7 @@ public class BackupManagerTest {
}
@Test
public void listExpiredBackupsByTier() {
public void listExpiredBackupsByTier() throws BackupException {
final byte[] backupId = TestRandomUtil.nextBytes(16);
// refreshed media timestamp at t=5
@@ -971,7 +926,7 @@ public class BackupManagerTest {
@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.INCLUDE, names = {"MEDIA", "ALL"})
public void expireBackup(ExpiredBackup.ExpirationType expirationType) {
public void expireBackup(ExpiredBackup.ExpirationType expirationType) throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
backupManager.createMessageBackupUploadDescriptor(backupUser);
@@ -1005,11 +960,9 @@ public class BackupManagerTest {
if (expirationType == ExpiredBackup.ExpirationType.ALL) {
// should have deleted the db row for the backup
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
StatusRuntimeException.class,
backupsDb.describeBackup(backupUser))
.getStatus().getCode())
.isEqualTo(Status.NOT_FOUND.getCode());
CompletableFutureTestUtil.assertFailsWithCause(
BackupNotFoundException.class,
backupsDb.describeBackup(backupUser));
} else {
// should have deleted all the media, but left the backup descriptor in place
assertThatNoException().isThrownBy(() -> backupsDb.describeBackup(backupUser).join());
@@ -1017,7 +970,7 @@ public class BackupManagerTest {
}
@Test
public void deleteBackupPaginated() {
public void deleteBackupPaginated() throws BackupException {
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
backupManager.createMessageBackupUploadDescriptor(backupUser);
@@ -1055,7 +1008,7 @@ public class BackupManagerTest {
@ParameterizedTest
@EnumSource(BackupLevel.class)
void svrbAuthValid(BackupLevel backupLevel) {
void svrbAuthValid(BackupLevel backupLevel) throws BackupException {
testClock.pin(Instant.ofEpochSecond(123));
final AuthenticatedBackupUser backupUser =
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
@@ -1072,29 +1025,26 @@ public class BackupManagerTest {
// Can't use MEDIA for svrb auth
final AuthenticatedBackupUser backupUser =
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, backupLevel);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.generateSvrbAuth(backupUser))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.UNAUTHENTICATED);
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
.isThrownBy(() -> backupManager.generateSvrbAuth(backupUser));
}
private CopyResult copyError(final AuthenticatedBackupUser backupUser, Throwable copyException) {
private CopyResult copyError(final AuthenticatedBackupUser backupUser, Throwable copyException) throws BackupException {
when(tusCredentialGenerator.generateUpload(any()))
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
when(remoteStorageManager.copy(eq(3), eq(COPY_PARAM.sourceKey()), eq(COPY_PARAM.sourceLength()), any(), any()))
.thenReturn(CompletableFuture.failedFuture(copyException));
return backupManager.copyToBackup(backupUser, List.of(COPY_PARAM)).single().block();
return backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, List.of(COPY_PARAM))).single().block();
}
private CopyResult copy(final AuthenticatedBackupUser backupUser) {
private CopyResult copy(final AuthenticatedBackupUser backupUser) throws BackupException {
when(tusCredentialGenerator.generateUpload(any()))
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
when(tusCredentialGenerator.generateUpload(any()))
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
when(remoteStorageManager.copy(eq(3), eq(COPY_PARAM.sourceKey()), eq(COPY_PARAM.sourceLength()), any(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
return backupManager.copyToBackup(backupUser, List.of(COPY_PARAM)).single().block();
return backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, List.of(COPY_PARAM))).single().block();
}
private static ExpiredBackup expiredBackup(final ExpiredBackup.ExpirationType expirationType,

View File

@@ -8,8 +8,6 @@ package org.whispersystems.textsecuregcm.backup;
import static org.assertj.core.api.Assertions.assertThat;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -212,10 +210,9 @@ public class BackupsDbTest {
backupsDb.finishExpiration(opt.get()).join();
// The backup entry should be gone
assertThat(CompletableFutureTestUtil.assertFailsWithCause(StatusRuntimeException.class,
backupsDb.describeBackup(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)))
.getStatus().getCode())
.isEqualTo(Status.Code.NOT_FOUND);
CompletableFutureTestUtil.assertFailsWithCause(
BackupNotFoundException.class,
backupsDb.describeBackup(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)));
assertThat(expiredBackups.apply(Instant.ofEpochSecond(10))).isEmpty();
}
}

View File

@@ -17,7 +17,6 @@ import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import io.grpc.Status;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
@@ -34,7 +33,6 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.glassfish.jersey.server.ServerProperties;
@@ -68,10 +66,16 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
import org.whispersystems.textsecuregcm.backup.BackupException;
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
import org.whispersystems.textsecuregcm.backup.BackupManager;
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
import org.whispersystems.textsecuregcm.backup.CopyResult;
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
import org.whispersystems.textsecuregcm.mappers.BackupExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
@@ -97,6 +101,7 @@ public class ArchiveControllerTest {
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
.addProvider(new CompletionExceptionMapper())
.addResource(new GrpcStatusRuntimeExceptionMapper())
.addResource(new BackupExceptionMapper())
.addProvider(new RateLimitExceededExceptionMapper())
.setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
@@ -165,7 +170,7 @@ public class ArchiveControllerTest {
}
@Test
public void setBackupId() throws RateLimitExceededException {
public void setBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
final Response response = resources.getJerseyTest()
.target("v1/archives/backupid")
.request()
@@ -183,7 +188,8 @@ public class ArchiveControllerTest {
}
@Test
public void setBackupIdPartial() throws RateLimitExceededException {
public void setBackupIdPartial()
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
final Response response = resources.getJerseyTest()
.target("v1/archives/backupid")
.request()
@@ -302,13 +308,15 @@ public class ArchiveControllerTest {
public static Stream<Arguments> setBackupIdException() {
return Stream.of(
Arguments.of(new RateLimitExceededException(null), 429),
Arguments.of(Status.INVALID_ARGUMENT.withDescription("test").asRuntimeException(), 400)
Arguments.of(new BackupInvalidArgumentException("test"), 400),
Arguments.of(new BackupPermissionException("test"), 403)
);
}
@ParameterizedTest
@MethodSource
public void setBackupIdException(final Exception ex, final int expectedStatus) throws RateLimitExceededException {
public void setBackupIdException(final Exception ex, final int expectedStatus)
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
doThrow(ex).when(backupAuthManager).commitBackupId(any(), any(), any(), any());
final Response response = resources.getJerseyTest()
.target("v1/archives/backupid")
@@ -322,7 +330,7 @@ public class ArchiveControllerTest {
}
@Test
public void getCredentials() {
public void getCredentials() throws BackupNotFoundException {
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
final Instant end = start.plus(Duration.ofDays(1));
final RedemptionRange expectedRange = RedemptionRange.inclusive(Clock.systemUTC(), start, end);
@@ -331,9 +339,12 @@ public class ArchiveControllerTest {
EnumMapUtil.toEnumMap(BackupCredentialType.class, credentialType -> backupAuthTestUtil.getCredentials(
BackupLevel.PAID, backupAuthTestUtil.getRequest(messagesBackupKey, aci), credentialType, start, end));
expectedCredentialsByType.forEach((credentialType, expectedCredentials) ->
for (Map.Entry<BackupCredentialType, List<BackupAuthManager.Credential>> entry : expectedCredentialsByType.entrySet()) {
final BackupCredentialType credentialType = entry.getKey();
final List<BackupAuthManager.Credential> expectedCredentials = entry.getValue();
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
.thenReturn(expectedCredentials));
.thenReturn(expectedCredentials);
}
final ArchiveController.BackupAuthCredentialsResponse credentialResponse = resources.getJerseyTest()
.target("v1/archives/auth")
@@ -385,7 +396,7 @@ public class ArchiveControllerTest {
}
@Test
public void getBackupInfo() throws VerificationFailedException {
public void getBackupInfo() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -405,13 +416,13 @@ public class ArchiveControllerTest {
}
@Test
public void putMediaBatchSuccess() throws VerificationFailedException {
public void putMediaBatchSuccess() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
when(backupManager.copyToBackup(any(), any()))
when(backupManager.copyToBackup(any()))
.thenReturn(Flux.just(
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[1], 1)));
@@ -449,7 +460,7 @@ public class ArchiveControllerTest {
}
@Test
public void putMediaBatchPartialFailure() throws VerificationFailedException {
public void putMediaBatchPartialFailure() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
@@ -457,7 +468,7 @@ public class ArchiveControllerTest {
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
final byte[][] mediaIds = IntStream.range(0, 4).mapToObj(i -> TestRandomUtil.nextBytes(15)).toArray(byte[][]::new);
when(backupManager.copyToBackup(any(), any()))
when(backupManager.copyToBackup(any()))
.thenReturn(Flux.just(
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
new CopyResult(CopyResult.Outcome.SOURCE_NOT_FOUND, mediaIds[1], null),
@@ -508,7 +519,7 @@ public class ArchiveControllerTest {
@Test
public void copyMediaWithNegativeLength() throws VerificationFailedException {
public void copyMediaWithNegativeLength() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -541,7 +552,7 @@ public class ArchiveControllerTest {
public void list(
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
throws VerificationFailedException {
throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -576,7 +587,7 @@ public class ArchiveControllerTest {
}
@Test
public void delete() throws VerificationFailedException {
public void delete() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID,
messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -612,7 +623,7 @@ public class ArchiveControllerTest {
@ParameterizedTest
@MethodSource
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) throws VerificationFailedException {
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -641,7 +652,7 @@ public class ArchiveControllerTest {
}
@Test
public void mediaUploadForm() throws VerificationFailedException, RateLimitExceededException {
public void mediaUploadForm() throws VerificationFailedException, BackupException, RateLimitExceededException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -671,7 +682,7 @@ public class ArchiveControllerTest {
}
@Test
public void readAuth() throws VerificationFailedException {
public void readAuth() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -689,7 +700,7 @@ public class ArchiveControllerTest {
@Test
public void svrbAuth() throws VerificationFailedException {
public void svrbAuth() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -706,7 +717,7 @@ public class ArchiveControllerTest {
}
@Test
public void readAuthInvalidParam() throws VerificationFailedException {
public void readAuthInvalidParam() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
Response response = resources.getJerseyTest()
@@ -728,7 +739,7 @@ public class ArchiveControllerTest {
}
@Test
public void deleteEntireBackup() throws VerificationFailedException {
public void deleteEntireBackup() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
@@ -743,7 +754,7 @@ public class ArchiveControllerTest {
}
@Test
public void invalidSourceAttachmentKey() throws VerificationFailedException {
public void invalidSourceAttachmentKey() throws VerificationFailedException, BackupException {
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))

View File

@@ -8,7 +8,6 @@ package org.whispersystems.textsecuregcm.grpc;
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.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@@ -19,19 +18,16 @@ import io.grpc.StatusRuntimeException;
import java.time.Clock;
import java.time.Duration;
import java.util.Arrays;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -62,10 +58,11 @@ import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
import org.signal.libsignal.zkgroup.backups.BackupLevel;
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
import org.whispersystems.textsecuregcm.backup.BackupException;
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
import org.whispersystems.textsecuregcm.backup.BackupManager;
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
import org.whispersystems.textsecuregcm.backup.CopyResult;
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
@@ -90,9 +87,12 @@ class BackupsAnonymousGrpcServiceTest extends
@BeforeEach
void setup() {
when(backupManager.authenticateBackupUserAsync(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(
backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
try {
when(backupManager.authenticateBackupUser(any(), any(), any()))
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
} catch (BackupFailedZkAuthenticationException e) {
Assertions.fail(e);
}
}
@Test
@@ -129,7 +129,7 @@ class BackupsAnonymousGrpcServiceTest extends
@Test
void putMediaBatchSuccess() {
final byte[][] mediaIds = {TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
when(backupManager.copyToBackup(any(), any()))
when(backupManager.copyToBackup(any()))
.thenReturn(Flux.just(
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[1], 1)));
@@ -174,7 +174,7 @@ class BackupsAnonymousGrpcServiceTest extends
CopyResult.Outcome.SOURCE_WRONG_LENGTH,
CopyResult.Outcome.OUT_OF_QUOTA
};
when(backupManager.copyToBackup(any(), any()))
when(backupManager.copyToBackup(any()))
.thenReturn(Flux.fromStream(IntStream.range(0, 4)
.mapToObj(i -> new CopyResult(
outcomes[i],
@@ -211,17 +211,17 @@ class BackupsAnonymousGrpcServiceTest extends
}
@Test
void getBackupInfo() {
void getBackupInfo() throws BackupException {
when(backupManager.backupInfo(any()))
.thenReturn(new BackupManager.BackupInfo(1, "myBackupDir", "myMediaDir", "filename", Optional.empty()));
final GetBackupInfoResponse response = unauthenticatedServiceStub().getBackupInfo(GetBackupInfoRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.build());
assertThat(response.getBackupDir()).isEqualTo("myBackupDir");
assertThat(response.getBackupName()).isEqualTo("filename");
assertThat(response.getCdn()).isEqualTo(1);
assertThat(response.getUsedSpace()).isEqualTo(0L);
assertThat(response.getBackupInfo().getBackupDir()).isEqualTo("myBackupDir");
assertThat(response.getBackupInfo().getBackupName()).isEqualTo("filename");
assertThat(response.getBackupInfo().getCdn()).isEqualTo(1);
assertThat(response.getBackupInfo().getUsedSpace()).isEqualTo(0L);
}
@@ -229,7 +229,7 @@ class BackupsAnonymousGrpcServiceTest extends
void list(
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
throws VerificationFailedException {
throws VerificationFailedException, BackupException {
final byte[] mediaId = TestRandomUtil.nextBytes(15);
final Optional<String> expectedCursor = cursorProvided ? Optional.of("myCursor") : Optional.empty();
@@ -250,15 +250,16 @@ class BackupsAnonymousGrpcServiceTest extends
}
final ListMediaResponse response = unauthenticatedServiceStub().listMedia(request.build());
assertThat(response.getPageCount()).isEqualTo(1);
assertThat(response.getPage(0).getLength()).isEqualTo(100);
assertThat(response.getPage(0).getMediaId().toByteArray()).isEqualTo(mediaId);
assertThat(response.hasCursor() ? response.getCursor() : null).isEqualTo(returnedCursor.orElse(null));
assertThat(response.getListResult().getPageCount()).isEqualTo(1);
assertThat(response.getListResult().getPage(0).getLength()).isEqualTo(100);
assertThat(response.getListResult().getPage(0).getMediaId().toByteArray()).isEqualTo(mediaId);
assertThat(response.getListResult().hasCursor() ? response.getListResult().getCursor() : null)
.isEqualTo(returnedCursor.orElse(null));
}
@Test
void delete() {
void delete() throws BackupException {
final DeleteMediaRequest request = DeleteMediaRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.addAllItems(IntStream.range(0, 100).mapToObj(i ->
@@ -278,7 +279,7 @@ class BackupsAnonymousGrpcServiceTest extends
}
@Test
void mediaUploadForm() throws RateLimitExceededException {
void mediaUploadForm() throws RateLimitExceededException, BackupException {
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
.thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org"));
final GetUploadFormRequest request = GetUploadFormRequest.newBuilder()
@@ -287,10 +288,10 @@ class BackupsAnonymousGrpcServiceTest extends
.build();
final GetUploadFormResponse uploadForm = unauthenticatedServiceStub().getUploadForm(request);
assertThat(uploadForm.getCdn()).isEqualTo(3);
assertThat(uploadForm.getKey()).isEqualTo("abc");
assertThat(uploadForm.getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
assertThat(uploadForm.getSignedUploadLocation()).isEqualTo("example.org");
assertThat(uploadForm.getUploadForm().getCdn()).isEqualTo(3);
assertThat(uploadForm.getUploadForm().getKey()).isEqualTo("abc");
assertThat(uploadForm.getUploadForm().getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
assertThat(uploadForm.getUploadForm().getSignedUploadLocation()).isEqualTo("example.org");
// rate limit
Duration duration = Duration.ofSeconds(10);
@@ -309,7 +310,7 @@ class BackupsAnonymousGrpcServiceTest extends
@ParameterizedTest
@MethodSource
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) {
public void messagesUploadForm(Optional<Long> uploadLength, boolean allowedSize) throws BackupException {
when(backupManager.createMessageBackupUploadDescriptor(any()))
.thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org"));
final GetUploadFormRequest.MessagesUploadType.Builder builder = GetUploadFormRequest.MessagesUploadType.newBuilder();
@@ -318,24 +319,20 @@ class BackupsAnonymousGrpcServiceTest extends
.setMessages(builder.build())
.setSignedPresentation(signedPresentation(presentation))
.build();
if (expectSuccess) {
final GetUploadFormResponse uploadForm = unauthenticatedServiceStub().getUploadForm(request);
assertThat(uploadForm.getCdn()).isEqualTo(3);
assertThat(uploadForm.getKey()).isEqualTo("abc");
assertThat(uploadForm.getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
assertThat(uploadForm.getSignedUploadLocation()).isEqualTo("example.org");
final GetUploadFormResponse response = unauthenticatedServiceStub().getUploadForm(request);
if (allowedSize) {
assertThat(response.getUploadForm().getCdn()).isEqualTo(3);
assertThat(response.getUploadForm().getKey()).isEqualTo("abc");
assertThat(response.getUploadForm().getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
assertThat(response.getUploadForm().getSignedUploadLocation()).isEqualTo("example.org");
} else {
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> unauthenticatedServiceStub().getUploadForm(request))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.FAILED_PRECONDITION.getCode());
assertThat(response.hasExceedsMaxUploadLength()).isTrue();
}
}
@Test
void readAuth() {
void readAuth() throws BackupException {
when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value"));
final GetCdnCredentialsResponse response = unauthenticatedServiceStub().getCdnCredentials(
@@ -343,7 +340,7 @@ class BackupsAnonymousGrpcServiceTest extends
.setCdn(3)
.setSignedPresentation(signedPresentation(presentation))
.build());
assertThat(response.getHeadersMap()).containsExactlyEntriesOf(Map.of("key", "value"));
assertThat(response.getCdnCredentials().getHeadersMap()).containsExactlyEntriesOf(Map.of("key", "value"));
}
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType,

View File

@@ -36,6 +36,7 @@ import org.signal.chat.backup.BackupsGrpc;
import org.signal.chat.backup.GetBackupAuthCredentialsRequest;
import org.signal.chat.backup.GetBackupAuthCredentialsResponse;
import org.signal.chat.backup.RedeemReceiptRequest;
import org.signal.chat.backup.RedeemReceiptResponse;
import org.signal.chat.backup.SetBackupIdRequest;
import org.signal.chat.common.ZkCredential;
import org.signal.libsignal.zkgroup.InvalidInputException;
@@ -54,6 +55,12 @@ import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
import org.whispersystems.textsecuregcm.backup.BackupBadReceiptException;
import org.whispersystems.textsecuregcm.backup.BackupException;
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
import org.whispersystems.textsecuregcm.backup.BackupMissingIdCommitmentException;
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -61,6 +68,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.EnumMapUtil;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import javax.annotation.Nullable;
class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, BackupsGrpc.BackupsBlockingStub> {
@@ -98,7 +106,7 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
@Test
void setBackupId() throws RateLimitExceededException {
void setBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
authenticatedServiceStub().setBackupId(
SetBackupIdRequest.newBuilder()
.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()))
@@ -111,7 +119,8 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
@ParameterizedTest
@ValueSource(booleans = {false, true})
void setBackupIdPartial(boolean media) throws RateLimitExceededException {
void setBackupIdPartial(boolean media)
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
final SetBackupIdRequest.Builder builder = SetBackupIdRequest.newBuilder();
if (media) {
builder.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()));
@@ -141,15 +150,14 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
public static Stream<Arguments> setBackupIdException() {
return Stream.of(
Arguments.of(new RateLimitExceededException(null), Status.RESOURCE_EXHAUSTED),
Arguments.of(Status.INVALID_ARGUMENT.withDescription("test").asRuntimeException(),
Status.INVALID_ARGUMENT)
);
Arguments.of(new BackupPermissionException("test"), Status.INVALID_ARGUMENT),
Arguments.of(new BackupInvalidArgumentException("test"), Status.INVALID_ARGUMENT));
}
@ParameterizedTest
@MethodSource
void setBackupIdException(final Exception ex, final Status expected)
throws RateLimitExceededException {
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
doThrow(ex).when(backupAuthManager).commitBackupId(any(), any(), any(), any());
GrpcTestUtils.assertStatusException(
@@ -160,8 +168,18 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
);
}
@Test
void redeemReceipt() throws InvalidInputException, VerificationFailedException {
public static Stream<Arguments> redeemReceipt() {
return Stream.of(
Arguments.of(null, RedeemReceiptResponse.OutcomeCase.SUCCESS),
Arguments.of(new BackupBadReceiptException("test"), RedeemReceiptResponse.OutcomeCase.INVALID_RECEIPT),
Arguments.of(new BackupMissingIdCommitmentException(), RedeemReceiptResponse.OutcomeCase.ACCOUNT_MISSING_COMMITMENT));
}
@ParameterizedTest
@MethodSource
void redeemReceipt(@Nullable final BackupException exception, final RedeemReceiptResponse.OutcomeCase expectedOutcome)
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
final ServerSecretParams params = ServerSecretParams.generate();
final ServerZkReceiptOperations serverOps = new ServerZkReceiptOperations(params);
final ClientZkReceiptOperations clientOps = new ClientZkReceiptOperations(params.getPublicParams());
@@ -171,16 +189,22 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
final ReceiptCredential receiptCredential = clientOps.receiveReceiptCredential(rcrc, rcr);
final ReceiptCredentialPresentation presentation = clientOps.createReceiptCredentialPresentation(receiptCredential);
authenticatedServiceStub().redeemReceipt(RedeemReceiptRequest.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize()))
.build());
if (exception != null) {
doThrow(exception).when(backupAuthManager).redeemReceipt(any(), any());
}
final RedeemReceiptResponse redeemReceiptResponse = authenticatedServiceStub().redeemReceipt(
RedeemReceiptRequest.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize()))
.build());
assertThat(redeemReceiptResponse.getOutcomeCase()).isEqualTo(expectedOutcome);
verify(backupAuthManager).redeemReceipt(account, presentation);
}
@Test
void getCredentials() {
void getCredentials() throws BackupNotFoundException {
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
final Instant end = start.plus(Duration.ofDays(1));
final RedemptionRange expectedRange = RedemptionRange.inclusive(Clock.systemUTC(), start, end);
@@ -190,9 +214,12 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
BackupLevel.PAID, backupAuthTestUtil.getRequest(messagesBackupKey, AUTHENTICATED_ACI), credentialType,
start, end));
expectedCredentialsByType.forEach((credentialType, expectedCredentials) ->
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
.thenReturn(expectedCredentials));
for (Map.Entry<BackupCredentialType, List<BackupAuthManager.Credential>> entry : expectedCredentialsByType.entrySet()) {
final BackupCredentialType credentialType = entry.getKey();
final List<BackupAuthManager.Credential> expectedCredentials = entry.getValue();
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
.thenReturn(expectedCredentials);
}
final GetBackupAuthCredentialsResponse credentialResponse = authenticatedServiceStub().getBackupAuthCredentials(
GetBackupAuthCredentialsRequest.newBuilder()
@@ -202,8 +229,8 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
expectedCredentialsByType.forEach((credentialType, expectedCredentials) -> {
final Map<Long, ZkCredential> creds = switch (credentialType) {
case MESSAGES -> credentialResponse.getMessageCredentialsMap();
case MEDIA -> credentialResponse.getMediaCredentialsMap();
case MESSAGES -> credentialResponse.getCredentials().getMessageCredentialsMap();
case MEDIA -> credentialResponse.getCredentials().getMediaCredentialsMap();
};
assertThat(creds).hasSize(expectedCredentials.size()).containsKey(start.getEpochSecond());