Allow, but do not require, message delivery to devices without active delivery channels

This commit is contained in:
Jon Chambers
2024-06-25 09:53:31 -04:00
committed by GitHub
parent f5ce34fb69
commit d306cafbcc
24 changed files with 189 additions and 281 deletions

View File

@@ -161,7 +161,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
when(device.getId()).thenReturn(deviceId);
when(device.hasMessageDeliveryChannel()).thenReturn(true);
when(device.getAuthTokenHash()).thenReturn(credentials);
@@ -191,7 +190,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
when(device.getId()).thenReturn(deviceId);
when(device.hasMessageDeliveryChannel()).thenReturn(true);
when(device.getAuthTokenHash()).thenReturn(credentials);
@@ -224,7 +222,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(authenticatedDevice));
when(account.isEnabled()).thenReturn(accountEnabled);
when(authenticatedDevice.getId()).thenReturn(deviceId);
when(authenticatedDevice.hasMessageDeliveryChannel()).thenReturn(deviceEnabled);
when(authenticatedDevice.getAuthTokenHash()).thenReturn(credentials);
@@ -260,7 +257,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
when(device.getId()).thenReturn(deviceId);
when(device.hasMessageDeliveryChannel()).thenReturn(true);
when(device.getAuthTokenHash()).thenReturn(credentials);
@@ -297,7 +293,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
when(device.getId()).thenReturn(deviceId);
when(device.hasMessageDeliveryChannel()).thenReturn(true);
when(device.getAuthTokenHash()).thenReturn(credentials);
@@ -325,7 +320,6 @@ class AccountAuthenticatorTest {
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true);
when(device.getId()).thenReturn(deviceId);
when(device.hasMessageDeliveryChannel()).thenReturn(true);
when(device.getAuthTokenHash()).thenReturn(credentials);

View File

