diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index a1c5434d8..a5b542583 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -46,7 +46,9 @@ import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle; +import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge; import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge; @@ -246,6 +248,8 @@ public class WhisperServerService extends Application devices = new LinkedList<>(); + + for (Device device : account.getDevices()) { + devices.add(new DeviceInfo(device.getId(), device.getName(), + device.getLastSeen(), device.getCreated())); + } + + return new DeviceInfoList(devices); + } + + @Timed + @DELETE + @Path("/{device_id}") + public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) { + account.removeDevice(deviceId); + accounts.update(account); + } + @Timed @GET @Path("/provisioning/code") @Produces(MediaType.APPLICATION_JSON) public VerificationCode createDeviceToken(@Auth Account account) - throws RateLimitExceededException + throws RateLimitExceededException, DeviceLimitExceededException { rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber()); + if (account.getActiveDeviceCount() >= MAX_DEVICES) { + throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES); + } + VerificationCode verificationCode = generateVerificationCode(); pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode()); @@ -92,7 +125,7 @@ public class DeviceController { public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode, @HeaderParam("Authorization") String authorizationHeader, @Valid AccountAttributes accountAttributes) - throws RateLimitExceededException + throws RateLimitExceededException, DeviceLimitExceededException { try { AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); @@ -115,13 +148,19 @@ public class DeviceController { throw new WebApplicationException(Response.status(403).build()); } + if (account.get().getActiveDeviceCount() >= MAX_DEVICES) { + throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES); + } + Device device = new Device(); + device.setName(accountAttributes.getName()); device.setAuthenticationCredentials(new AuthenticationCredentials(password)); device.setSignalingKey(accountAttributes.getSignalingKey()); device.setFetchesMessages(accountAttributes.getFetchesMessages()); device.setId(account.get().getNextDeviceId()); device.setRegistrationId(accountAttributes.getRegistrationId()); device.setLastSeen(Util.todayInMillis()); + device.setCreated(System.currentTimeMillis()); account.get().addDevice(device); accounts.update(account.get()); diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java new file mode 100644 index 000000000..35dc1741d --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java @@ -0,0 +1,21 @@ +package org.whispersystems.textsecuregcm.controllers; + + +public class DeviceLimitExceededException extends Exception { + + private final int currentDevices; + private final int maxDevices; + + public DeviceLimitExceededException(int currentDevices, int maxDevices) { + this.currentDevices = currentDevices; + this.maxDevices = maxDevices; + } + + public int getCurrentDevices() { + return currentDevices; + } + + public int getMaxDevices() { + return maxDevices; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index 58cf85293..545c92ab8 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -17,8 +17,12 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.Max; + public class AccountAttributes { @JsonProperty @@ -31,12 +35,23 @@ public class AccountAttributes { @JsonProperty private int registrationId; + @JsonProperty + @Length(max = 50, message = "This field must be less than 50 characters") + private String name; + public AccountAttributes() {} + @VisibleForTesting public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) { + this(signalingKey, fetchesMessages, registrationId, null); + } + + @VisibleForTesting + public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name) { this.signalingKey = signalingKey; this.fetchesMessages = fetchesMessages; this.registrationId = registrationId; + this.name = name; } public String getSignalingKey() { @@ -50,4 +65,8 @@ public class AccountAttributes { public int getRegistrationId() { return registrationId; } + + public String getName() { + return name; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java new file mode 100644 index 000000000..8ca6a1b7b --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java @@ -0,0 +1,24 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DeviceInfo { + @JsonProperty + private long id; + + @JsonProperty + private String name; + + @JsonProperty + private long lastSeen; + + @JsonProperty + private long created; + + public DeviceInfo(long id, String name, long lastSeen, long created) { + this.id = id; + this.name = name; + this.lastSeen = lastSeen; + this.created = created; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java new file mode 100644 index 000000000..bb3507eed --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java @@ -0,0 +1,15 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class DeviceInfoList { + + @JsonProperty + private List devices; + + public DeviceInfoList(List devices) { + this.devices = devices; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java index 909b651bc..50277a539 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java @@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account { @Override public Optional getAuthenticatedDevice() { - return Optional.of(new Device(deviceId, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis())); + return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis())); } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java b/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java new file mode 100644 index 000000000..5314b231a --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java @@ -0,0 +1,33 @@ +package org.whispersystems.textsecuregcm.mappers; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class DeviceLimitExceededExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(DeviceLimitExceededException exception) { + return Response.status(411) + .entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(), + exception.getMaxDevices())) + .build(); + } + + private static class DeviceLimitExceededDetails { + @JsonProperty + private int current; + @JsonProperty + private int max; + + public DeviceLimitExceededDetails(int current, int max) { + this.current = current; + this.max = max; + } + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 3e381e44d..15fa31716 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -70,6 +70,10 @@ public class Account { this.devices.add(device); } + public void removeDevice(long deviceId) { + this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0)); + } + public Set getDevices() { return devices; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index d5190b24a..97f524978 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -31,6 +31,9 @@ public class Device { @JsonProperty private long id; + @JsonProperty + private String name; + @JsonProperty private String authToken; @@ -64,14 +67,19 @@ public class Device { @JsonProperty private long lastSeen; + @JsonProperty + private long created; + public Device() {} - public Device(long id, String authToken, String salt, + public Device(long id, String name, String authToken, String salt, String signalingKey, String gcmId, String apnId, String voipApnId, boolean fetchesMessages, - int registrationId, SignedPreKey signedPreKey, long lastSeen) + int registrationId, SignedPreKey signedPreKey, + long lastSeen, long created) { this.id = id; + this.name = name; this.authToken = authToken; this.salt = salt; this.signalingKey = signalingKey; @@ -82,6 +90,7 @@ public class Device { this.registrationId = registrationId; this.signedPreKey = signedPreKey; this.lastSeen = lastSeen; + this.created = created; } public String getApnId() { @@ -112,6 +121,14 @@ public class Device { return lastSeen; } + public void setCreated(long created) { + this.created = created; + } + + public long getCreated() { + return this.created; + } + public String getGcmId() { return gcmId; } @@ -132,6 +149,14 @@ public class Device { this.id = id; } + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + public void setAuthenticationCredentials(AuthenticationCredentials credentials) { this.authToken = credentials.getHashedAuthenticationToken(); this.salt = credentials.getSalt(); diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java b/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java index 9311f98f0..5a1449d72 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java +++ b/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java @@ -2,6 +2,7 @@ package org.whispersystems.textsecuregcm.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; public class SystemMapper { @@ -11,6 +12,7 @@ public class SystemMapper { static { mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } public static ObjectMapper getMapper() { diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index d1de0e573..f21b34360 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -17,6 +17,7 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.base.Optional; +import com.sun.jersey.api.client.ClientResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -25,6 +26,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.DeviceResponse; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; @@ -34,8 +36,10 @@ import org.whispersystems.textsecuregcm.util.VerificationCode; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; +import io.dropwizard.jersey.validation.ConstraintViolationExceptionMapper; import io.dropwizard.testing.junit.ResourceTestRule; import static org.fest.assertions.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; public class DeviceControllerTest { @@ -56,10 +60,13 @@ public class DeviceControllerTest { private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiter rateLimiter = mock(RateLimiter.class ); private Account account = mock(Account.class ); + private Account maxedAccount = mock(Account.class); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthenticator()) + .addProvider(new DeviceLimitExceededExceptionMapper()) + .addProvider(new ConstraintViolationExceptionMapper()) .addResource(new DumbVerificationDeviceController(pendingDevicesManager, accountsManager, rateLimiters)) @@ -75,9 +82,12 @@ public class DeviceControllerTest { when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter); when(account.getNextDeviceId()).thenReturn(42L); + when(maxedAccount.getActiveDeviceCount()).thenReturn(3); when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901")); + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of("1112223")); when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); + when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); } @Test @@ -98,4 +108,24 @@ public class DeviceControllerTest { verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER); } + + @Test + public void maxDevicesTest() throws Exception { + ClientResponse response = resources.client().resource("/v1/devices/provisioning/code") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .get(ClientResponse.class); + + assertEquals(response.getStatus(), 411); + } + + @Test + public void longNameTest() throws Exception { + ClientResponse response = resources.client().resource("/v1/devices/5678901") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters")) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class); + + assertEquals(response.getStatus(), 422); + } } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java index 909da6142..c8695b105 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java @@ -74,12 +74,12 @@ public class FederatedControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis())); }}; Set multiDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis())); - add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis())); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis())); + add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis())); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java index 342df3cf1..11e1aa864 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -69,13 +69,13 @@ public class MessageControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis())); }}; Set multiDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis())); - add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis())); - add(new Device(3, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31))); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis())); + add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis())); + add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis())); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java index a633f6638..97188ac26 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java @@ -47,12 +47,12 @@ public class ReceiptControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis())); }}; Set multiDeviceList = new HashSet() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis())); - add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis())); + add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis())); + add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis())); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 6f99fe902..913fd999d 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -17,6 +17,7 @@ import java.util.LinkedList; import java.util.List; import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -24,23 +25,38 @@ public class AuthHelper { public static final String VALID_NUMBER = "+14150000000"; public static final String VALID_PASSWORD = "foo"; + public static final String VALID_NUMBER_TWO = "+14151111111"; + public static final String VALID_PASSWORD_TWO = "baz"; + public static final String INVVALID_NUMBER = "+14151111111"; public static final String INVALID_PASSWORD = "bar"; public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class ); public static Account VALID_ACCOUNT = mock(Account.class ); + public static Account VALID_ACCOUNT_TWO = mock(Account.class); public static Device VALID_DEVICE = mock(Device.class ); - public static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class); + public static Device VALID_DEVICE_TWO = mock(Device.class); + private static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class); + private static AuthenticationCredentials VALID_CREDENTIALS_TWO = mock(AuthenticationCredentials.class); public static MultiBasicAuthProvider getAuthenticator() { when(VALID_CREDENTIALS.verify("foo")).thenReturn(true); + when(VALID_CREDENTIALS_TWO.verify("baz")).thenReturn(true); when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS); + when(VALID_DEVICE_TWO.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS_TWO); when(VALID_DEVICE.getId()).thenReturn(1L); + when(VALID_DEVICE_TWO.getId()).thenReturn(1L); when(VALID_ACCOUNT.getDevice(anyLong())).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT_TWO.getDevice(eq(1L))).thenReturn(Optional.of(VALID_DEVICE_TWO)); + when(VALID_ACCOUNT_TWO.getActiveDeviceCount()).thenReturn(3); when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER); + when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO); when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT_TWO.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE_TWO)); when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.absent()); + when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.absent()); when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT)); + when(ACCOUNTS_MANAGER.get(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); List peer = new LinkedList() {{ add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));