diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java index cd8b6d5ea..e641a7560 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.Nullable; import org.signal.libsignal.zkgroup.GenericServerSecretParams; @@ -101,40 +100,59 @@ public class BackupAuthManager { public CompletableFuture commitBackupId( final Account account, final Device device, - final BackupAuthCredentialRequest messagesBackupCredentialRequest, - final BackupAuthCredentialRequest mediaBackupCredentialRequest) { + final Optional messagesBackupCredentialRequest, + final Optional mediaBackupCredentialRequest) { if (!device.isPrimary()) { throw Status.PERMISSION_DENIED.withDescription("Only primary device can set backup-id").asRuntimeException(); } - final byte[] serializedMessageCredentialRequest = messagesBackupCredentialRequest.serialize(); - final byte[] serializedMediaCredentialRequest = mediaBackupCredentialRequest.serialize(); - final boolean messageCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES) - .map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMessageCredentialRequest)) - .orElse(false); + if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) { + throw Status.INVALID_ARGUMENT + .withDescription("Must set at least one of message/media credential requests") + .asRuntimeException(); + } - final boolean mediaCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MEDIA) - .map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMediaCredentialRequest)) - .orElse(false); + final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES) + .orElse(null); + final byte[] storedMediaCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MEDIA) + .orElse(null); - if (messageCredentialRequestMatches && mediaCredentialRequestMatches) { + // If the provided credential request is null, we want to set to the existing request + final byte[] targetMessageCredentialRequest = messagesBackupCredentialRequest + .map(BackupAuthCredentialRequest::serialize) + .orElse(storedMessageCredentialRequest); + final byte[] targetMediaCredentialRequest = mediaBackupCredentialRequest + .map(BackupAuthCredentialRequest::serialize) + .orElse(storedMediaCredentialRequest); + + final boolean requiresMessageRotation = + !MessageDigest.isEqual(targetMessageCredentialRequest, storedMessageCredentialRequest); + final boolean requiresMediaRotation = + !MessageDigest.isEqual(targetMediaCredentialRequest, storedMediaCredentialRequest); + + if (!requiresMessageRotation && !requiresMediaRotation) { // No need to update or enforce rate limits, this is the credential that the user has already // committed to. return CompletableFuture.completedFuture(null); } - CompletionStage rateLimitFuture = rateLimiters - .forDescriptor(RateLimiters.For.SET_BACKUP_ID) - .validateAsync(account.getUuid()); + CompletableFuture rateLimitFuture = CompletableFuture.completedFuture(null); - if (!mediaCredentialRequestMatches && hasActiveVoucher(account)) { + if (requiresMessageRotation) { + rateLimitFuture = rateLimitFuture.thenCombine( + rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID).validateAsync(account.getUuid()), + (_, _) -> null); + } + + if (requiresMediaRotation && hasActiveVoucher(account)) { rateLimitFuture = rateLimitFuture.thenCombine( rateLimiters.forDescriptor(RateLimiters.For.SET_PAID_MEDIA_BACKUP_ID).validateAsync(account.getUuid()), - (ignore1, ignore2) -> null); + (_, _) -> null); } return rateLimitFuture.thenCompose(ignored -> this.accountsManager - .updateAsync(account, a -> a.setBackupCredentialRequests(serializedMessageCredentialRequest, serializedMediaCredentialRequest)) + .updateAsync(account, a -> + a.setBackupCredentialRequests(targetMessageCredentialRequest, targetMediaCredentialRequest)) .thenRun(Util.NOOP)) .toCompletableFuture(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java index a2db99376..bd34ee9dd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java @@ -114,19 +114,20 @@ public class ArchiveController { @Schema(description = """ A BackupAuthCredentialRequest containing a blinded encrypted backup-id, encoded in standard padded base64. This backup-id should be used for message backups only, and must have the message backup type set on the - credential. + credential. If absent, the message credential request will not be updated. """, implementation = String.class) @JsonDeserialize(using = BackupAuthCredentialAdapter.CredentialRequestDeserializer.class) @JsonSerialize(using = BackupAuthCredentialAdapter.CredentialRequestSerializer.class) - @NotNull BackupAuthCredentialRequest messagesBackupAuthCredentialRequest, + BackupAuthCredentialRequest messagesBackupAuthCredentialRequest, @Schema(description = """ A BackupAuthCredentialRequest containing a blinded encrypted backup-id, encoded in standard padded base64. - This backup-id should be used for media only, and must have the media type set on the credential. + This backup-id should be used for media only, and must have the media type set on the credential. If absent, + only the media credential request will not be updated. """, implementation = String.class) @JsonDeserialize(using = BackupAuthCredentialAdapter.CredentialRequestDeserializer.class) @JsonSerialize(using = BackupAuthCredentialAdapter.CredentialRequestSerializer.class) - @NotNull BackupAuthCredentialRequest mediaBackupAuthCredentialRequest) {} + BackupAuthCredentialRequest mediaBackupAuthCredentialRequest) {} @PUT @@ -136,11 +137,13 @@ public class ArchiveController { @Operation( summary = "Set backup id", description = """ - Set a (blinded) backup-id for the account. Each account may have a single active backup-id that can be used - to store and retrieve backups. Once the backup-id is set, BackupAuthCredentials can be generated - using /v1/archives/auth. + Set (blinded) backup-id(s) for the account. Each account may have a single active backup-id for each + credential type that can be used to store and retrieve backups. Once the backup-id is set, + BackupAuthCredentials can be generated using /v1/archives/auth. The blinded backup-id and the key-pair used to blind it should be derived from a recoverable secret. + + At least one of `messagesBackupAuthCredentialRequest`, `mediaBackupAuthCredentialRequest` must be set. """) @ApiResponse(responseCode = "204", description = "The backup-id was set") @ApiResponse(responseCode = "400", description = "The provided backup auth credential request was invalid") @@ -159,8 +162,9 @@ public class ArchiveController { .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); return backupAuthManager - .commitBackupId(account, device, setBackupIdRequest.messagesBackupAuthCredentialRequest, - setBackupIdRequest.mediaBackupAuthCredentialRequest) + .commitBackupId(account, device, + Optional.ofNullable(setBackupIdRequest.messagesBackupAuthCredentialRequest), + Optional.ofNullable(setBackupIdRequest.mediaBackupAuthCredentialRequest)) .thenApply(Util.ASYNC_EMPTY_RESPONSE); }); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java index d7446008f..4d7ba2dea 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcService.java @@ -10,6 +10,7 @@ import io.micrometer.core.instrument.Tag; import java.time.Clock; import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.signal.chat.backup.GetBackupAuthCredentialsRequest; import org.signal.chat.backup.GetBackupAuthCredentialsResponse; @@ -46,17 +47,16 @@ public class BackupsGrpcService extends ReactorBackupsGrpc.BackupsImplBase { this.backupMetrics = backupMetrics; } - @Override public Mono setBackupId(SetBackupIdRequest request) { - final BackupAuthCredentialRequest messagesCredentialRequest = deserialize( + final Optional messagesCredentialRequest = deserializeWithEmptyPresenceCheck( BackupAuthCredentialRequest::new, - request.getMessagesBackupAuthCredentialRequest().toByteArray()); + request.getMessagesBackupAuthCredentialRequest()); - final BackupAuthCredentialRequest mediaCredentialRequest = deserialize( + final Optional mediaCredentialRequest = deserializeWithEmptyPresenceCheck( BackupAuthCredentialRequest::new, - request.getMediaBackupAuthCredentialRequest().toByteArray()); + request.getMediaBackupAuthCredentialRequest()); final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); return authenticatedAccount() @@ -137,6 +137,13 @@ public class BackupsGrpcService extends ReactorBackupsGrpc.BackupsImplBase { T deserialize(byte[] bytes) throws InvalidInputException; } + private Optional deserializeWithEmptyPresenceCheck(Deserializer deserializer, ByteString byteString) { + if (byteString.isEmpty()) { + return Optional.empty(); + } + return Optional.of(deserialize(deserializer, byteString.toByteArray())); + } + private T deserialize(Deserializer deserializer, byte[] bytes) { try { return deserializer.deserialize(bytes); diff --git a/service/src/main/proto/org/signal/chat/backups.proto b/service/src/main/proto/org/signal/chat/backups.proto index 9320bf305..c3027401b 100644 --- a/service/src/main/proto/org/signal/chat/backups.proto +++ b/service/src/main/proto/org/signal/chat/backups.proto @@ -34,15 +34,17 @@ service Backups { option (require.auth) = AUTH_ONLY_AUTHENTICATED; /** - * Set a (blinded) backup-id for the account. + * Set (blinded) backup-id(s) for the account. * - * Each account may have a single active backup-id that can be used - * to store and retrieve backups. Once the backup-id is set, + * Each account may have a single active backup-id for each credential type + * that can be used to store and retrieve backups. Once the backup-id is set, * BackupAuthCredentials can be generated using GetBackupAuthCredentials. * * The blinded backup-id and the key-pair used to blind it must be derived * from a recoverable secret. * + * At least one of the credential types must be set on the request. + * * errors: * PERMISSION_DENIED: This account is not currently eligible for backups */ @@ -89,14 +91,15 @@ message SetBackupIdRequest { * A BackupAuthCredentialRequest containing a blinded encrypted backup-id, * encoded in standard padded base64. This backup-id should be used for * message backups only, and must have the message backup type set on the - * credential. + * credential. If absent, the message credential request will not be updated. */ bytes messages_backup_auth_credential_request = 1; /** * A BackupAuthCredentialRequest containing a blinded encrypted backup-id, * encoded in standard padded base64. This backup-id should be used for - * media only, and must have the media type set on the credential. + * media only, and must have the media type set on the credential. If absent, + * the media credential request will not be updated. */ bytes media_backup_auth_credential_request = 2; } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java index 3d79bee40..ba621ad0d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java @@ -6,7 +6,9 @@ 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.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -27,6 +29,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.assertj.core.api.Assertions; @@ -124,7 +127,9 @@ public class BackupAuthManagerTest { final BackupAuthCredentialRequest messagesCredentialRequest = backupAuthTestUtil.getRequest(messagesBackupKey, aci); final BackupAuthCredentialRequest mediaCredentialRequest = backupAuthTestUtil.getRequest(mediaBackupKey, aci); - authManager.commitBackupId(account, primaryDevice(), messagesCredentialRequest, mediaCredentialRequest).join(); + authManager.commitBackupId(account, primaryDevice(), + Optional.of(messagesCredentialRequest), + Optional.of(mediaCredentialRequest)).join(); verify(account).setBackupCredentialRequests(messagesCredentialRequest.serialize(), mediaCredentialRequest.serialize()); @@ -140,8 +145,8 @@ public class BackupAuthManagerTest { final ThrowableAssert.ThrowingCallable commit = () -> authManager.commitBackupId(account, primaryDevice(), - backupAuthTestUtil.getRequest(messagesBackupKey, aci), - backupAuthTestUtil.getRequest(mediaBackupKey, aci)).join(); + Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)), + Optional.of(backupAuthTestUtil.getRequest(mediaBackupKey, aci))).join(); Assertions.assertThatNoException().isThrownBy(commit); } @@ -154,8 +159,8 @@ public class BackupAuthManagerTest { final ThrowableAssert.ThrowingCallable commit = () -> authManager.commitBackupId(account, linkedDevice(), - backupAuthTestUtil.getRequest(messagesBackupKey, aci), - backupAuthTestUtil.getRequest(mediaBackupKey, aci)).join(); + Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)), + Optional.of(backupAuthTestUtil.getRequest(mediaBackupKey, aci))).join(); assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(commit) .extracting(ex -> ex.getStatus().getCode()) @@ -489,76 +494,68 @@ public class BackupAuthManagerTest { assertThat(limit.nextPermitAvailable()).isEqualTo(expectedDuration); } + enum CredentialChangeType { + // Provided a new credential that matches the stored credential + MATCH, + // Provided a new credential that did not match the stored credential + MISMATCH, + // Provided no credential (should not update the credential) + NO_UPDATE + } + @CartesianTest void testChangeIdRateLimits( - @CartesianTest.Values(booleans = {true, false}) boolean changeMessage, - @CartesianTest.Values(booleans = {true, false}) boolean changeMedia, - @CartesianTest.Values(booleans = {true, false}) boolean rateLimitBackupId) { - - final BackupAuthManager authManager = create(BackupLevel.FREE, rateLimiter(aci, rateLimitBackupId, false)); - final BackupAuthCredentialRequest storedMessagesCredential = backupAuthTestUtil.getRequest(messagesBackupKey, aci); - final BackupAuthCredentialRequest storedMediaCredential = backupAuthTestUtil.getRequest(mediaBackupKey, aci); - final Account account = new MockAccountBuilder() - .mediaCredential(storedMediaCredential) - .messagesCredential(storedMessagesCredential) - .backupVoucher(null) - .build(); - when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account)); - - final BackupAuthCredentialRequest newMessagesCredential = changeMessage - ? backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci) - : storedMessagesCredential; - - final BackupAuthCredentialRequest newMediaCredential = changeMedia - ? backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci) - : storedMediaCredential; - - final boolean expectRateLimit = (changeMedia || changeMessage) && rateLimitBackupId; - final CompletableFuture future = authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential); - if (expectRateLimit) { - CompletableFutureTestUtil.assertFailsWithCause(RateLimitExceededException.class, future); - } else { - assertDoesNotThrow(() -> future.join()); - } - } - - @CartesianTest - void testChangePaidMediaIdRateLimits( - @CartesianTest.Values(booleans = {true, false}) boolean changeMessage, - @CartesianTest.Values(booleans = {true, false}) boolean changeMedia, + @CartesianTest.Enum CredentialChangeType messageChange, + @CartesianTest.Enum CredentialChangeType mediaChange, @CartesianTest.Values(booleans = {true, false}) boolean paid, - @CartesianTest.Values(booleans = {true, false}) boolean rateLimitPaidMedia) { + @CartesianTest.Values(booleans = {true, false}) boolean rateLimitMessagesBackupId, + @CartesianTest.Values(booleans = {true, false}) boolean rateLimitMediaBackupId) { - final BackupAuthManager authManager = create(BackupLevel.FREE, rateLimiter(aci, false, rateLimitPaidMedia)); + final BackupAuthManager authManager = + create(BackupLevel.FREE, rateLimiter(aci, rateLimitMessagesBackupId, rateLimitMediaBackupId)); final BackupAuthCredentialRequest storedMessagesCredential = backupAuthTestUtil.getRequest(messagesBackupKey, aci); final BackupAuthCredentialRequest storedMediaCredential = backupAuthTestUtil.getRequest(mediaBackupKey, aci); + // Set clock before the voucher expires if paid, otherwise after final Account.BackupVoucher backupVoucher = new Account.BackupVoucher(1, Instant.ofEpochSecond(100)); clock.pin(paid ? Instant.ofEpochSecond(99) : Instant.ofEpochSecond(101)); - final Account account = new MockAccountBuilder() .mediaCredential(storedMediaCredential) .messagesCredential(storedMessagesCredential) .backupVoucher(backupVoucher) .build(); + when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account)); - final BackupAuthCredentialRequest newMessagesCredential = changeMessage - ? backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci) - : storedMessagesCredential; + final Optional newMessagesCredential = switch (messageChange) { + case MATCH -> Optional.of(storedMessagesCredential); + case MISMATCH -> Optional.of(backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci)); + case NO_UPDATE -> Optional.empty(); + }; + final Optional newMediaCredential = switch (mediaChange) { + case MATCH -> Optional.of(storedMediaCredential); + case MISMATCH -> Optional.of(backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci)); + case NO_UPDATE -> Optional.empty(); + }; - final BackupAuthCredentialRequest newMediaCredential = changeMedia - ? backupAuthTestUtil.getRequest(TestRandomUtil.nextBytes(32), aci) - : storedMediaCredential; + // We should get rate limited if we try to change and + // 1. we are out of media changes on a paid account, or + // 2. we are out of messages changes + final boolean expectRateLimit = ((mediaChange == CredentialChangeType.MISMATCH) && rateLimitMediaBackupId && paid) + || ((messageChange == CredentialChangeType.MISMATCH) && rateLimitMessagesBackupId); + final ThrowableAssert.ThrowingCallable commit = () -> + authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential).join(); - // We should get rate limited iff we are out of paid media changes and we changed the media backup-id - final boolean expectRateLimit = changeMedia && paid && rateLimitPaidMedia; - final CompletableFuture future = authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential); - if (expectRateLimit) { - CompletableFutureTestUtil.assertFailsWithCause(RateLimitExceededException.class, future); + if (messageChange == CredentialChangeType.NO_UPDATE && mediaChange == CredentialChangeType.NO_UPDATE) { + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(commit) + .extracting(ex -> ex.getStatus().getCode()) + .isEqualTo(Status.Code.INVALID_ARGUMENT); + } else if (expectRateLimit) { + assertThatException().isThrownBy(commit).withRootCauseInstanceOf(RateLimitExceededException.class); } else { - assertDoesNotThrow(() -> future.join()); + assertThatNoException().isThrownBy(commit); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java index 610d84bdc..c42e378be 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java @@ -179,8 +179,27 @@ public class ArchiveControllerTest { assertThat(response.getStatus()).isEqualTo(204); verify(backupAuthManager).commitBackupId(AuthHelper.VALID_ACCOUNT, AuthHelper.VALID_DEVICE, - backupAuthTestUtil.getRequest(messagesBackupKey, aci), - backupAuthTestUtil.getRequest(mediaBackupKey, aci)); + Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)), + Optional.of(backupAuthTestUtil.getRequest(mediaBackupKey, aci))); + } + + @Test + public void setBackupIdPartial() { + when(backupAuthManager.commitBackupId(any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final Response response = resources.getJerseyTest() + .target("v1/archives/backupid") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(""" + {"messagesBackupAuthCredentialRequest": "%s"} + """.formatted(Base64.getEncoder().encodeToString(backupAuthTestUtil.getRequest(messagesBackupKey, aci).serialize())))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(backupAuthManager).commitBackupId(AuthHelper.VALID_ACCOUNT, AuthHelper.VALID_DEVICE, + Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)), + Optional.empty()); } @ParameterizedTest @@ -279,7 +298,6 @@ public class ArchiveControllerTest { @ParameterizedTest @CsvSource(textBlock = """ - {}, 422 '{"messagesBackupAuthCredentialRequest": "aaa", "mediaBackupAuthCredentialRequest": "aaa"}', 400 '{"messagesBackupAuthCredentialRequest": "", "mediaBackupAuthCredentialRequest": ""}', 400 """) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java index 4db25687b..107f249f6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/BackupsGrpcServiceTest.java @@ -29,6 +29,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.mockito.Mock; import org.signal.chat.backup.BackupsGrpc; import org.signal.chat.backup.GetBackupAuthCredentialsRequest; @@ -104,30 +106,31 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().setBackupId(SetBackupIdRequest.newBuilder() - .setMessagesBackupAuthCredentialRequest(ByteString.copyFrom(messagesAuthCredRequest.serialize())) - .build()) - ); - - // missing message credential - GrpcTestUtils.assertStatusException( - Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setBackupId(SetBackupIdRequest.newBuilder() - .setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize())) - .build()) - ); - - // missing all credentials - GrpcTestUtils.assertStatusException( - Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setBackupId(SetBackupIdRequest.newBuilder().build()) - ); - // invalid serialization GrpcTestUtils.assertStatusException( Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setBackupId(