Add gRPC backup services

This commit is contained in:
Ravi Khadiwala
2024-05-28 13:41:14 -05:00
committed by ravi-signal
parent 3ca9a66323
commit a88560e557
8 changed files with 1482 additions and 2 deletions

View File

@@ -0,0 +1,328 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.time.Clock;
import java.util.Arrays;
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 org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.Mock;
import org.signal.chat.backup.BackupsAnonymousGrpc;
import org.signal.chat.backup.CopyMediaItem;
import org.signal.chat.backup.CopyMediaRequest;
import org.signal.chat.backup.CopyMediaResponse;
import org.signal.chat.backup.DeleteMediaItem;
import org.signal.chat.backup.DeleteMediaRequest;
import org.signal.chat.backup.GetBackupInfoRequest;
import org.signal.chat.backup.GetBackupInfoResponse;
import org.signal.chat.backup.GetCdnCredentialsRequest;
import org.signal.chat.backup.GetCdnCredentialsResponse;
import org.signal.chat.backup.GetUploadFormRequest;
import org.signal.chat.backup.GetUploadFormResponse;
import org.signal.chat.backup.ListMediaRequest;
import org.signal.chat.backup.ListMediaResponse;
import org.signal.chat.backup.SetPublicKeyRequest;
import org.signal.chat.backup.SignedPresentation;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
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.BackupManager;
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
import org.whispersystems.textsecuregcm.backup.CopyResult;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import reactor.core.publisher.Flux;
class BackupsAnonymousGrpcServiceTest extends
SimpleBaseGrpcTest<BackupsAnonymousGrpcService, BackupsAnonymousGrpc.BackupsAnonymousBlockingStub> {
private final UUID aci = UUID.randomUUID();
private final byte[] messagesBackupKey = TestRandomUtil.nextBytes(32);
private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(Clock.systemUTC());
private final BackupAuthCredentialPresentation presentation =
presentation(backupAuthTestUtil, messagesBackupKey, aci);
@Mock
private BackupManager backupManager;
@Override
protected BackupsAnonymousGrpcService createServiceBeforeEachTest() {
return new BackupsAnonymousGrpcService(backupManager);
}
@BeforeEach
void setup() {
when(backupManager.authenticateBackupUser(any(), any()))
.thenReturn(CompletableFuture.completedFuture(
backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
}
@Test
void setPublicKey() {
when(backupManager.setPublicKey(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
assertThatNoException().isThrownBy(() -> unauthenticatedServiceStub().setPublicKey(SetPublicKeyRequest.newBuilder()
.setPublicKey(ByteString.copyFrom(Curve.generateKeyPair().getPublicKey().serialize()))
.setSignedPresentation(signedPresentation(presentation))
.build()));
}
@Test
void setBadPublicKey() {
when(backupManager.setPublicKey(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() ->
unauthenticatedServiceStub().setPublicKey(SetPublicKeyRequest.newBuilder()
.setPublicKey(ByteString.copyFromUtf8("aaaaa")) // Invalid public key
.setSignedPresentation(signedPresentation(presentation))
.build()))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
}
@Test
void setMissingPublicKey() {
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() ->
unauthenticatedServiceStub().setPublicKey(SetPublicKeyRequest.newBuilder()
// Missing public key
.setSignedPresentation(signedPresentation(presentation))
.build()))
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.INVALID_ARGUMENT);
}
@Test
void putMediaBatchSuccess() {
final byte[][] mediaIds = {TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
when(backupManager.copyToBackup(any(), any()))
.thenReturn(Flux.just(
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[1], 1)));
final CopyMediaRequest request = CopyMediaRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.addItems(CopyMediaItem.newBuilder()
.setSourceAttachmentCdn(3)
.setSourceKey("abc")
.setObjectLength(100)
.setMediaId(ByteString.copyFrom(mediaIds[0]))
.setHmacKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.setEncryptionKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.build())
.addItems(CopyMediaItem.newBuilder()
.setSourceAttachmentCdn(3)
.setSourceKey("def")
.setObjectLength(200)
.setMediaId(ByteString.copyFrom(mediaIds[1]))
.setHmacKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.setEncryptionKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.build())
.build();
final Iterator<CopyMediaResponse> it = unauthenticatedServiceStub().copyMedia(request);
for (int i = 0; i < 2; i++) {
final CopyMediaResponse response = it.next();
assertThat(response.getSuccess().getCdn()).isEqualTo(1);
assertThat(response.getMediaId().toByteArray()).isEqualTo(mediaIds[i]);
}
assertThat(it.hasNext()).isFalse();
}
@Test
void putMediaBatchPartialFailure() {
// Copy four different mediaIds, with a variety of success/failure outcomes
final byte[][] mediaIds = IntStream.range(0, 4).mapToObj(i -> TestRandomUtil.nextBytes(15)).toArray(byte[][]::new);
final CopyResult.Outcome[] outcomes = new CopyResult.Outcome[]{
CopyResult.Outcome.SUCCESS,
CopyResult.Outcome.SOURCE_NOT_FOUND,
CopyResult.Outcome.SOURCE_WRONG_LENGTH,
CopyResult.Outcome.OUT_OF_QUOTA
};
when(backupManager.copyToBackup(any(), any()))
.thenReturn(Flux.fromStream(IntStream.range(0, 4)
.mapToObj(i -> new CopyResult(
outcomes[i],
mediaIds[i],
outcomes[i] == CopyResult.Outcome.SUCCESS ? 1 : null))));
final CopyMediaRequest request = CopyMediaRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.addAllItems(Arrays.stream(mediaIds)
.map(mediaId -> CopyMediaItem.newBuilder()
.setSourceAttachmentCdn(3)
.setSourceKey("abc")
.setObjectLength(100)
.setMediaId(ByteString.copyFrom(mediaId))
.setHmacKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.setEncryptionKey(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
.build())
.collect(Collectors.toList()))
.build();
final Iterator<CopyMediaResponse> responses = unauthenticatedServiceStub().copyMedia(request);
// Verify that we get the expected response for each mediaId
for (int i = 0; i < mediaIds.length; i++) {
final CopyMediaResponse response = responses.next();
switch (outcomes[i]) {
case SUCCESS -> assertThat(response.getSuccess().getCdn()).isEqualTo(1);
case SOURCE_WRONG_LENGTH -> assertThat(response.getWrongSourceLength()).isNotNull();
case OUT_OF_QUOTA -> assertThat(response.getOutOfSpace()).isNotNull();
case SOURCE_NOT_FOUND -> assertThat(response.getSourceNotFound()).isNotNull();
}
assertThat(response.getMediaId().toByteArray()).isEqualTo(mediaIds[i]);
}
}
@Test
void getBackupInfo() {
when(backupManager.backupInfo(any())).thenReturn(CompletableFuture.completedFuture(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);
}
@CartesianTest
void list(
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
throws VerificationFailedException {
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();
final int limit = 17;
when(backupManager.list(any(), eq(expectedCursor), eq(limit)))
.thenReturn(CompletableFuture.completedFuture(new BackupManager.ListMediaResult(
List.of(new BackupManager.StorageDescriptorWithLength(1, mediaId, 100)),
returnedCursor)));
final ListMediaRequest.Builder request = ListMediaRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.setLimit(limit);
if (cursorProvided) {
request.setCursor("myCursor");
}
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));
}
@Test
void delete() {
final DeleteMediaRequest request = DeleteMediaRequest.newBuilder()
.setSignedPresentation(signedPresentation(presentation))
.addAllItems(IntStream.range(0, 100).mapToObj(i ->
DeleteMediaItem.newBuilder()
.setCdn(3)
.setMediaId(ByteString.copyFrom(TestRandomUtil.nextBytes(15)))
.build())
.toList()).build();
when(backupManager.deleteMedia(any(), any()))
.thenReturn(Flux.fromStream(request.getItemsList().stream()
.map(m -> new BackupManager.StorageDescriptor(m.getCdn(), m.getMediaId().toByteArray()))));
final AtomicInteger count = new AtomicInteger(0);
unauthenticatedServiceStub().deleteMedia(request).forEachRemaining(i -> count.getAndIncrement());
assertThat(count.get()).isEqualTo(100);
}
@Test
void mediaUploadForm() {
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
.thenReturn(CompletableFuture.completedFuture(
new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org")));
final GetUploadFormRequest request = GetUploadFormRequest.newBuilder()
.setMedia(GetUploadFormRequest.MediaUploadType.getDefaultInstance())
.setSignedPresentation(signedPresentation(presentation))
.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");
// rate limit
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
.thenReturn(CompletableFuture.failedFuture(new RateLimitExceededException(null)));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> unauthenticatedServiceStub().getUploadForm(request))
.extracting(StatusRuntimeException::getStatus)
.isEqualTo(Status.RESOURCE_EXHAUSTED);
}
@Test
void readAuth() {
when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value"));
final GetCdnCredentialsResponse response = unauthenticatedServiceStub().getCdnCredentials(
GetCdnCredentialsRequest.newBuilder()
.setCdn(3)
.setSignedPresentation(signedPresentation(presentation))
.build());
assertThat(response.getHeadersMap()).containsExactlyEntriesOf(Map.of("key", "value"));
}
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType,
final BackupLevel backupLevel) {
return new AuthenticatedBackupUser(backupId, credentialType, backupLevel, "myBackupDir", "myMediaDir");
}
private static BackupAuthCredentialPresentation presentation(BackupAuthTestUtil backupAuthTestUtil,
byte[] messagesBackupKey, UUID aci) {
try {
return backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
} catch (VerificationFailedException e) {
throw new RuntimeException(e);
}
}
private static SignedPresentation signedPresentation(BackupAuthCredentialPresentation presentation) {
return SignedPresentation.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize()))
.setPresentationSignature(ByteString.copyFromUtf8("aaa")).build();
}
}

View File

@@ -0,0 +1,243 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.Mock;
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.SetBackupIdRequest;
import org.signal.chat.common.ZkCredential;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
import org.signal.libsignal.zkgroup.backups.BackupLevel;
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredential;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.EnumMapUtil;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, BackupsGrpc.BackupsBlockingStub> {
private final byte[] messagesBackupKey = TestRandomUtil.nextBytes(32);
private final byte[] mediaBackupKey = TestRandomUtil.nextBytes(32);
private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(Clock.systemUTC());
final BackupAuthCredentialRequest mediaAuthCredRequest =
backupAuthTestUtil.getRequest(mediaBackupKey, AUTHENTICATED_ACI);
final BackupAuthCredentialRequest messagesAuthCredRequest =
backupAuthTestUtil.getRequest(messagesBackupKey, AUTHENTICATED_ACI);
private final Account account = mock(Account.class);
@Mock
private BackupAuthManager backupAuthManager;
@Mock
private AccountsManager accountsManager;
@Override
protected BackupsGrpcService createServiceBeforeEachTest() {
return new BackupsGrpcService(accountsManager, backupAuthManager);
}
@BeforeEach
void setup() {
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
}
@Test
void setBackupId() {
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
authenticatedServiceStub().setBackupId(
SetBackupIdRequest.newBuilder()
.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()))
.setMessagesBackupAuthCredentialRequest(ByteString.copyFrom(messagesAuthCredRequest.serialize()))
.build());
verify(backupAuthManager).commitBackupId(account, messagesAuthCredRequest, mediaAuthCredRequest);
}
@Test
void setBackupIdInvalid() {
// missing media credential
GrpcTestUtils.assertStatusException(
Status.INVALID_ARGUMENT, () -> 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(
SetBackupIdRequest.newBuilder()
.setMessagesBackupAuthCredentialRequest(ByteString.fromHex("FF"))
.setMediaBackupAuthCredentialRequest(ByteString.fromHex("FF"))
.build())
);
}
public static Stream<Arguments> setBackupIdException() {
return Stream.of(
Arguments.of(new RateLimitExceededException(null), false, Status.RESOURCE_EXHAUSTED),
Arguments.of(Status.INVALID_ARGUMENT.withDescription("async").asRuntimeException(), false,
Status.INVALID_ARGUMENT),
Arguments.of(Status.INVALID_ARGUMENT.withDescription("sync").asRuntimeException(), true,
Status.INVALID_ARGUMENT)
);
}
@ParameterizedTest
@MethodSource
void setBackupIdException(final Exception ex, final boolean sync, final Status expected) {
if (sync) {
when(backupAuthManager.commitBackupId(any(), any(), any())).thenThrow(ex);
} else {
when(backupAuthManager.commitBackupId(any(), any(), any())).thenReturn(CompletableFuture.failedFuture(ex));
}
GrpcTestUtils.assertStatusException(
expected, () -> authenticatedServiceStub().setBackupId(SetBackupIdRequest.newBuilder()
.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()))
.setMessagesBackupAuthCredentialRequest(ByteString.copyFrom(messagesAuthCredRequest.serialize()))
.build())
);
}
@Test
void redeemReceipt() throws InvalidInputException, VerificationFailedException {
final ServerSecretParams params = ServerSecretParams.generate();
final ServerZkReceiptOperations serverOps = new ServerZkReceiptOperations(params);
final ClientZkReceiptOperations clientOps = new ClientZkReceiptOperations(params.getPublicParams());
final ReceiptCredentialRequestContext rcrc = clientOps
.createReceiptCredentialRequestContext(new ReceiptSerial(TestRandomUtil.nextBytes(ReceiptSerial.SIZE)));
final ReceiptCredentialResponse rcr = serverOps.issueReceiptCredential(rcrc.getRequest(), 0L, 3L);
final ReceiptCredential receiptCredential = clientOps.receiveReceiptCredential(rcrc, rcr);
final ReceiptCredentialPresentation presentation = clientOps.createReceiptCredentialPresentation(receiptCredential);
when(backupAuthManager.redeemReceipt(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
authenticatedServiceStub().redeemReceipt(RedeemReceiptRequest.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize()))
.build());
verify(backupAuthManager).redeemReceipt(account, presentation);
}
@Test
void getCredentials() {
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
final Instant end = start.plus(Duration.ofDays(1));
final Map<BackupCredentialType, List<BackupAuthManager.Credential>> expectedCredentialsByType =
EnumMapUtil.toEnumMap(BackupCredentialType.class, credentialType -> backupAuthTestUtil.getCredentials(
BackupLevel.PAID, backupAuthTestUtil.getRequest(messagesBackupKey, AUTHENTICATED_ACI), credentialType,
start, end));
expectedCredentialsByType.forEach((credentialType, expectedCredentials) ->
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(start), eq(end)))
.thenReturn(CompletableFuture.completedFuture(expectedCredentials)));
final GetBackupAuthCredentialsResponse credentialResponse = authenticatedServiceStub().getBackupAuthCredentials(
GetBackupAuthCredentialsRequest.newBuilder()
.setRedemptionStart(start.getEpochSecond()).setRedemptionStop(end.getEpochSecond())
.build());
expectedCredentialsByType.forEach((credentialType, expectedCredentials) -> {
final Map<Long, ZkCredential> creds = switch (credentialType) {
case MESSAGES -> credentialResponse.getMessageCredentialsMap();
case MEDIA -> credentialResponse.getMediaCredentialsMap();
};
assertThat(creds).hasSize(expectedCredentials.size()).containsKey(start.getEpochSecond());
for (BackupAuthManager.Credential expectedCred : expectedCredentials) {
assertThat(creds)
.extractingByKey(expectedCred.redemptionTime().getEpochSecond())
.isNotNull()
.extracting(ZkCredential::getCredential)
.extracting(ByteString::toByteArray)
.isEqualTo(expectedCred.credential().serialize());
}
});
}
@ParameterizedTest
@CsvSource({
"true, false",
"false, true",
"true, true"
})
void getCredentialsBadInput(final boolean missingStart, final boolean missingEnd) {
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
final Instant end = start.plus(Duration.ofDays(1));
final GetBackupAuthCredentialsRequest.Builder builder = GetBackupAuthCredentialsRequest.newBuilder();
if (!missingStart) {
builder.setRedemptionStart(start.getEpochSecond());
}
if (!missingEnd) {
builder.setRedemptionStop(end.getEpochSecond());
}
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().getBackupAuthCredentials(builder.build()));
}
}

View File

@@ -35,7 +35,7 @@ public final class GrpcTestUtils {
final BindableService service) {
mockAuthenticationInterceptor.setAuthenticatedDevice(authenticatedAci, authenticatedDeviceId);
extension.getServiceRegistry()
.addService(ServerInterceptors.intercept(service, mockRequestAttributesInterceptor, mockAuthenticationInterceptor, new ErrorMappingInterceptor()));
.addService(ServerInterceptors.intercept(service, new ValidatingInterceptor(), mockRequestAttributesInterceptor, mockAuthenticationInterceptor, new ErrorMappingInterceptor()));
}
public static void setupUnauthenticatedExtension(
@@ -43,7 +43,7 @@ public final class GrpcTestUtils {
final MockRequestAttributesInterceptor mockRequestAttributesInterceptor,
final BindableService service) {
extension.getServiceRegistry()
.addService(ServerInterceptors.intercept(service, mockRequestAttributesInterceptor, new ErrorMappingInterceptor()));
.addService(ServerInterceptors.intercept(service, new ValidatingInterceptor(), mockRequestAttributesInterceptor, new ErrorMappingInterceptor()));
}
public static void assertStatusException(final Status expected, final Executable serviceCall) {