@@ -5,152 +5,141 @@
package org.whispersystems.textsecuregcm.auth;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import javax.ws.rs.WebApplicationException;
import org.junit.jupiter.api.Test;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
class OptionalAccessTest {
@Test
void testUnidentifiedMissingTarget() {
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@ParameterizedTest
@MethodSource
void verify(final Optional<Account> requestAccount,
final Optional<Anonymous> accessKey,
final Optional<Account> targetAccount,
final String deviceSelector,
final OptionalInt expectedStatusCode) {
expectedStatusCode.ifPresentOrElse(statusCode -> {
final WebApplicationException webApplicationException = assertThrows(WebApplicationException.class,
() -> OptionalAccess.verify(requestAccount, accessKey, targetAccount, deviceSelector));
assertEquals(statusCode, webApplicationException.getResponse().getStatus());
}, () -> assertDoesNotThrow(() -> OptionalAccess.verify(requestAccount, accessKey, targetAccount, deviceSelector)));
}
@Test
void testUnidentifiedMissingTargetDevice() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getDevice(eq((byte) 10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
private static List<Arguments> verify() {
final String unidentifiedAccessKey = RandomStringUtils.randomAlphanumeric(16);
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "10");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
final Anonymous correctUakHeader =
new Anonymous(Base64.getEncoder().encodeToString(unidentifiedAccessKey.getBytes()));
@Test
void testUnidentifiedBadTargetDevice() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getDevice(eq((byte) 10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
final Anonymous incorrectUakHeader =
new Anonymous(Base64.getEncoder().encodeToString((unidentifiedAccessKey + "-incorrect").getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "$$");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 422);
}
}
final Account targetAccount = mock(Account.class);
when(targetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(mock(Device.class)));
when(targetAccount.getUnidentifiedAccessKey())
.thenReturn(Optional.of(unidentifiedAccessKey.getBytes(StandardCharsets.UTF_8)));
final Account allowAllTargetAccount = mock(Account.class);
when(allowAllTargetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(mock(Device.class)));
when(allowAllTargetAccount.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
@Test
void testUnidentifiedBadCode() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
final Account noUakTargetAccount = mock(Account.class);
when(noUakTargetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(mock(Device.class)));
when(noUakTargetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.empty());
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("5678".getBytes()))), Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
final Account inactiveTargetAccount = mock(Account.class);
when(inactiveTargetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(mock(Device.class)));
when(inactiveTargetAccount.getUnidentifiedAccessKey())
.thenReturn(Optional.of(unidentifiedAccessKey.getBytes(StandardCharsets.UTF_8)));
@Test
void testIdentifiedMissingTarget() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
return List.of(
// Unidentified caller; correct UAK
Arguments.of(Optional.empty(),
Optional.of(correctUakHeader),
Optional.of(targetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.empty()),
try {
OptionalAccess.verify(Optional.of(account), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 404);
}
}
// Identified caller; no UAK needed
Arguments.of(Optional.of(mock(Account.class)),
Optional.empty(),
Optional.of(targetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.empty()),
@Test
void testUnsolicitedBadTarget() {
Account account = mock(Account.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.isEnabled()).thenReturn(true);
// Unidentified caller; target account not found
Arguments.of(Optional.empty(),
Optional.empty(),
Optional.empty(),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.of(401)),
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
// Identified caller; target account not found
Arguments.of(Optional.of(mock(Account.class)),
Optional.empty(),
Optional.empty(),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.of(404)),
@Test
void testUnsolicitedGoodTarget() {
Account account = mock(Account.class);
Anonymous random = mock(Anonymous.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
when(account.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(random), Optional.of(account));
}
// Unidentified caller; target account found, but target device not found
Arguments.of(Optional.empty(),
Optional.of(correctUakHeader),
Optional.of(targetAccount),
String.valueOf(Device.PRIMARY_ID + 1),
OptionalInt.of(401)),
@Test
void testUnidentifiedGoodTarget() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account));
}
// Unidentified caller; target account found, but incorrect UAK provided
Arguments.of(Optional.empty(),
Optional.of(incorrectUakHeader),
Optional.of(targetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.of(401)),
@Test
void testUnidentifiedTargetMissingAccessKey() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.empty());
when(account.isEnabled()).thenReturn(true);
try {
OptionalAccess.verify(
Optional.empty(),
Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))),
Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
// Unidentified caller; target account found, but has no UAK
Arguments.of(Optional.empty(),
Optional.of(correctUakHeader),
Optional.of(noUakTargetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.of(401)),
@Test
void testUnidentifiedInactive() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isEnabled()).thenReturn(false);
// Unidentified caller; target account found, allows unrestricted unidentified access
Arguments.of(Optional.empty(),
Optional.of(incorrectUakHeader),
Optional.of(allowAllTargetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.empty()),
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account));
throw new AssertionError();
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
// Unidentified caller; target account found, but inactive
Arguments.of(Optional.empty(),
Optional.of(correctUakHeader),
Optional.of(inactiveTargetAccount),
OptionalAccess.ALL_DEVICES_SELECTOR,
OptionalInt.empty()),
@Test
void testIdentifiedGoodTarget() {
Account source = mock(Account.class);
Account target = mock(Account.class);
when(target.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.of(source), Optional.empty(), Optional.of(target));
// Malformed device ID
Arguments.of(Optional.empty(),
Optional.of(correctUakHeader),
Optional.of(targetAccount),
"not a valid identifier",
OptionalInt.of(422))
);
}
}

View File

@@ -143,7 +143,6 @@ class DeviceControllerTest {
when(account.getNumber()).thenReturn(AuthHelper.VALID_NUMBER);
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(account.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI);
when(account.isEnabled()).thenReturn(false);
when(account.isPaymentActivationSupported()).thenReturn(false);
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));

View File

