Profile gRPC: Define `getVersionedProfile` endpoint

This commit is contained in:
Katherine Yen 2023-08-30 14:47:11 -07:00 committed by GitHub
parent 5afc058f90
commit dd18fcaea2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 354 additions and 4 deletions

View File

@ -649,7 +649,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.addService(new KeysAnonymousGrpcService(accountsManager, keys))
.addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager,
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
.addService(new ProfileAnonymousGrpcService(accountsManager, profileBadgeConverter));
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter));
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
environment.servlets()

View File

@ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.grpc;
import io.grpc.Status;
import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileAnonymousRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.ReactorProfileAnonymousGrpc;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
@ -10,16 +12,20 @@ import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import reactor.core.publisher.Mono;
public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.ProfileAnonymousImplBase {
private final AccountsManager accountsManager;
private final ProfilesManager profilesManager;
private final ProfileBadgeConverter profileBadgeConverter;
public ProfileAnonymousGrpcService(
final AccountsManager accountsManager,
final ProfilesManager profilesManager,
final ProfileBadgeConverter profileBadgeConverter) {
this.accountsManager = accountsManager;
this.profilesManager = profilesManager;
this.profileBadgeConverter = profileBadgeConverter;
}
@ -40,8 +46,20 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro
profileBadgeConverter));
}
private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) {
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier))
@Override
public Mono<GetVersionedProfileResponse> getVersionedProfile(final GetVersionedProfileAnonymousRequest request) {
final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier());
if (targetIdentifier.identityType() != IdentityType.ACI) {
throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException();
}
return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray())
.flatMap(targetAccount -> ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion()));
}
private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) {
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier))
.flatMap(Mono::justOrEmpty)
.filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey))
.switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException()));

View File

