Add delete sync capability

This commit is contained in:
Katherine 2024-06-12 13:54:06 -04:00 committed by GitHub
parent 155450380e
commit 0414da8c32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 144 additions and 40 deletions

View File

@ -126,7 +126,7 @@ public class TestUser {
} }
public AccountAttributes accountAttributes() { public AccountAttributes accountAttributes() {
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false)) return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false, false))
.withUnidentifiedAccessKey(unidentifiedAccessKey) .withUnidentifiedAccessKey(unidentifiedAccessKey)
.withRecoveryPassword(registrationPassword); .withRecoveryPassword(registrationPassword);
} }

View File

@ -232,6 +232,8 @@ public class DeviceController {
if (capabilities == null) { if (capabilities == null) {
throw new WebApplicationException(Response.status(422, "Missing device capabilities").build()); throw new WebApplicationException(Response.status(422, "Missing device capabilities").build());
} else if (isCapabilityDowngrade(account, capabilities)) {
throw new WebApplicationException(Response.status(409).build());
} }
final String signalAgent; final String signalAgent;
@ -387,6 +389,10 @@ public class DeviceController {
return Optional.of(aci); return Optional.of(aci);
} }
private static boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities) {
return account.isDeleteSyncSupported() && !capabilities.deleteSync();
}
private static String getUsedTokenKey(final String token) { private static String getUsedTokenKey(final String token) {
return "usedToken::" + token; return "usedToken::" + token;
} }

View File

@ -447,7 +447,7 @@ public class ProfileController {
return new BaseProfileResponse(account.getIdentityKey(IdentityType.ACI), return new BaseProfileResponse(account.getIdentityKey(IdentityType.ACI),
account.getUnidentifiedAccessKey().map(UnidentifiedAccessChecksum::generateFor).orElse(null), account.getUnidentifiedAccessKey().map(UnidentifiedAccessChecksum::generateFor).orElse(null),
account.isUnrestrictedUnidentifiedAccess(), account.isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(), UserCapabilities.createForAccount(account),
profileBadgeConverter.convert( profileBadgeConverter.convert(
getAcceptableLanguagesForRequest(containerRequestContext), getAcceptableLanguagesForRequest(containerRequestContext),
account.getBadges(), account.getBadges(),
@ -459,7 +459,7 @@ public class ProfileController {
return new BaseProfileResponse(account.getIdentityKey(IdentityType.PNI), return new BaseProfileResponse(account.getIdentityKey(IdentityType.PNI),
null, null,
false, false,
new UserCapabilities(), UserCapabilities.createForAccount(account),
Collections.emptyList(), Collections.emptyList(),
new PniServiceIdentifier(account.getPhoneNumberIdentifier())); new PniServiceIdentifier(account.getPhoneNumberIdentifier()));
} }

View File

@ -5,11 +5,14 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import org.whispersystems.textsecuregcm.storage.Account;
public record UserCapabilities( public record UserCapabilities(
// TODO: Remove the paymentActivation capability entirely sometime soon after 2024-06-30 // TODO: Remove the paymentActivation capability entirely sometime soon after 2024-06-30
boolean paymentActivation) { boolean paymentActivation,
boolean deleteSync) {
public UserCapabilities() { public static UserCapabilities createForAccount(final Account account) {
this(true); return new UserCapabilities(true, account.isDeleteSyncSupported());
} }
} }

View File

@ -201,7 +201,8 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
d -> d.setCapabilities(new Device.DeviceCapabilities( d -> d.setCapabilities(new Device.DeviceCapabilities(
request.getStorage(), request.getStorage(),
request.getTransfer(), request.getTransfer(),
request.getPaymentActivation()))))) request.getPaymentActivation(),
request.getDeleteSync())))))
.thenReturn(SetCapabilitiesResponse.newBuilder().build()); .thenReturn(SetCapabilitiesResponse.newBuilder().build());
} }
} }

View File