@@ -240,7 +240,6 @@ class KeysControllerTest {
when(existsAccount.getDevice(sampleDevice4Id)).thenReturn(Optional.of(sampleDevice4));
when(existsAccount.getDevice((byte) 22)).thenReturn(Optional.empty());
when(existsAccount.getDevices()).thenReturn(allDevices);
when(existsAccount.isEnabled()).thenReturn(true);
when(existsAccount.getIdentityKey(IdentityType.ACI)).thenReturn(IDENTITY_KEY);
when(existsAccount.getIdentityKey(IdentityType.PNI)).thenReturn(PNI_IDENTITY_KEY);
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
@@ -676,7 +675,7 @@ class KeysControllerTest {
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponse.class);
assertThat(results.getDevicesCount()).isEqualTo(3);
assertThat(results.getDevicesCount()).isEqualTo(4);
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI));
ECSignedPreKey signedPreKey = results.getDevice(SAMPLE_DEVICE_ID).getSignedPreKey();
@@ -711,12 +710,15 @@ class KeysControllerTest {
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verifyNoMoreInteractions(KEYS);
}
@@ -746,7 +748,7 @@ class KeysControllerTest {
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponse.class);
assertThat(results.getDevicesCount()).isEqualTo(3);
assertThat(results.getDevicesCount()).isEqualTo(4);
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI));
ECSignedPreKey signedPreKey = results.getDevice(SAMPLE_DEVICE_ID).getSignedPreKey();
@@ -789,10 +791,13 @@ class KeysControllerTest {
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).takeEC(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verify(KEYS).takePQ(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID2);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID3);
verify(KEYS).getEcSignedPreKey(EXISTS_UUID, SAMPLE_DEVICE_ID4);
verifyNoMoreInteractions(KEYS);
}

View File

@@ -1584,18 +1584,20 @@ class MessageControllerTest {
void sendMultiRecipientMessageMismatchedDevices(final ServiceIdentifier serviceIdentifier)
throws JsonProcessingException {
final byte extraDeviceId = MULTI_DEVICE_ID3 + 1;
final List<Recipient> recipients = List.of(
new Recipient(serviceIdentifier, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]),
new Recipient(serviceIdentifier, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]),
new Recipient(serviceIdentifier, MULTI_DEVICE_ID3, MULTI_DEVICE_REG_ID3, new byte[48]));
new Recipient(serviceIdentifier, MULTI_DEVICE_ID3, MULTI_DEVICE_REG_ID3, new byte[48]),
new Recipient(serviceIdentifier, extraDeviceId, 1234, new byte[48]));
// initialize our binary payload and create an input stream
byte[] buffer = new byte[2048];
// InputStream stream = initializeMultiPayload(recipientUUID, buffer);
InputStream stream = initializeMultiPayload(recipients, buffer, true);
final byte[] buffer = new byte[2048];
final InputStream stream = initializeMultiPayload(recipients, buffer, true);
// set up the entity to use in our PUT request
Entity<InputStream> entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE);
final Entity<InputStream> entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE);
// start building the request
final Invocation.Builder invocationBuilder = resources
@@ -1619,7 +1621,7 @@ class MessageControllerTest {
.constructCollectionType(List.class, AccountMismatchedDevices.class));
assertEquals(List.of(new AccountMismatchedDevices(serviceIdentifier,
new MismatchedDevices(Collections.emptyList(), List.of(MULTI_DEVICE_ID3)))),
new MismatchedDevices(Collections.emptyList(), List.of(extraDeviceId)))),
mismatchedDevices);
}
}

View File

@@ -199,7 +199,6 @@ class ProfileControllerTest {
when(profileAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY);
when(profileAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID_TWO);
when(profileAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI_TWO);
when(profileAccount.isEnabled()).thenReturn(true);
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty());
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));
@@ -209,7 +208,6 @@ class ProfileControllerTest {
when(capabilitiesAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY);
when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY);
when(capabilitiesAccount.isEnabled()).thenReturn(true);
when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty());
@@ -1012,7 +1010,6 @@ class ProfileControllerTest {
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(account.getCurrentProfileVersion()).thenReturn(Optional.of(versionHex("version")));
when(account.isEnabled()).thenReturn(true);
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
when(profilesManager.get(any(), any())).thenReturn(Optional.empty());
@@ -1168,7 +1165,6 @@ class ProfileControllerTest {
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(account.getCurrentProfileVersion()).thenReturn(Optional.of(version));
when(account.isEnabled()).thenReturn(true);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));
final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION)
@@ -1234,7 +1230,6 @@ class ProfileControllerTest {
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(account.isEnabled()).thenReturn(true);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));
when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(Optional.of(account));

