Delay processing FCM uninstalled feedback

Check to make sure client is not still active before unregistering,
since FCM feedback seems to be often erroneous
This commit is contained in:
Moxie Marlinspike 2019-05-06 21:06:18 -07:00
parent 92ca8862e1
commit 4d9c9206cf
13 changed files with 259 additions and 63 deletions

View File

@ -210,7 +210,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ActiveUserCounter activeUserCounter = new ActiveUserCounter(config.getMetricsFactory(), cacheClient); ActiveUserCounter activeUserCounter = new ActiveUserCounter(config.getMetricsFactory(), cacheClient);
DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryReconciliationClient, directory); DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryReconciliationClient, directory);
AccountCleaner accountCleaner = new AccountCleaner(accountsManager, directoryQueue); AccountCleaner accountCleaner = new AccountCleaner(accountsManager, directoryQueue);
List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = Arrays.asList(activeUserCounter, directoryReconciler, accountCleaner); PushFeedbackProcessor pushFeedbackProcessor = new PushFeedbackProcessor(accountsManager, directoryQueue);
List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = List.of(pushFeedbackProcessor, activeUserCounter, directoryReconciler, accountCleaner);
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheClient); AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheClient);
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accounts, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()); AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accounts, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs());

View File

@ -17,6 +17,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -111,19 +112,20 @@ public class GCMSender implements Managed {
GcmMessage message = (GcmMessage)result.getContext(); GcmMessage message = (GcmMessage)result.getContext();
logger.warn("Got GCM unregistered notice! " + message.getGcmId()); logger.warn("Got GCM unregistered notice! " + message.getGcmId());
// Optional<Account> account = getAccountForEvent(message); Optional<Account> account = getAccountForEvent(message);
//
// if (account.isPresent()) { if (account.isPresent()) {
// Device device = account.get().getDevice(message.getDeviceId()).get(); Device device = account.get().getDevice(message.getDeviceId()).get();
device.setUninstalledFeedbackTimestamp(Util.todayInMillis());
// device.setGcmId(null); // device.setGcmId(null);
// device.setFetchesMessages(false); // device.setFetchesMessages(false);
//
// accountsManager.update(account.get()); accountsManager.update(account.get());
//
// if (!account.get().isActive()) { // if (!account.get().isActive()) {
// directoryQueue.deleteRegisteredUser(account.get().getNumber()); // directoryQueue.deleteRegisteredUser(account.get().getNumber());
// } // }
// } }
unregistered.mark(); unregistered.mark();
} }

View File