@ -83,6 +83,7 @@ public class ProfileGrpcHelper {
static UserCapabilities buildUserCapabilities(final org.whispersystems.textsecuregcm.entities.UserCapabilities capabilities) { static UserCapabilities buildUserCapabilities(final org.whispersystems.textsecuregcm.entities.UserCapabilities capabilities) {
return UserCapabilities.newBuilder() return UserCapabilities.newBuilder()
.setPaymentActivation(capabilities.paymentActivation()) .setPaymentActivation(capabilities.paymentActivation())
.setDeleteSync(capabilities.deleteSync())
.build(); .build();
} }
@ -104,7 +105,7 @@ public class ProfileGrpcHelper {
final ProfileBadgeConverter profileBadgeConverter) { final ProfileBadgeConverter profileBadgeConverter) {
final GetUnversionedProfileResponse.Builder responseBuilder = GetUnversionedProfileResponse.newBuilder() final GetUnversionedProfileResponse.Builder responseBuilder = GetUnversionedProfileResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(targetIdentifier.identityType()).serialize())) .setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(targetIdentifier.identityType()).serialize()))
.setCapabilities(buildUserCapabilities(new org.whispersystems.textsecuregcm.entities.UserCapabilities())); .setCapabilities(buildUserCapabilities(org.whispersystems.textsecuregcm.entities.UserCapabilities.createForAccount(targetAccount)));
switch (targetIdentifier.identityType()) { switch (targetIdentifier.identityType()) {
case ACI -> { case ACI -> {

View File

@ -292,6 +292,10 @@ public class Account {
return allEnabledDevicesHaveCapability(DeviceCapabilities::paymentActivation); return allEnabledDevicesHaveCapability(DeviceCapabilities::paymentActivation);
} }
public boolean isDeleteSyncSupported() {
return allEnabledDevicesHaveCapability(DeviceCapabilities::deleteSync);
}
private boolean allEnabledDevicesHaveCapability(final Predicate<DeviceCapabilities> predicate) { private boolean allEnabledDevicesHaveCapability(final Predicate<DeviceCapabilities> predicate) {
requireNotStale(); requireNotStale();

View File

@ -248,6 +248,6 @@ public class Device {
return this.userAgent; return this.userAgent;
} }
public record DeviceCapabilities(boolean storage, boolean transfer, boolean paymentActivation) { public record DeviceCapabilities(boolean storage, boolean transfer, boolean paymentActivation, boolean deleteSync) {
} }
} }

View File

@ -148,6 +148,7 @@ message SetCapabilitiesRequest {
bool storage = 1; bool storage = 1;
bool transfer = 2; bool transfer = 2;
bool paymentActivation = 3; bool paymentActivation = 3;
bool deleteSync = 4;
} }
message SetCapabilitiesResponse {} message SetCapabilitiesResponse {}

View File

@ -322,6 +322,10 @@ message UserCapabilities {
* Whether all devices linked to the account support MobileCoin payments. * Whether all devices linked to the account support MobileCoin payments.
*/ */
bool payment_activation = 1; bool payment_activation = 1;
/**
* Whether all devices linked to the account support delete syncing
*/
bool delete_sync = 2;
} }
message Badge { message Badge {

View File

@ -218,7 +218,8 @@ class DeviceControllerTest {
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, true)); final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null,
null, true, new DeviceCapabilities(true, true, true, false));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
accountAttributes, accountAttributes,
@ -264,6 +265,58 @@ class DeviceControllerTest {
); );
} }
@ParameterizedTest
@MethodSource
void deviceDowngradeDeleteSync(final boolean accountSupportsDeleteSync, final boolean deviceSupportsDeleteSync, final int expectedStatus) {
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
when(accountsManager.addDevice(any(), any()))
.thenReturn(CompletableFuture.completedFuture(new Pair<>(mock(Account.class), mock(Device.class))));
final Device primaryDevice = mock(Device.class);
when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(primaryDevice));
final ECSignedPreKey aciSignedPreKey;
final ECSignedPreKey pniSignedPreKey;
final KEMSignedPreKey aciPqLastResortPreKey;
final KEMSignedPreKey pniPqLastResortPreKey;
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
when(account.isDeleteSyncSupported()).thenReturn(accountSupportsDeleteSync);
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, true, deviceSupportsDeleteSync)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
try (final Response response = resources.getJerseyTest()
.target("/v1/devices/link")
.request()
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(expectedStatus, response.getStatus());
}
}
private static List<Arguments> deviceDowngradeDeleteSync() {
return List.of(
Arguments.of(true, true, 200),
Arguments.of(true, false, 409),
Arguments.of(false, true, 200),
Arguments.of(false, false, 200));
}
@Test @Test
void linkDeviceAtomicBadCredentials() { void linkDeviceAtomicBadCredentials() {
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
@ -633,7 +686,7 @@ class DeviceControllerTest {
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
new AccountAttributes(false, registrationId, pniRegistrationId, null, null, true, new DeviceCapabilities(true, true, true)), new AccountAttributes(false, registrationId, pniRegistrationId, null, null, true, new DeviceCapabilities(true, true, true, false)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.of(new ApnRegistrationId("apn", null)), Optional.empty())); new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.of(new ApnRegistrationId("apn", null)), Optional.empty()));
try (final Response response = resources.getJerseyTest() try (final Response response = resources.getJerseyTest()
@ -692,7 +745,7 @@ class DeviceControllerTest {
@Test @Test
void putCapabilitiesSuccessTest() { void putCapabilitiesSuccessTest() {
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true); final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false);
final Response response = resources final Response response = resources
.getJerseyTest() .getJerseyTest()
.target("/v1/devices/capabilities") .target("/v1/devices/capabilities")

View File

@ -144,6 +144,7 @@ class ProfileControllerTest {
private DynamicPaymentsConfiguration dynamicPaymentsConfiguration; private DynamicPaymentsConfiguration dynamicPaymentsConfiguration;
private Account profileAccount; private Account profileAccount;
private Account capabilitiesAccount;
private static final ResourceExtension resources = ResourceExtension.builder() private static final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
@ -203,12 +204,11 @@ class ProfileControllerTest {
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH)); when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));
Account capabilitiesAccount = mock(Account.class); capabilitiesAccount = mock(Account.class);
when(capabilitiesAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID); when(capabilitiesAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY); when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY);
when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY); when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY);
when(capabilitiesAccount.isPaymentActivationSupported()).thenReturn(false);
when(capabilitiesAccount.isEnabled()).thenReturn(true); when(capabilitiesAccount.isEnabled()).thenReturn(true);
when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty()); when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty());
@ -439,14 +439,17 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(401); assertThat(response.getStatus()).isEqualTo(401);
} }
@Test @ParameterizedTest
void testProfileCapabilities() { @ValueSource(booleans = {true, false})
void testProfileCapabilities(final boolean isDeleteSyncSupported) {
when(capabilitiesAccount.isDeleteSyncSupported()).thenReturn(isDeleteSyncSupported);
final BaseProfileResponse profile = resources.getJerseyTest() final BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID) .target("/v1/profile/" + AuthHelper.VALID_UUID)
.request() .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(BaseProfileResponse.class); .get(BaseProfileResponse.class);
assertEquals(isDeleteSyncSupported, profile.getCapabilities().deleteSync());
assertThat(profile.getCapabilities().paymentActivation()).isTrue(); assertThat(profile.getCapabilities().paymentActivation()).isTrue();
} }