View File

@@ -9,8 +9,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
import com.google.protobuf.ByteString;
@@ -26,7 +26,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

View File

@@ -96,32 +96,6 @@ class AccountTest {
}
@Test
void testIsEnabled() {
final Device enabledPrimaryDevice = mock(Device.class);
final Device enabledLinkedDevice = mock(Device.class);
final Device disabledPrimaryDevice = mock(Device.class);
final Device disabledLinkedDevice = mock(Device.class);
when(enabledPrimaryDevice.hasMessageDeliveryChannel()).thenReturn(true);
when(enabledLinkedDevice.hasMessageDeliveryChannel()).thenReturn(true);
when(disabledPrimaryDevice.hasMessageDeliveryChannel()).thenReturn(false);
when(disabledLinkedDevice.hasMessageDeliveryChannel()).thenReturn(false);
when(enabledPrimaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
final byte deviceId2 = 2;
when(enabledLinkedDevice.getId()).thenReturn(deviceId2);
when(disabledPrimaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
when(disabledLinkedDevice.getId()).thenReturn(deviceId2);
assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledPrimaryDevice)).isEnabled());
assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledPrimaryDevice, enabledLinkedDevice)).isEnabled());
assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledPrimaryDevice, disabledLinkedDevice)).isEnabled());
assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledPrimaryDevice)).isEnabled());
assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledPrimaryDevice, enabledLinkedDevice)).isEnabled());
assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledPrimaryDevice, disabledLinkedDevice)).isEnabled());
}
@Test
void testIsTransferSupported() {
final Device transferCapablePrimaryDevice = mock(Device.class);
@@ -308,49 +282,4 @@ class AccountTest {
final JsonFilter jsonFilterAnnotation = (JsonFilter) maybeJsonFilterAnnotation.get();
assertEquals(Account.class.getSimpleName(), jsonFilterAnnotation.value());
}
@ParameterizedTest
@MethodSource
public void testHasEnabledLinkedDevice(final Account account, final boolean expect) {
assertEquals(expect, account.hasEnabledLinkedDevice());
}
static Stream<Arguments> testHasEnabledLinkedDevice() {
final Device enabledPrimary = mock(Device.class);
when(enabledPrimary.hasMessageDeliveryChannel()).thenReturn(true);
when(enabledPrimary.getId()).thenReturn(Device.PRIMARY_ID);
final Device disabledPrimary = mock(Device.class);
when(disabledPrimary.getId()).thenReturn(Device.PRIMARY_ID);
final byte linked1DeviceId = Device.PRIMARY_ID + 1;
final Device enabledLinked1 = mock(Device.class);
when(enabledLinked1.hasMessageDeliveryChannel()).thenReturn(true);
when(enabledLinked1.getId()).thenReturn(linked1DeviceId);
final Device disabledLinked1 = mock(Device.class);
when(disabledLinked1.getId()).thenReturn(linked1DeviceId);
final byte linked2DeviceId = Device.PRIMARY_ID + 2;
final Device enabledLinked2 = mock(Device.class);
when(enabledLinked2.hasMessageDeliveryChannel()).thenReturn(true);
when(enabledLinked2.getId()).thenReturn(linked2DeviceId);
final Device disabledLinked2 = mock(Device.class);
when(disabledLinked2.getId()).thenReturn(linked2DeviceId);
return Stream.of(
Arguments.of(AccountsHelper.generateTestAccount("+14155550123", List.of(enabledPrimary)), false),
Arguments.of(AccountsHelper.generateTestAccount("+14155550123", List.of(enabledPrimary, disabledLinked1)),
false),
Arguments.of(AccountsHelper.generateTestAccount("+14155550123",
List.of(enabledPrimary, disabledLinked1, disabledLinked2)), false),
Arguments.of(AccountsHelper.generateTestAccount("+14155550123",
List.of(enabledPrimary, enabledLinked1, disabledLinked2)), true),
Arguments.of(AccountsHelper.generateTestAccount("+14155550123",
List.of(enabledPrimary, disabledLinked1, enabledLinked2)), true),
Arguments.of(AccountsHelper.generateTestAccount("+14155550123",
List.of(disabledLinked2, enabledLinked1, enabledLinked2)), true)
);
}
}

