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