Profile gRPC: Define `getVersionedProfile` endpoint
This commit is contained in:
		
							parent
							
								
									5afc058f90
								
							
						
					
					
						commit
						dd18fcaea2
					
				|  | @ -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() | ||||
|  |  | |||
|  | @ -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())); | ||||
|  |  | |||
|  | @ -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<>(); | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -162,7 +162,7 @@ message GetVersionedProfileRequest { | |||
|   /** | ||||
|    * The profile version to retrieve. | ||||
|    */ | ||||
|   bytes version = 2; | ||||
|   string version = 2; | ||||
| } | ||||
| 
 | ||||
| message GetVersionedProfileAnonymousRequest { | ||||
|  |  | |||
|  | @ -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()); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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()); | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Katherine Yen
						Katherine Yen