View File

@ -477,10 +477,10 @@ class RegistrationControllerTest {
} }
final AccountAttributes fetchesMessagesAccountAttributes = final AccountAttributes fetchesMessagesAccountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
final AccountAttributes pushAccountAttributes = final AccountAttributes pushAccountAttributes =
new AccountAttributes(false, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); new AccountAttributes(false, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
return Stream.of( return Stream.of(
// "Fetches messages" is true, but an APNs token is provided // "Fetches messages" is true, but an APNs token is provided
@ -566,7 +566,7 @@ class RegistrationControllerTest {
} }
final AccountAttributes accountAttributes = final AccountAttributes accountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
return Stream.of( return Stream.of(
// Signed PNI EC pre-key is missing // Signed PNI EC pre-key is missing
@ -736,13 +736,13 @@ class RegistrationControllerTest {
final int registrationId = 1; final int registrationId = 1;
final int pniRegistrationId = 2; final int pniRegistrationId = 2;
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false); final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false, false);
final AccountAttributes fetchesMessagesAccountAttributes = final AccountAttributes fetchesMessagesAccountAttributes =
new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
final AccountAttributes pushAccountAttributes = final AccountAttributes pushAccountAttributes =
new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
final String apnsToken = "apns-token"; final String apnsToken = "apns-token";
final String apnsVoipToken = "apns-voip-token"; final String apnsVoipToken = "apns-voip-token";
@ -857,7 +857,7 @@ class RegistrationControllerTest {
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
final AccountAttributes accountAttributes = new AccountAttributes(true, registrationId, pniRegistrationId, "name".getBytes(StandardCharsets.UTF_8), "reglock", final AccountAttributes accountAttributes = new AccountAttributes(true, registrationId, pniRegistrationId, "name".getBytes(StandardCharsets.UTF_8), "reglock",
true, new Device.DeviceCapabilities(true, true, true)); true, new Device.DeviceCapabilities(true, true, true, false));
final RegistrationRequest request = new RegistrationRequest( final RegistrationRequest request = new RegistrationRequest(
Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)),

View File

@ -393,7 +393,8 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
@CartesianTest.Values(bytes = {Device.PRIMARY_ID, Device.PRIMARY_ID + 1}) final byte deviceId, @CartesianTest.Values(bytes = {Device.PRIMARY_ID, Device.PRIMARY_ID + 1}) final byte deviceId,
@CartesianTest.Values(booleans = {true, false}) final boolean storage, @CartesianTest.Values(booleans = {true, false}) final boolean storage,
@CartesianTest.Values(booleans = {true, false}) final boolean transfer, @CartesianTest.Values(booleans = {true, false}) final boolean transfer,
@CartesianTest.Values(booleans = {true, false}) final boolean paymentActivation) { @CartesianTest.Values(booleans = {true, false}) final boolean paymentActivation,
@CartesianTest.Values(booleans = {true, false}) final boolean deleteSync) {
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId); mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId);
@ -404,12 +405,14 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
.setStorage(storage) .setStorage(storage)
.setTransfer(transfer) .setTransfer(transfer)
.setPaymentActivation(paymentActivation) .setPaymentActivation(paymentActivation)
.setDeleteSync(deleteSync)
.build()); .build());
final Device.DeviceCapabilities expectedCapabilities = new Device.DeviceCapabilities( final Device.DeviceCapabilities expectedCapabilities = new Device.DeviceCapabilities(
storage, storage,
transfer, transfer,
paymentActivation); paymentActivation,
deleteSync);
verify(device).setCapabilities(expectedCapabilities); verify(device).setCapabilities(expectedCapabilities);
} }