@ -5,18 +5,51 @@ import com.google.protobuf.ByteString;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import io.grpc.Status;
import org.signal.chat.profile.Badge;
import org.signal.chat.profile.BadgeSvg;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.UserCapabilities;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.ProfileHelper;
import reactor.core.publisher.Mono;
public class ProfileGrpcHelper {
static Mono<GetVersionedProfileResponse> getVersionedProfile(final Account account,
final ProfilesManager profilesManager,
final String requestVersion) {
return Mono.fromFuture(() -> profilesManager.getAsync(account.getUuid(), requestVersion))
.map(maybeProfile -> {
if (maybeProfile.isEmpty()) {
throw Status.NOT_FOUND.withDescription("Profile version not found").asRuntimeException();
}
final GetVersionedProfileResponse.Builder responseBuilder = GetVersionedProfileResponse.newBuilder();
maybeProfile.map(VersionedProfile::name).map(ByteString::copyFrom).ifPresent(responseBuilder::setName);
maybeProfile.map(VersionedProfile::about).map(ByteString::copyFrom).ifPresent(responseBuilder::setAbout);
maybeProfile.map(VersionedProfile::aboutEmoji).map(ByteString::copyFrom).ifPresent(responseBuilder::setAboutEmoji);
maybeProfile.map(VersionedProfile::avatar).ifPresent(responseBuilder::setAvatar);
// Allow requests where either the version matches the latest version on Account or the latest version on Account
// is empty to read the payment address.
maybeProfile
.filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(requestVersion)).orElse(true))
.map(VersionedProfile::paymentAddress)
.map(ByteString::copyFrom)
.ifPresent(responseBuilder::setPaymentAddress);
return responseBuilder.build();
});
}
@VisibleForTesting
static List<Badge> buildBadges(final List<org.whispersystems.textsecuregcm.entities.Badge> badges) {
final ArrayList<Badge> grpcBadges = new ArrayList<>();

View File

@ -4,6 +4,8 @@ import com.google.protobuf.ByteString;
import io.grpc.Status;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.SetProfileRequest.AvatarChange;
import org.signal.chat.profile.ProfileAvatarUploadAttributes;
import org.signal.chat.profile.ReactorProfileGrpc;
@ -15,6 +17,7 @@ import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
@ -169,6 +172,20 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
profileBadgeConverter));
}
@Override
public Mono<GetVersionedProfileResponse> getVersionedProfile(final GetVersionedProfileRequest request) {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
final ServiceIdentifier targetIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getAccountIdentifier());
if (targetIdentifier.identityType() != IdentityType.ACI) {
throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException();
}
return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier)
.flatMap(account -> ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion()));
}
private Mono<Account> validateRateLimitAndGetAccount(final UUID requesterUuid,
final ServiceIdentifier targetIdentifier) {
return rateLimiters.getProfileLimiter().validateReactive(requesterUuid)

View File

@ -162,7 +162,7 @@ message GetVersionedProfileRequest {
/**
* The profile version to retrieve.
*/
bytes version = 2;
string version = 2;
}
message GetVersionedProfileAnonymousRequest {

View File

@ -31,6 +31,9 @@ import org.signal.chat.common.ServiceIdentifier;
import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileAnonymousRequest;
import org.signal.chat.profile.GetVersionedProfileRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.ProfileAnonymousGrpc;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.Curve;
@ -43,11 +46,16 @@ import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.ProfileHelper;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import javax.annotation.Nullable;
public class ProfileAnonymousGrpcServiceTest {
private Account account;
private AccountsManager accountsManager;
private ProfilesManager profilesManager;
private ProfileBadgeConverter profileBadgeConverter;
private ProfileAnonymousGrpc.ProfileAnonymousBlockingStub profileAnonymousBlockingStub;
@ -58,16 +66,19 @@ public class ProfileAnonymousGrpcServiceTest {
void setup() {
account = mock(Account.class);
accountsManager = mock(AccountsManager.class);
profilesManager = mock(ProfilesManager.class);
profileBadgeConverter = mock(ProfileBadgeConverter.class);
final Metadata metadata = new Metadata();
metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us");
metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3");
profileAnonymousBlockingStub = ProfileAnonymousGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel())
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
final ProfileAnonymousGrpcService profileAnonymousGrpcService = new ProfileAnonymousGrpcService(
accountsManager,
profilesManager,
profileBadgeConverter
);
@ -166,4 +177,153 @@ public class ProfileAnonymousGrpcServiceTest {
Arguments.of(IdentityType.IDENTITY_TYPE_ACI, false, true)
);
}
@ParameterizedTest
@MethodSource
void getVersionedProfile(final String requestVersion,
@Nullable final String accountVersion,
final boolean expectResponseHasPaymentAddress) {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
final VersionedProfile profile = mock(VersionedProfile.class);
final byte[] name = ProfileHelper.generateRandomByteArray(81);
final byte[] emoji = ProfileHelper.generateRandomByteArray(60);
final byte[] about = ProfileHelper.generateRandomByteArray(156);
final byte[] paymentAddress = ProfileHelper.generateRandomByteArray(582);
final String avatar = "profiles/" + ProfileHelper.generateRandomBase64FromByteArray(16);
when(profile.name()).thenReturn(name);
when(profile.aboutEmoji()).thenReturn(emoji);
when(profile.about()).thenReturn(about);
when(profile.paymentAddress()).thenReturn(paymentAddress);
when(profile.avatar()).thenReturn(avatar);
when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion));
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile)));
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion(requestVersion)
.build())
.build();
final GetVersionedProfileResponse response = profileAnonymousBlockingStub.getVersionedProfile(request);
final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder()
.setName(ByteString.copyFrom(name))
.setAbout(ByteString.copyFrom(about))
.setAboutEmoji(ByteString.copyFrom(emoji))
.setAvatar(avatar);
if (expectResponseHasPaymentAddress) {
expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress));
}
assertEquals(expectedResponseBuilder.build(), response);
}
private static Stream<Arguments> getVersionedProfile() {
return Stream.of(
Arguments.of("version1", "version1", true),
Arguments.of("version1", null, true),
Arguments.of("version1", "version2", false)
);
}
@Test
void getVersionedProfileVersionNotFound() {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build())
.build();
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(request));
assertEquals(Status.NOT_FOUND.getCode(), statusRuntimeException.getStatus().getCode());
}
@ParameterizedTest
@MethodSource
void getVersionedProfileUnauthenticated(final boolean missingUnidentifiedAccessKey,
final boolean accountNotFound) {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(
CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account)));
final GetVersionedProfileAnonymousRequest.Builder requestBuilder = GetVersionedProfileAnonymousRequest.newBuilder()
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build());
if (!missingUnidentifiedAccessKey) {
requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey));
}
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(requestBuilder.build()));
assertEquals(Status.UNAUTHENTICATED.getCode(), statusRuntimeException.getStatus().getCode());
}
private static Stream<Arguments> getVersionedProfileUnauthenticated() {
return Stream.of(
Arguments.of(true, false),
Arguments.of(false, true)
);
}
@Test
void getVersionedProfilePniInvalidArgument() {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_PNI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build())
.build();
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(request));
assertEquals(Status.INVALID_ARGUMENT.getCode(), statusRuntimeException.getStatus().getCode());
}
}