@ -93,7 +93,7 @@ public class Account implements Principal {
} }
public void removeDevice(long deviceId) { public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false)); this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false, 0));
} }
public Set<Device> getDevices() { public Set<Device> getDevices() {

View File

@ -55,6 +55,9 @@ public class Device {
@JsonProperty @JsonProperty
private long pushTimestamp; private long pushTimestamp;
@JsonProperty
private long uninstalledFeedback;
@JsonProperty @JsonProperty
private boolean fetchesMessages; private boolean fetchesMessages;
@ -83,7 +86,7 @@ public class Device {
String voipApnId, boolean fetchesMessages, String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey, int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, String userAgent, long lastSeen, long created, String userAgent,
boolean unauthenticatedDelivery) boolean unauthenticatedDelivery, long uninstalledFeedback)
{ {
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -100,6 +103,7 @@ public class Device {
this.created = created; this.created = created;
this.userAgent = userAgent; this.userAgent = userAgent;
this.unauthenticatedDelivery = unauthenticatedDelivery; this.unauthenticatedDelivery = unauthenticatedDelivery;
this.uninstalledFeedback = uninstalledFeedback;
} }
public String getApnId() { public String getApnId() {
@ -122,6 +126,14 @@ public class Device {
this.voipApnId = voipApnId; this.voipApnId = voipApnId;
} }
public void setUninstalledFeedbackTimestamp(long uninstalledFeedback) {
this.uninstalledFeedback = uninstalledFeedback;
}
public long getUninstalledFeedbackTimestamp() {
return uninstalledFeedback;
}
public void setLastSeen(long lastSeen) { public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
} }

View File

@ -0,0 +1,69 @@
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
public class PushFeedbackProcessor implements AccountDatabaseCrawlerListener {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter expired = metricRegistry.meter(name(getClass(), "unregistered", "expired"));
private final Meter recovered = metricRegistry.meter(name(getClass(), "unregistered", "recovered"));
private final AccountsManager accountsManager;
private final DirectoryQueue directoryQueue;
public PushFeedbackProcessor(AccountsManager accountsManager, DirectoryQueue directoryQueue) {
this.accountsManager = accountsManager;
this.directoryQueue = directoryQueue;
}
@Override
public void onCrawlStart() {}
@Override
public void onCrawlChunk(Optional<String> fromNumber, List<Account> chunkAccounts) {
for (Account account : chunkAccounts) {
boolean update = false;
for (Device device : account.getDevices()) {
if (device.getUninstalledFeedbackTimestamp() != 0 &&
device.getUninstalledFeedbackTimestamp() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis())
{
if (device.getLastSeen() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis()) {
device.setGcmId(null);
device.setApnId(null);
device.setVoipApnId(null);
device.setFetchesMessages(false);
expired.mark();
} else {
device.setUninstalledFeedbackTimestamp(0);
recovered.mark();
}
update = true;
}
}
if (update) {
accountsManager.update(account);
if (!account.isEnabled()) {
directoryQueue.deleteRegisteredUser(account.getNumber());
}
}
}
}
@Override
public void onCrawlEnd(Optional<String> fromNumber) {}
}

View File

@ -80,13 +80,13 @@ public class MessageControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{ Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true)); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
}}; }};
Set<Device> multiDeviceList = new HashSet<Device>() {{ Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true)); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true)); add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true)); add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true, 0));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList, "1234".getBytes()); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList, "1234".getBytes());

View File