View File

@ -162,7 +162,7 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
.setIdentityKey(ByteString.copyFrom(identityKey.serialize())) .setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum)) .setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum))
.setUnrestrictedUnidentifiedAccess(false) .setUnrestrictedUnidentifiedAccess(false)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities())) .setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges)) .addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build(); .build();
@ -214,7 +214,7 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<ProfileA
final GetUnversionedProfileResponse expectedResponse = GetUnversionedProfileResponse.newBuilder() final GetUnversionedProfileResponse expectedResponse = GetUnversionedProfileResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize())) .setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnrestrictedUnidentifiedAccess(false) .setUnrestrictedUnidentifiedAccess(false)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities())) .setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges)) .addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build(); .build();

View File

@ -436,7 +436,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
.setIdentityKey(ByteString.copyFrom(identityKey.serialize())) .setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum)) .setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum))
.setUnrestrictedUnidentifiedAccess(true) .setUnrestrictedUnidentifiedAccess(true)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities())) .setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges)) .addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build(); .build();

View File

@ -184,6 +184,7 @@ public class AccountCreationDeletionIntegrationTest {
final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16); final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16);
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities( final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean()); ThreadLocalRandom.current().nextBoolean());
@ -296,14 +297,14 @@ public class AccountCreationDeletionIntegrationTest {
final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniKeyPair); final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniKeyPair);
final Account originalAccount = accountsManager.create(number, final Account originalAccount = accountsManager.create(number,
new AccountAttributes(true, 1, 1, "name".getBytes(StandardCharsets.UTF_8), "registration-lock", false, new Device.DeviceCapabilities(false, false, false)), new AccountAttributes(true, 1, 1, "name".getBytes(StandardCharsets.UTF_8), "registration-lock", false, new Device.DeviceCapabilities(false, false, false, false)),
Collections.emptyList(), Collections.emptyList(),
new IdentityKey(aciKeyPair.getPublicKey()), new IdentityKey(aciKeyPair.getPublicKey()),
new IdentityKey(pniKeyPair.getPublicKey()), new IdentityKey(pniKeyPair.getPublicKey()),
new DeviceSpec(null, new DeviceSpec(null,
"password?", "password?",
"OWI", "OWI",
new Device.DeviceCapabilities(false, false, false), new Device.DeviceCapabilities(false, false, false, false),
1, 1,
2, 2,
true, true,
@ -325,6 +326,7 @@ public class AccountCreationDeletionIntegrationTest {
final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16); final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16);
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities( final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean()); ThreadLocalRandom.current().nextBoolean());
@ -414,6 +416,7 @@ public class AccountCreationDeletionIntegrationTest {
final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16); final String registrationLockSecret = RandomStringUtils.randomAlphanumeric(16);
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities( final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(), ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean()); ThreadLocalRandom.current().nextBoolean());

View File

@ -49,6 +49,8 @@ class AccountTest {
private final Device paymentActivationCapableDevice = mock(Device.class); private final Device paymentActivationCapableDevice = mock(Device.class);
private final Device paymentActivationIncapableDevice = mock(Device.class); private final Device paymentActivationIncapableDevice = mock(Device.class);
private final Device paymentActivationIncapableExpiredDevice = mock(Device.class); private final Device paymentActivationIncapableExpiredDevice = mock(Device.class);
private final Device deleteSyncCapableDevice = mock(Device.class);
private final Device deleteSyncIncapableDevice = mock(Device.class);
@BeforeEach @BeforeEach
void setup() { void setup() {
@ -74,15 +76,24 @@ class AccountTest {
when(oldSecondaryDevice.getId()).thenReturn(deviceId2); when(oldSecondaryDevice.getId()).thenReturn(deviceId2);
when(paymentActivationCapableDevice.getCapabilities()).thenReturn( when(paymentActivationCapableDevice.getCapabilities()).thenReturn(
new DeviceCapabilities(true, true, true)); new DeviceCapabilities(true, true, true, false));
when(paymentActivationCapableDevice.hasMessageDeliveryChannel()).thenReturn(true); when(paymentActivationCapableDevice.hasMessageDeliveryChannel()).thenReturn(true);
when(paymentActivationIncapableDevice.getCapabilities()).thenReturn( when(paymentActivationIncapableDevice.getCapabilities()).thenReturn(
new DeviceCapabilities(true, true, false)); new DeviceCapabilities(true, true, false, false));
when(paymentActivationIncapableDevice.hasMessageDeliveryChannel()).thenReturn(true); when(paymentActivationIncapableDevice.hasMessageDeliveryChannel()).thenReturn(true);
when(paymentActivationIncapableExpiredDevice.getCapabilities()).thenReturn( when(paymentActivationIncapableExpiredDevice.getCapabilities()).thenReturn(
new DeviceCapabilities(true, true, false)); new DeviceCapabilities(true, true, false, false));
when(paymentActivationIncapableExpiredDevice.hasMessageDeliveryChannel()).thenReturn(false); when(paymentActivationIncapableExpiredDevice.hasMessageDeliveryChannel()).thenReturn(false);
when(deleteSyncCapableDevice.getCapabilities()).thenReturn(
new DeviceCapabilities(true, true, true, true)
);
when(deleteSyncCapableDevice.hasMessageDeliveryChannel()).thenReturn(true);
when(deleteSyncIncapableDevice.getCapabilities()).thenReturn(
new DeviceCapabilities(true, true, true, false)
);
when(deleteSyncIncapableDevice.hasMessageDeliveryChannel()).thenReturn(true);
} }
@Test @Test
@ -184,6 +195,16 @@ class AccountTest {
"1234".getBytes(StandardCharsets.UTF_8)).isPaymentActivationSupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isPaymentActivationSupported()).isTrue();
} }
@Test
void isDeleteSyncSupported() {
assertTrue(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
List.of(deleteSyncCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isDeleteSyncSupported());
assertFalse(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
List.of(deleteSyncCapableDevice, deleteSyncIncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isDeleteSyncSupported());
}
@Test @Test
void stale() { void stale() {
final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Collections.emptyList(), final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Collections.emptyList(),

View File

@ -193,7 +193,7 @@ class AccountsManagerChangeNumberIntegrationTest {
final int rotatedPniRegistrationId = 17; final int rotatedPniRegistrationId = 17;
final ECKeyPair rotatedPniIdentityKeyPair = Curve.generateKeyPair(); final ECKeyPair rotatedPniIdentityKeyPair = Curve.generateKeyPair();
final ECSignedPreKey rotatedSignedPreKey = KeysHelper.signedECPreKey(1L, rotatedPniIdentityKeyPair); final ECSignedPreKey rotatedSignedPreKey = KeysHelper.signedECPreKey(1L, rotatedPniIdentityKeyPair);
final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, rotatedPniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false)); final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, rotatedPniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
final Account account = AccountsHelper.createAccount(accountsManager, originalNumber, accountAttributes); final Account account = AccountsHelper.createAccount(accountsManager, originalNumber, accountAttributes);
keysManager.storeEcSignedPreKeys(account.getIdentifier(IdentityType.ACI), keysManager.storeEcSignedPreKeys(account.getIdentifier(IdentityType.ACI),

View File

@ -156,7 +156,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
null, null,
"password", "password",
null, null,
new Device.DeviceCapabilities(false, false, false), new Device.DeviceCapabilities(false, false, false, false),
1, 1,
2, 2,
true, true,

View File

@ -876,7 +876,7 @@ class AccountsManagerTest {
@ValueSource(booleans = {true, false}) @ValueSource(booleans = {true, false})
void testCreateWithStorageCapability(final boolean hasStorage) throws InterruptedException { void testCreateWithStorageCapability(final boolean hasStorage) throws InterruptedException {
final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null, final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null,
true, new DeviceCapabilities(hasStorage, false, false)); true, new DeviceCapabilities(hasStorage, false, false, false));
final Account account = createAccount("+18005550123", attributes); final Account account = createAccount("+18005550123", attributes);
@ -901,7 +901,7 @@ class AccountsManagerTest {
final byte[] deviceNameCiphertext = "device-name".getBytes(StandardCharsets.UTF_8); final byte[] deviceNameCiphertext = "device-name".getBytes(StandardCharsets.UTF_8);
final String password = "password"; final String password = "password";
final String signalAgent = "OWT"; final String signalAgent = "OWT";
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true); final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false);
final int aciRegistrationId = 17; final int aciRegistrationId = 17;
final int pniRegistrationId = 19; final int pniRegistrationId = 19;
final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciKeyPair); final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciKeyPair);

View File

@ -172,7 +172,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8), "device-name".getBytes(StandardCharsets.UTF_8),
"password", "password",
"OWT", "OWT",
new Device.DeviceCapabilities(true, true, true), new Device.DeviceCapabilities(true, true, true, false),
1, 1,
2, 2,
true, true,
@ -215,7 +215,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8), "device-name".getBytes(StandardCharsets.UTF_8),
"password", "password",
"OWT", "OWT",
new Device.DeviceCapabilities(true, true, true), new Device.DeviceCapabilities(true, true, true, false),
1, 1,
2, 2,
true, true,
@ -268,7 +268,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8), "device-name".getBytes(StandardCharsets.UTF_8),
"password", "password",
"OWT", "OWT",
new Device.DeviceCapabilities(true, true, true), new Device.DeviceCapabilities(true, true, true, false),
1, 1,
2, 2,
true, true,

