Implement key transparency endpoints using simple-grpc

This commit is contained in:
Katherine
2025-06-24 14:01:35 -04:00
committed by GitHub
parent 51773f5709
commit 059caa4c57
8 changed files with 562 additions and 116 deletions

View File

@@ -35,12 +35,8 @@ import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;
import java.io.UncheckedIOException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
@@ -54,8 +50,10 @@ import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.signal.keytransparency.client.CondensedTreeSearchResponse;
import org.signal.keytransparency.client.DistinguishedResponse;
import org.signal.keytransparency.client.E164SearchRequest;
import org.signal.keytransparency.client.FullTreeHead;
import org.signal.keytransparency.client.MonitorResponse;
import org.signal.keytransparency.client.SearchProof;
import org.signal.keytransparency.client.SearchResponse;
import org.signal.keytransparency.client.UpdateValue;
@@ -81,16 +79,16 @@ import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
@ExtendWith(DropwizardExtensionsSupport.class)
public class KeyTransparencyControllerTest {
private static final String NUMBER = PhoneNumberUtil.getInstance().format(
public static final String NUMBER = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("US"),
PhoneNumberUtil.PhoneNumberFormat.E164);
private static final AciServiceIdentifier ACI = new AciServiceIdentifier(UUID.randomUUID());
private static final byte[] USERNAME_HASH = TestRandomUtil.nextBytes(20);
public static final AciServiceIdentifier ACI = new AciServiceIdentifier(UUID.randomUUID());
public static final byte[] USERNAME_HASH = TestRandomUtil.nextBytes(20);
private static final TestRemoteAddressFilterProvider TEST_REMOTE_ADDRESS_FILTER_PROVIDER
= new TestRemoteAddressFilterProvider("127.0.0.1");
private static final IdentityKey ACI_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
public static final IdentityKey ACI_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey());
private static final byte[] COMMITMENT_INDEX = new byte[32];
private static final byte[] UNIDENTIFIED_ACCESS_KEY = new byte[16];
public static final byte[] UNIDENTIFIED_ACCESS_KEY = new byte[16];
private final KeyTransparencyServiceClient keyTransparencyServiceClient = mock(KeyTransparencyServiceClient.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter searchRatelimiter = mock(RateLimiter.class);
@@ -141,8 +139,8 @@ public class KeyTransparencyControllerTest {
e164.ifPresent(ignored -> searchResponseBuilder.setE164(CondensedTreeSearchResponse.getDefaultInstance()));
usernameHash.ifPresent(ignored -> searchResponseBuilder.setUsernameHash(CondensedTreeSearchResponse.getDefaultInstance()));
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any()))
.thenReturn(CompletableFuture.completedFuture(searchResponseBuilder.build().toByteArray()));
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong()))
.thenReturn(searchResponseBuilder.build());
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/search")
@@ -167,8 +165,7 @@ public class KeyTransparencyControllerTest {
ArgumentCaptor<Optional<E164SearchRequest>> e164Argument = ArgumentCaptor.forClass(Optional.class);
verify(keyTransparencyServiceClient).search(aciArgument.capture(), aciIdentityKeyArgument.capture(),
usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L),
eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
usernameHashArgument.capture(), e164Argument.capture(), eq(Optional.of(3L)), eq(4L));
assertArrayEquals(ACI.toCompactByteArray(), aciArgument.getValue().toByteArray());
assertArrayEquals(ACI_IDENTITY_KEY.serialize(), aciIdentityKeyArgument.getValue().toByteArray());
@@ -218,8 +215,8 @@ public class KeyTransparencyControllerTest {
@ParameterizedTest
@MethodSource
void searchGrpcErrors(final Status grpcStatus, final int httpStatus) {
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong(), any()))
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
when(keyTransparencyServiceClient.search(any(), any(), any(), any(), any(), anyLong()))
.thenThrow(new StatusRuntimeException(grpcStatus));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/search")
@@ -228,7 +225,7 @@ public class KeyTransparencyControllerTest {
Entity.json(createRequestJson(new KeyTransparencySearchRequest(ACI, Optional.empty(), Optional.empty(),
ACI_IDENTITY_KEY, Optional.empty(), Optional.empty(), 4L))))) {
assertEquals(httpStatus, response.getStatus());
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong(), any());
verify(keyTransparencyServiceClient, times(1)).search(any(), any(), any(), any(), any(), anyLong());
}
}
@@ -295,8 +292,8 @@ public class KeyTransparencyControllerTest {
@Test
void monitorSuccess() {
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong(), any()))
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong()))
.thenReturn(MonitorResponse.getDefaultInstance());
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/monitor")
@@ -314,7 +311,7 @@ public class KeyTransparencyControllerTest {
assertNotNull(keyTransparencyMonitorResponse.serializedResponse());
verify(keyTransparencyServiceClient, times(1)).monitor(
any(), any(), any(), eq(3L), eq(4L), eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
any(), any(), any(), eq(3L), eq(4L));
}
}
@@ -337,8 +334,8 @@ public class KeyTransparencyControllerTest {
@ParameterizedTest
@MethodSource
void monitorGrpcErrors(final Status grpcStatus, final int httpStatus) {
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong(), any()))
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
when(keyTransparencyServiceClient.monitor(any(), any(), any(), anyLong(), anyLong()))
.thenThrow(new StatusRuntimeException(grpcStatus));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/monitor")
@@ -349,7 +346,7 @@ public class KeyTransparencyControllerTest {
new KeyTransparencyMonitorRequest.AciMonitor(ACI, 3, COMMITMENT_INDEX),
Optional.empty(), Optional.empty(), 3L, 4L))))) {
assertEquals(httpStatus, response.getStatus());
verify(keyTransparencyServiceClient, times(1)).monitor(any(), any(), any(), anyLong(), anyLong(), any());
verify(keyTransparencyServiceClient, times(1)).monitor(any(), any(), any(), anyLong(), anyLong());
}
}
@@ -500,8 +497,8 @@ public class KeyTransparencyControllerTest {
@ParameterizedTest
@CsvSource(", 1")
void distinguishedSuccess(@Nullable Long lastTreeHeadSize) {
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
.thenReturn(DistinguishedResponse.getDefaultInstance());
WebTarget webTarget = resources.getJerseyTest()
.target("/v1/key-transparency/distinguished");
@@ -518,8 +515,7 @@ public class KeyTransparencyControllerTest {
assertNotNull(distinguishedKeyResponse.serializedResponse());
verify(keyTransparencyServiceClient, times(1))
.getDistinguishedKey(eq(Optional.ofNullable(lastTreeHeadSize)),
eq(KeyTransparencyController.KEY_TRANSPARENCY_RPC_TIMEOUT));
.getDistinguishedKey(eq(Optional.ofNullable(lastTreeHeadSize)));
}
}
@@ -538,15 +534,15 @@ public class KeyTransparencyControllerTest {
@ParameterizedTest
@MethodSource
void distinguishedGrpcErrors(final Status grpcStatus, final int httpStatus) {
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
.thenReturn(CompletableFuture.failedFuture(new CompletionException(new StatusRuntimeException(grpcStatus))));
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
.thenThrow(new StatusRuntimeException(grpcStatus));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/distinguished")
.request();
try (Response response = request.get()) {
assertEquals(httpStatus, response.getStatus());
verify(keyTransparencyServiceClient).getDistinguishedKey(any(), any());
verify(keyTransparencyServiceClient).getDistinguishedKey(any());
}
}
@@ -561,8 +557,8 @@ public class KeyTransparencyControllerTest {
@Test
void distinguishedInvalidRequest() {
when(keyTransparencyServiceClient.getDistinguishedKey(any(), any()))
.thenReturn(CompletableFuture.completedFuture(TestRandomUtil.nextBytes(16)));
when(keyTransparencyServiceClient.getDistinguishedKey(any()))
.thenReturn(DistinguishedResponse.getDefaultInstance());
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/key-transparency/distinguished")

View File

@@ -0,0 +1,305 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.ByteString;
import io.grpc.Channel;
import io.grpc.Status;
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.MethodSource;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.signal.keytransparency.client.AciMonitorRequest;
import org.signal.keytransparency.client.ConsistencyParameters;
import org.signal.keytransparency.client.DistinguishedRequest;
import org.signal.keytransparency.client.DistinguishedResponse;
import org.signal.keytransparency.client.E164MonitorRequest;
import org.signal.keytransparency.client.E164SearchRequest;
import org.signal.keytransparency.client.KeyTransparencyQueryServiceGrpc;
import org.signal.keytransparency.client.MonitorRequest;
import org.signal.keytransparency.client.MonitorResponse;
import org.signal.keytransparency.client.SearchRequest;
import org.signal.keytransparency.client.SearchResponse;
import org.signal.keytransparency.client.UsernameHashMonitorRequest;
import org.signal.libsignal.protocol.IdentityKey;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.keytransparency.KeyTransparencyServiceClient;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Optional;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.ACI;
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.ACI_IDENTITY_KEY;
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.NUMBER;
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.UNIDENTIFIED_ACCESS_KEY;
import static org.whispersystems.textsecuregcm.controllers.KeyTransparencyControllerTest.USERNAME_HASH;
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
import static org.whispersystems.textsecuregcm.grpc.KeyTransparencyGrpcService.COMMITMENT_INDEX_LENGTH;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class KeyTransparencyGrpcServiceTest extends SimpleBaseGrpcTest<KeyTransparencyGrpcService, KeyTransparencyQueryServiceGrpc.KeyTransparencyQueryServiceBlockingStub>{
@Mock
private KeyTransparencyServiceClient keyTransparencyServiceClient;
@Mock
private RateLimiter rateLimiter;
@Override
protected KeyTransparencyGrpcService createServiceBeforeEachTest() {
final RateLimiters rateLimiters = mock(RateLimiters.class);
when(rateLimiters.getKeyTransparencySearchLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getKeyTransparencyDistinguishedLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getKeyTransparencyMonitorLimiter()).thenReturn(rateLimiter);
return new KeyTransparencyGrpcService(rateLimiters, keyTransparencyServiceClient);
}
@Override
protected KeyTransparencyQueryServiceGrpc.KeyTransparencyQueryServiceBlockingStub createStub(final Channel channel) {
return KeyTransparencyQueryServiceGrpc.newBlockingStub(channel);
}
@Test
void searchSuccess() throws RateLimitExceededException {
when(keyTransparencyServiceClient.search(any())).thenReturn(SearchResponse.getDefaultInstance());
Mockito.doNothing().when(rateLimiter).validate(any(String.class));
final SearchRequest request = SearchRequest.newBuilder()
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
.setAciIdentityKey(ByteString.copyFrom(ACI_IDENTITY_KEY.serialize()))
.setConsistency(ConsistencyParameters.newBuilder()
.setDistinguished(10)
.build())
.build();
assertDoesNotThrow(() -> unauthenticatedServiceStub().search(request));
verify(keyTransparencyServiceClient, times(1)).search(eq(request));
}
@ParameterizedTest
@MethodSource
void searchInvalidRequest(final Optional<byte[]> aciServiceIdentifier,
final Optional<IdentityKey> aciIdentityKey,
final Optional<String> e164,
final Optional<byte[]> unidentifiedAccessKey,
final Optional<byte[]> usernameHash,
final Optional<Long> lastTreeHeadSize,
final Optional<Long> distinguishedTreeHeadSize) {
final SearchRequest.Builder requestBuilder = SearchRequest.newBuilder();
aciServiceIdentifier.ifPresent(v -> requestBuilder.setAci(ByteString.copyFrom(v)));
aciIdentityKey.ifPresent(v -> requestBuilder.setAciIdentityKey(ByteString.copyFrom(v.serialize())));
usernameHash.ifPresent(v -> requestBuilder.setUsernameHash(ByteString.copyFrom(v)));
final E164SearchRequest.Builder e164RequestBuilder = E164SearchRequest.newBuilder();
e164.ifPresent(e164RequestBuilder::setE164);
unidentifiedAccessKey.ifPresent(v -> e164RequestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(v)));
requestBuilder.setE164SearchRequest(e164RequestBuilder.build());
final ConsistencyParameters.Builder consistencyBuilder = ConsistencyParameters.newBuilder();
distinguishedTreeHeadSize.ifPresent(consistencyBuilder::setDistinguished);
lastTreeHeadSize.ifPresent(consistencyBuilder::setLast);
requestBuilder.setConsistency(consistencyBuilder.build());
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().search(requestBuilder.build()));
verifyNoInteractions(keyTransparencyServiceClient);
}
private static Stream<Arguments> searchInvalidRequest() {
byte[] aciBytes = ACI.toCompactByteArray();
return Stream.of(
Arguments.argumentSet("Empty ACI", Optional.empty(), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
Arguments.argumentSet("Null ACI identity key", Optional.of(aciBytes), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
Arguments.argumentSet("Invalid ACI", Optional.of(new byte[15]), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
Arguments.argumentSet("Non-positive consistency.last", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(0L), Optional.of(4L)),
Arguments.argumentSet("consistency.distinguished not provided",Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()),
Arguments.argumentSet("Non-positive consistency.distinguished",Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(0L)),
Arguments.argumentSet("E164 can't be provided without an unidentified access key", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.of(NUMBER), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L)),
Arguments.argumentSet("Unidentified access key can't be provided without E164", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.of(UNIDENTIFIED_ACCESS_KEY), Optional.empty(), Optional.empty(), Optional.of(4L)),
Arguments.argumentSet("Invalid username hash", Optional.of(aciBytes), Optional.of(ACI_IDENTITY_KEY), Optional.empty(), Optional.empty(), Optional.of(new byte[19]), Optional.empty(), Optional.of(4L))
);
}
@Test
void searchRatelimited() throws RateLimitExceededException {
final Duration retryAfterDuration = Duration.ofMinutes(7);
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
final SearchRequest request = SearchRequest.newBuilder()
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
.setAciIdentityKey(ByteString.copyFrom(ACI_IDENTITY_KEY.serialize()))
.setConsistency(ConsistencyParameters.newBuilder()
.setDistinguished(10)
.build())
.build();
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().search(request));
verifyNoInteractions(keyTransparencyServiceClient);
}
@Test
void monitorSuccess() {
when(keyTransparencyServiceClient.monitor(any())).thenReturn(MonitorResponse.getDefaultInstance());
when(rateLimiter.validateReactive(any(String.class)))
.thenReturn(Mono.empty());
final AciMonitorRequest aciMonitorRequest = AciMonitorRequest.newBuilder()
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
.setCommitmentIndex(ByteString.copyFrom(new byte[COMMITMENT_INDEX_LENGTH]))
.setEntryPosition(10)
.build();
final MonitorRequest request = MonitorRequest.newBuilder()
.setAci(aciMonitorRequest)
.setConsistency(ConsistencyParameters.newBuilder()
.setDistinguished(10)
.setLast(10)
.build())
.build();
assertDoesNotThrow(() -> unauthenticatedServiceStub().monitor(request));
verify(keyTransparencyServiceClient, times(1)).monitor(eq(request));
}
@ParameterizedTest
@MethodSource
void monitorInvalidRequest(final Optional<AciMonitorRequest> aciMonitorRequest,
final Optional<E164MonitorRequest> e164MonitorRequest,
final Optional<UsernameHashMonitorRequest> usernameHashMonitorRequest,
final Optional<Long> lastTreeHeadSize,
final Optional<Long> distinguishedTreeHeadSize) {
final MonitorRequest.Builder requestBuilder = MonitorRequest.newBuilder();
aciMonitorRequest.ifPresent(requestBuilder::setAci);
e164MonitorRequest.ifPresent(requestBuilder::setE164);
usernameHashMonitorRequest.ifPresent(requestBuilder::setUsernameHash);
final ConsistencyParameters.Builder consistencyBuilder = ConsistencyParameters.newBuilder();
lastTreeHeadSize.ifPresent(consistencyBuilder::setLast);
distinguishedTreeHeadSize.ifPresent(consistencyBuilder::setDistinguished);
requestBuilder.setConsistency(consistencyBuilder.build());
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().monitor(requestBuilder.build()));
}
private static Stream<Arguments> monitorInvalidRequest() {
final Optional<AciMonitorRequest> validAciMonitorRequest = Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[32], 10));
return Stream.of(
Arguments.argumentSet("ACI monitor request can't be unset", Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("ACI can't be empty",Optional.of(AciMonitorRequest.newBuilder().build()), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Empty ACI on ACI monitor request",Optional.of(constructAciMonitorRequest(new byte[0], new byte[32], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid ACI", Optional.of(constructAciMonitorRequest(new byte[15], new byte[32], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid commitment index on ACI monitor request", Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[31], 10)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid entry position on ACI monitor request", Optional.of(constructAciMonitorRequest(ACI.toCompactByteArray(), new byte[32], 0)), Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("E164 can't be blank", validAciMonitorRequest, Optional.of(constructE164MonitorRequest("", new byte[32], 10)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid commitment index on E164 monitor request", validAciMonitorRequest, Optional.of(constructE164MonitorRequest(NUMBER, new byte[31], 10)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid entry position on E164 monitor request", validAciMonitorRequest, Optional.of(constructE164MonitorRequest(NUMBER, new byte[32], 0)), Optional.empty(), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Username hash can't be empty", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(new byte[0], new byte[32], 10)), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid username hash length", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(new byte[31], new byte[32], 10)), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid commitment index on username hash monitor request", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(USERNAME_HASH, new byte[31], 10)), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("Invalid entry position on username hash monitor request", validAciMonitorRequest, Optional.empty(), Optional.of(constructUsernameHashMonitorRequest(USERNAME_HASH, new byte[32], 0)), Optional.of(4L), Optional.of(4L)),
Arguments.argumentSet("consistency.last must be provided", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(4L),
Arguments.argumentSet("consistency.last must be positive", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(0L), Optional.of(4L)),
Arguments.argumentSet("consistency.distinguished must be provided", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(4L)), Optional.empty()),
Arguments.argumentSet("consistency.distinguished must be positive", validAciMonitorRequest, Optional.empty(), Optional.empty(), Optional.of(4L), Optional.of(0L))
);
}
@Test
void monitorRatelimited() throws RateLimitExceededException {
final Duration retryAfterDuration = Duration.ofMinutes(7);
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
final AciMonitorRequest aciMonitorRequest = AciMonitorRequest.newBuilder()
.setAci(ByteString.copyFrom(ACI.toCompactByteArray()))
.setCommitmentIndex(ByteString.copyFrom(new byte[COMMITMENT_INDEX_LENGTH]))
.setEntryPosition(10)
.build();
final MonitorRequest request = MonitorRequest.newBuilder()
.setAci(aciMonitorRequest)
.setConsistency(ConsistencyParameters.newBuilder()
.setDistinguished(10)
.setLast(10)
.build())
.build();
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().monitor(request));
verifyNoInteractions(keyTransparencyServiceClient);
}
@Test
void distinguishedSuccess() {
when(keyTransparencyServiceClient.distinguished(any())).thenReturn(DistinguishedResponse.getDefaultInstance());
when(rateLimiter.validateReactive(any(String.class)))
.thenReturn(Mono.empty());
final DistinguishedRequest request = DistinguishedRequest.newBuilder().build();
assertDoesNotThrow(() -> unauthenticatedServiceStub().distinguished(request));
verify(keyTransparencyServiceClient, times(1)).distinguished(eq(request));
}
@Test
void distinguishedInvalidRequest() {
final DistinguishedRequest request = DistinguishedRequest.newBuilder()
.setLast(0)
.build();
assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().distinguished(request));
verifyNoInteractions(keyTransparencyServiceClient);
}
@Test
void distinguishedRatelimited() throws RateLimitExceededException {
final Duration retryAfterDuration = Duration.ofMinutes(7);
Mockito.doThrow(new RateLimitExceededException(retryAfterDuration)).when(rateLimiter).validate(any(String.class));
final DistinguishedRequest request = DistinguishedRequest.newBuilder()
.setLast(10)
.build();
assertRateLimitExceeded(retryAfterDuration, () -> unauthenticatedServiceStub().distinguished(request));
verifyNoInteractions(keyTransparencyServiceClient);
}
private static AciMonitorRequest constructAciMonitorRequest(final byte[] aci, final byte[] commitmentIndex, final long entryPosition) {
return AciMonitorRequest.newBuilder()
.setAci(ByteString.copyFrom(aci))
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
.setEntryPosition(entryPosition)
.build();
}
private static E164MonitorRequest constructE164MonitorRequest(final String e164, final byte[] commitmentIndex, final long entryPosition) {
return E164MonitorRequest.newBuilder()
.setE164(e164)
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
.setEntryPosition(entryPosition)
.build();
}
private static UsernameHashMonitorRequest constructUsernameHashMonitorRequest(final byte[] usernameHash, final byte[] commitmentIndex, final long entryPosition) {
return UsernameHashMonitorRequest.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.setCommitmentIndex(ByteString.copyFrom(commitmentIndex))
.setEntryPosition(entryPosition)
.build();
}
}