@ -1,9 +1,7 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.hamcrest.MatcherAssert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -25,7 +23,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.*; import static junit.framework.TestCase.*;
@ -55,8 +52,8 @@ public class TransparentDataControllerTest {
@Before @Before
public void setup() { public void setup() {
Account accountOne = new Account("+14151231111", Collections.singleton(new Device(1, "foo", "bar", "salt", "keykey", "gcm-id", "apn-id", "voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]); Account accountOne = new Account("+14151231111", Collections.singleton(new Device(1, "foo", "bar", "salt", "keykey", "gcm-id", "apn-id", "voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true, 0)), new byte[16]);
Account accountTwo = new Account("+14151232222", Collections.singleton(new Device(1, "2foo", "2bar", "2salt", "2keykey", "2gcm-id", "2apn-id", "2voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]); Account accountTwo = new Account("+14151232222", Collections.singleton(new Device(1, "2foo", "2bar", "2salt", "2keykey", "2gcm-id", "2apn-id", "2voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true, 0)), new byte[16]);
accountOne.setProfileName("OneProfileName"); accountOne.setProfileName("OneProfileName");
accountOne.setIdentityKey("identity_key_value"); accountOne.setIdentityKey("identity_key_value");

View File

@ -13,6 +13,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService; import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Optional; import java.util.Optional;
@ -48,45 +49,45 @@ public class GCMSenderTest {
verify(sender, times(1)).send(any(Message.class), eq(message)); verify(sender, times(1)).send(any(Message.class), eq(message));
} }
// @Test @Test
// public void testSendError() { public void testSendUninstalled() {
// String destinationNumber = "+12223334444"; String destinationNumber = "+12223334444";
// String gcmId = "foo"; String gcmId = "foo";
//
// AccountsManager accountsManager = mock(AccountsManager.class); AccountsManager accountsManager = mock(AccountsManager.class);
// Sender sender = mock(Sender.class ); Sender sender = mock(Sender.class );
// Result invalidResult = mock(Result.class ); Result invalidResult = mock(Result.class );
// DirectoryQueue directoryQueue = mock(DirectoryQueue.class ); DirectoryQueue directoryQueue = mock(DirectoryQueue.class );
// SynchronousExecutorService executorService = new SynchronousExecutorService(); SynchronousExecutorService executorService = new SynchronousExecutorService();
//
// Account destinationAccount = mock(Account.class); Account destinationAccount = mock(Account.class);
// Device destinationDevice = mock(Device.class ); Device destinationDevice = mock(Device.class );
//
// when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
// when(accountsManager.get(destinationNumber)).thenReturn(Optional.of(destinationAccount)); when(accountsManager.get(destinationNumber)).thenReturn(Optional.of(destinationAccount));
// when(destinationDevice.getGcmId()).thenReturn(gcmId); when(destinationDevice.getGcmId()).thenReturn(gcmId);
//
// when(invalidResult.isInvalidRegistrationId()).thenReturn(true); when(invalidResult.isInvalidRegistrationId()).thenReturn(true);
// when(invalidResult.isUnregistered()).thenReturn(false); when(invalidResult.isUnregistered()).thenReturn(false);
// when(invalidResult.hasCanonicalRegistrationId()).thenReturn(false); when(invalidResult.hasCanonicalRegistrationId()).thenReturn(false);
// when(invalidResult.isSuccess()).thenReturn(true); when(invalidResult.isSuccess()).thenReturn(true);
//
// GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false); GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false);
// GCMSender gcmSender = new GCMSender(accountsManager, sender, directoryQueue, executorService); GCMSender gcmSender = new GCMSender(accountsManager, sender, directoryQueue, executorService);
//
// SettableFuture<Result> invalidFuture = SettableFuture.create(); SettableFuture<Result> invalidFuture = SettableFuture.create();
// invalidFuture.set(invalidResult); invalidFuture.set(invalidResult);
//
// when(sender.send(any(Message.class), Matchers.anyObject())).thenReturn(invalidFuture); when(sender.send(any(Message.class), Matchers.anyObject())).thenReturn(invalidFuture);
// when(invalidResult.getContext()).thenReturn(message); when(invalidResult.getContext()).thenReturn(message);
//
// gcmSender.sendMessage(message); gcmSender.sendMessage(message);
//
// verify(sender, times(1)).send(any(Message.class), eq(message)); verify(sender, times(1)).send(any(Message.class), eq(message));
// verify(accountsManager, times(1)).get(eq(destinationNumber)); verify(accountsManager, times(1)).get(eq(destinationNumber));
// verify(accountsManager, times(1)).update(eq(destinationAccount)); verify(accountsManager, times(1)).update(eq(destinationAccount));
// verify(destinationDevice, times(1)).setGcmId(eq((String)null)); verify(destinationDevice, times(1)).setUninstalledFeedbackTimestamp(eq(Util.todayInMillis()));
// } }
@Test @Test
public void testCanonicalId() { public void testCanonicalId() {

View File

@ -241,7 +241,7 @@ public class AccountsTest {
private Device generateDevice(long id) { private Device generateDevice(long id) {
Random random = new Random(System.currentTimeMillis()); Random random = new Random(System.currentTimeMillis());
SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt()); SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt());
return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt(), random.nextBoolean()); return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt(), random.nextBoolean(), 0);
} }
private Account generateAccount(String number) { private Account generateAccount(String number) {

View File

@ -21,7 +21,7 @@ public class PublicAccountTest {
@Test @Test
public void testPinSanitation() throws IOException { public void testPinSanitation() throws IOException {
Set<Device> devices = Collections.singleton(new Device(1, "foo", "bar", "12345", null, "gcm-1234", null, null, true, 1234, new SignedPreKey(1, "public-foo", "signature-foo"), 31337, 31336, "Android4Life", true)); Set<Device> devices = Collections.singleton(new Device(1, "foo", "bar", "12345", null, "gcm-1234", null, null, true, 1234, new SignedPreKey(1, "public-foo", "signature-foo"), 31337, 31336, "Android4Life", true, 0));
Account account = new Account("+14151231234", devices, new byte[16]); Account account = new Account("+14151231234", devices, new byte[16]);
account.setPin("123456"); account.setPin("123456");

View File

@ -0,0 +1,114 @@
package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Before;
import org.junit.Test;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.mockito.Mockito.*;
public class PushFeedbackProcessorTest {
private AccountsManager accountsManager = mock(AccountsManager.class);
private DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
private Account uninstalledAccount = mock(Account.class);
private Account mixedAccount = mock(Account.class);
private Account freshAccount = mock(Account.class);
private Account cleanAccount = mock(Account.class);
private Account stillActiveAccount = mock(Account.class);
private Device uninstalledDevice = mock(Device.class);
private Device uninstalledDeviceTwo = mock(Device.class);
private Device installedDevice = mock(Device.class);
private Device installedDeviceTwo = mock(Device.class);
private Device recentUninstalledDevice = mock(Device.class);
private Device stillActiveDevice = mock(Device.class);
@Before
public void setup() {
when(uninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(uninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(uninstalledDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(3));
when(uninstalledDeviceTwo.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(3));
when(installedDevice.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(installedDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(recentUninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(1));
when(recentUninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis());
when(stillActiveDevice.getUninstalledFeedbackTimestamp()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2));
when(stillActiveDevice.getLastSeen()).thenReturn(Util.todayInMillis());
when(uninstalledAccount.getDevices()).thenReturn(Set.of(uninstalledDevice));
when(mixedAccount.getDevices()).thenReturn(Set.of(installedDevice, uninstalledDeviceTwo));
when(freshAccount.getDevices()).thenReturn(Set.of(recentUninstalledDevice));
when(cleanAccount.getDevices()).thenReturn(Set.of(installedDeviceTwo));
when(stillActiveAccount.getDevices()).thenReturn(Set.of(stillActiveDevice));
}
@Test
public void testEmpty() {
PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, directoryQueue);
processor.onCrawlChunk(Optional.of("+14152222222"), Collections.emptyList());
verifyZeroInteractions(accountsManager);
verifyZeroInteractions(directoryQueue);
}
@Test
public void testUpdate() {
PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, directoryQueue);
processor.onCrawlChunk(Optional.of("+14153333333"), List.of(uninstalledAccount, mixedAccount, stillActiveAccount, freshAccount, cleanAccount));
verify(uninstalledDevice).setApnId(isNull());
verify(uninstalledDevice).setGcmId(isNull());
verify(uninstalledDevice).setFetchesMessages(eq(false));
verify(accountsManager).update(eq(uninstalledAccount));
verify(uninstalledDeviceTwo).setApnId(isNull());
verify(uninstalledDeviceTwo).setGcmId(isNull());
verify(uninstalledDeviceTwo).setFetchesMessages(eq(false));
verify(installedDevice, never()).setApnId(any());
verify(installedDevice, never()).setGcmId(any());
verify(installedDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager).update(eq(mixedAccount));
verify(recentUninstalledDevice, never()).setApnId(any());
verify(recentUninstalledDevice, never()).setGcmId(any());
verify(recentUninstalledDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager, never()).update(eq(freshAccount));
verify(installedDeviceTwo, never()).setApnId(any());
verify(installedDeviceTwo, never()).setGcmId(any());
verify(installedDeviceTwo, never()).setFetchesMessages(anyBoolean());
verify(accountsManager, never()).update(eq(cleanAccount));
verify(stillActiveDevice).setUninstalledFeedbackTimestamp(eq(0L));
verify(stillActiveDevice, never()).setApnId(any());
verify(stillActiveDevice, never()).setGcmId(any());
verify(stillActiveDevice, never()).setFetchesMessages(anyBoolean());
verify(accountsManager).update(eq(stillActiveAccount));
}
}

View File

@ -1 +1 @@
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false} {"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}

View File

@ -1 +1 @@
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false} {"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}