View File

@ -149,6 +149,7 @@ public class AccountsHelper {
case "isDiscoverableByPhoneNumber" -> when(updatedAccount.isDiscoverableByPhoneNumber()).thenAnswer(stubbing); case "isDiscoverableByPhoneNumber" -> when(updatedAccount.isDiscoverableByPhoneNumber()).thenAnswer(stubbing);
case "getNextDeviceId" -> when(updatedAccount.getNextDeviceId()).thenAnswer(stubbing); case "getNextDeviceId" -> when(updatedAccount.getNextDeviceId()).thenAnswer(stubbing);
case "isPaymentActivationSupported" -> when(updatedAccount.isPaymentActivationSupported()).thenAnswer(stubbing); case "isPaymentActivationSupported" -> when(updatedAccount.isPaymentActivationSupported()).thenAnswer(stubbing);
case "isDeleteSyncSupported" -> when(updatedAccount.isDeleteSyncSupported()).thenAnswer(stubbing);
case "hasEnabledLinkedDevice" -> when(updatedAccount.hasEnabledLinkedDevice()).thenAnswer(stubbing); case "hasEnabledLinkedDevice" -> when(updatedAccount.hasEnabledLinkedDevice()).thenAnswer(stubbing);
case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing); case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing);
case "getIdentityKey" -> case "getIdentityKey" ->