View File

@ -21,6 +21,8 @@ import org.signal.chat.common.IdentityType;
import org.signal.chat.common.ServiceIdentifier;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.SetProfileRequest.AvatarChange;
import org.signal.chat.profile.ProfileGrpc;
import org.signal.chat.profile.SetProfileRequest;
@ -55,10 +57,12 @@ import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.ProfileHelper;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import reactor.core.publisher.Mono;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import javax.annotation.Nullable;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
@ -140,6 +144,7 @@ public class ProfileGrpcServiceTest {
PhoneNumberUtil.PhoneNumberFormat.E164);
final Metadata metadata = new Metadata();
metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us");
metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3");
profileBlockingStub = ProfileGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel())
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
@ -472,4 +477,121 @@ public class ProfileGrpcServiceTest {
verifyNoInteractions(accountsManager);
}
@ParameterizedTest
@MethodSource
void getVersionedProfile(final String requestVersion, @Nullable final String accountVersion, final boolean expectResponseHasPaymentAddress) {
final VersionedProfile profile = mock(VersionedProfile.class);
final byte[] name = ProfileHelper.generateRandomByteArray(81);
final byte[] emoji = ProfileHelper.generateRandomByteArray(60);
final byte[] about = ProfileHelper.generateRandomByteArray(156);
final byte[] paymentAddress = ProfileHelper.generateRandomByteArray(582);
final String avatar = "profiles/" + ProfileHelper.generateRandomBase64FromByteArray(16);
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion(requestVersion)
.build();
when(profile.name()).thenReturn(name);
when(profile.about()).thenReturn(about);
when(profile.aboutEmoji()).thenReturn(emoji);
when(profile.avatar()).thenReturn(avatar);
when(profile.paymentAddress()).thenReturn(paymentAddress);
when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile)));
final GetVersionedProfileResponse response = profileBlockingStub.getVersionedProfile(request);
final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder()
.setName(ByteString.copyFrom(name))
.setAbout(ByteString.copyFrom(about))
.setAboutEmoji(ByteString.copyFrom(emoji))
.setAvatar(avatar);
if (expectResponseHasPaymentAddress) {
expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress));
}
assertEquals(expectedResponseBuilder.build(), response);
}
private static Stream<Arguments> getVersionedProfile() {
return Stream.of(
Arguments.of("version1", "version1", true),
Arguments.of("version1", null, true),
Arguments.of("version1", "version2", false)
);
}
@ParameterizedTest
@MethodSource
void getVersionedProfileAccountOrProfileNotFound(final boolean missingAccount, final boolean missingProfile) {
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("versionWithNoProfile")
.build();
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(missingAccount ? Optional.empty() : Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(missingProfile ? Optional.empty() : Optional.of(profile)));
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.NOT_FOUND.getCode(), statusRuntimeException.getStatus().getCode());
}
private static Stream<Arguments> getVersionedProfileAccountOrProfileNotFound() {
return Stream.of(
Arguments.of(true, false),
Arguments.of(false, true)
);
}
@Test
void getVersionedProfileRatelimited() {
final Duration retryAfterDuration = Duration.ofMinutes(7);
when(rateLimiter.validateReactive(any(UUID.class)))
.thenReturn(Mono.error(new RateLimitExceededException(retryAfterDuration, false)));
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build();
final StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.Code.RESOURCE_EXHAUSTED, exception.getStatus().getCode());
assertNotNull(exception.getTrailers());
assertEquals(retryAfterDuration, exception.getTrailers().get(RateLimitUtil.RETRY_AFTER_DURATION_KEY));
verifyNoInteractions(accountsManager);
verifyNoInteractions(profilesManager);
}
@Test
void getVersionedProfilePniInvalidArgument() {
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_PNI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build();
final StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.INVALID_ARGUMENT.getCode(), exception.getStatus().getCode());
}
}