View File

@@ -1066,11 +1066,13 @@ class AccountsManagerTest {
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
final Map<Byte, ECSignedPreKey> newSignedKeys = Map.of(
Device.PRIMARY_ID, KeysHelper.signedECPreKey(1, identityKeyPair),
deviceId2, KeysHelper.signedECPreKey(2, identityKeyPair));
deviceId2, KeysHelper.signedECPreKey(2, identityKeyPair),
deviceId3, KeysHelper.signedECPreKey(3, identityKeyPair));
final Map<Byte, KEMSignedPreKey> newSignedPqKeys = Map.of(
Device.PRIMARY_ID, KeysHelper.signedKEMPreKey(3, identityKeyPair),
deviceId2, KeysHelper.signedKEMPreKey(4, identityKeyPair));
final Map<Byte, Integer> newRegistrationIds = Map.of(Device.PRIMARY_ID, 201, deviceId2, 202);
Device.PRIMARY_ID, KeysHelper.signedKEMPreKey(4, identityKeyPair),
deviceId2, KeysHelper.signedKEMPreKey(5, identityKeyPair),
deviceId3, KeysHelper.signedKEMPreKey(6, identityKeyPair));
final Map<Byte, Integer> newRegistrationIds = Map.of(Device.PRIMARY_ID, 201, deviceId2, 202, deviceId3, 203);
final Account existingAccount = AccountsHelper.generateTestAccount(targetNumber, existingAccountUuid, targetPni, new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.getByE164(targetNumber)).thenReturn(Optional.of(existingAccount));
@@ -1097,7 +1099,9 @@ class AccountsManagerTest {
verify(keysManager).getPqEnabledDevices(uuid);
verify(keysManager).buildWriteItemForEcSignedPreKey(eq(newPni), eq(Device.PRIMARY_ID), any());
verify(keysManager).buildWriteItemForEcSignedPreKey(eq(newPni), eq(deviceId2), any());
verify(keysManager).buildWriteItemForEcSignedPreKey(eq(newPni), eq(deviceId3), any());
verify(keysManager).buildWriteItemForLastResortKey(eq(newPni), eq(Device.PRIMARY_ID), any());
verify(keysManager).buildWriteItemForLastResortKey(eq(newPni), eq(deviceId3), any());
verifyNoMoreInteractions(keysManager);
}

View File

@@ -315,7 +315,7 @@ class AccountsTest {
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.jsonMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.isDiscoverableByPhoneNumber())))
.build())
.build();

View File

@@ -145,12 +145,10 @@ public class AccountsHelper {
case "getDevices" -> when(updatedAccount.getDevices()).thenAnswer(stubbing);
case "getDevice" -> when(updatedAccount.getDevice(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing);
case "getPrimaryDevice" -> when(updatedAccount.getPrimaryDevice()).thenAnswer(stubbing);
case "isEnabled" -> when(updatedAccount.isEnabled()).thenAnswer(stubbing);
case "isDiscoverableByPhoneNumber" -> when(updatedAccount.isDiscoverableByPhoneNumber()).thenAnswer(stubbing);
case "getNextDeviceId" -> when(updatedAccount.getNextDeviceId()).thenAnswer(stubbing);
case "isPaymentActivationSupported" -> when(updatedAccount.isPaymentActivationSupported()).thenAnswer(stubbing);
case "isDeleteSyncSupported" -> when(updatedAccount.isDeleteSyncSupported()).thenAnswer(stubbing);
case "hasEnabledLinkedDevice" -> when(updatedAccount.hasEnabledLinkedDevice()).thenAnswer(stubbing);
case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing);
case "getIdentityKey" ->
when(updatedAccount.getIdentityKey(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing);

View File

@@ -159,8 +159,6 @@ public class AuthHelper {
when(UNDISCOVERABLE_ACCOUNT.getDevices()).thenReturn(List.of(UNDISCOVERABLE_DEVICE));
when(VALID_ACCOUNT_3.getDevices()).thenReturn(List.of(VALID_DEVICE_3_PRIMARY, VALID_DEVICE_3_LINKED));
when(VALID_ACCOUNT_TWO.hasEnabledLinkedDevice()).thenReturn(true);
when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER);
when(VALID_ACCOUNT.getUuid()).thenReturn(VALID_UUID);
when(VALID_ACCOUNT.getPhoneNumberIdentifier()).thenReturn(VALID_PNI);
@@ -180,11 +178,6 @@ public class AuthHelper {
when(VALID_ACCOUNT_3.getIdentifier(IdentityType.ACI)).thenReturn(VALID_UUID_3);
when(VALID_ACCOUNT_3.getIdentifier(IdentityType.PNI)).thenReturn(VALID_PNI_3);
when(VALID_ACCOUNT.isEnabled()).thenReturn(true);
when(VALID_ACCOUNT_TWO.isEnabled()).thenReturn(true);
when(UNDISCOVERABLE_ACCOUNT.isEnabled()).thenReturn(true);
when(VALID_ACCOUNT_3.isEnabled()).thenReturn(true);
when(VALID_ACCOUNT.isDiscoverableByPhoneNumber()).thenReturn(true);
when(VALID_ACCOUNT_TWO.isDiscoverableByPhoneNumber()).thenReturn(true);
when(UNDISCOVERABLE_ACCOUNT.isDiscoverableByPhoneNumber()).thenReturn(false);
@@ -284,7 +277,6 @@ public class AuthHelper {
when(account.getPrimaryDevice()).thenReturn(device);
when(account.getNumber()).thenReturn(number);
when(account.getUuid()).thenReturn(uuid);
when(account.isEnabled()).thenReturn(true);
when(accountsManager.getByE164(number)).thenReturn(Optional.of(account));
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
}

View File

@@ -119,35 +119,44 @@ class DestinationDeviceValidatorTest {
return account;
}
static Stream<Arguments> validateCompleteDeviceListSource() {
static Stream<Arguments> validateCompleteDeviceList() {
final byte id1 = 1;
final byte id2 = 2;
final byte id3 = 3;
return Stream.of(
// Device IDs provided for all enabled devices
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id1, id3),
null,
null,
Collections.emptySet()),
// Device ID provided for disabled device
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id1, id2, id3),
null,
Set.of(id2),
null,
Collections.emptySet()),
// Device ID omitted for enabled device
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id1),
Set.of(id3),
null,
Collections.emptySet()),
// Device ID included for disabled device, omitted for enabled device
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id1, id2),
Set.of(id3),
Set.of(id2),
null,
Collections.emptySet()),
// Device ID omitted for enabled device, included for device in excluded list
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id1),
@@ -155,13 +164,17 @@ class DestinationDeviceValidatorTest {
Set.of(id1),
Set.of(id1)
),
// Device ID omitted for enabled device, included for disabled device, omitted for excluded device
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id2),
Set.of(id3),
Set.of(id2),
null,
Set.of(id1)
),
// Device ID included for enabled device, omitted for excluded device
arguments(
mockAccountWithDeviceAndEnabled(Map.of(id1, true, id2, false, id3, true)),
Set.of(id3),
@@ -173,8 +186,8 @@ class DestinationDeviceValidatorTest {
}
@ParameterizedTest
@MethodSource("validateCompleteDeviceListSource")
void testValidateCompleteDeviceList(
@MethodSource
void validateCompleteDeviceList(
Account account,
Set<Byte> deviceIds,
Collection<Byte> expectedMissingDeviceIds,