Only update devices that aren't already disabled

This commit is contained in:
Jon Chambers 2023-10-24 15:29:03 -04:00 committed by GitHub
parent 21125c2f5a
commit e4de6bf4a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 7 deletions

View File

@ -112,7 +112,7 @@ public class ProcessPushNotificationFeedbackCommand extends AbstractSinglePassCr
} }
@VisibleForTesting @VisibleForTesting
boolean deviceNeedsUpdate(final Device device) { boolean pushFeedbackIntervalElapsed(final Device device) {
// After we get an indication that a device may have uninstalled the Signal app (`uninstalledFeedbackTimestamp` is // After we get an indication that a device may have uninstalled the Signal app (`uninstalledFeedbackTimestamp` is
// non-zero), check back in after a few days to see what ultimately happened. // non-zero), check back in after a few days to see what ultimately happened.
return device.getUninstalledFeedbackTimestamp() != 0 && return device.getUninstalledFeedbackTimestamp() != 0 &&
@ -129,6 +129,11 @@ public class ProcessPushNotificationFeedbackCommand extends AbstractSinglePassCr
return Instant.ofEpochMilli(device.getLastSeen()).plus(MAX_TOKEN_REFRESH_DELAY).isBefore(clock.instant()); return Instant.ofEpochMilli(device.getLastSeen()).plus(MAX_TOKEN_REFRESH_DELAY).isBefore(clock.instant());
} }
@VisibleForTesting
boolean deviceNeedsUpdate(final Device device) {
return pushFeedbackIntervalElapsed(device) && (device.isEnabled() || device.getLastSeen() > device.getUninstalledFeedbackTimestamp());
}
@VisibleForTesting @VisibleForTesting
static Optional<String> getUserAgent(final Device device) { static Optional<String> getUserAgent(final Device device) {
if (StringUtils.isNotBlank(device.getApnId())) { if (StringUtils.isNotBlank(device.getApnId())) {

View File

@ -20,6 +20,7 @@ import reactor.core.publisher.Flux;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -40,6 +41,8 @@ class ProcessPushNotificationFeedbackCommandTest {
private ProcessPushNotificationFeedbackCommand processPushNotificationFeedbackCommand; private ProcessPushNotificationFeedbackCommand processPushNotificationFeedbackCommand;
private static final Instant CURRENT_TIME = Instant.now();
private static class TestProcessPushNotificationFeedbackCommand extends ProcessPushNotificationFeedbackCommand { private static class TestProcessPushNotificationFeedbackCommand extends ProcessPushNotificationFeedbackCommand {
private final CommandDependencies commandDependencies; private final CommandDependencies commandDependencies;
@ -80,7 +83,7 @@ class ProcessPushNotificationFeedbackCommandTest {
return CompletableFuture.completedFuture(account); return CompletableFuture.completedFuture(account);
}); });
clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); clock = Clock.fixed(CURRENT_TIME, ZoneId.systemDefault());
processPushNotificationFeedbackCommand = processPushNotificationFeedbackCommand =
new TestProcessPushNotificationFeedbackCommand(clock, accountsManager, true); new TestProcessPushNotificationFeedbackCommand(clock, accountsManager, true);
@ -95,6 +98,7 @@ class ProcessPushNotificationFeedbackCommandTest {
final Account accountWithActiveDevice = mock(Account.class); final Account accountWithActiveDevice = mock(Account.class);
{ {
final Device device = mock(Device.class); final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(true);
when(accountWithActiveDevice.getDevices()).thenReturn(List.of(device)); when(accountWithActiveDevice.getDevices()).thenReturn(List.of(device));
} }
@ -102,31 +106,44 @@ class ProcessPushNotificationFeedbackCommandTest {
final Account accountWithUninstalledDevice = mock(Account.class); final Account accountWithUninstalledDevice = mock(Account.class);
{ {
final Device uninstalledDevice = mock(Device.class); final Device uninstalledDevice = mock(Device.class);
when(uninstalledDevice.isEnabled()).thenReturn(true);
when(uninstalledDevice.getUninstalledFeedbackTimestamp()) when(uninstalledDevice.getUninstalledFeedbackTimestamp())
.thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2)).toEpochMilli()); .thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2)).toEpochMilli());
when(accountWithUninstalledDevice.getDevices()).thenReturn(List.of(uninstalledDevice)); when(accountWithUninstalledDevice.getDevices()).thenReturn(List.of(uninstalledDevice));
} }
final Account accountWithAlreadyDisabledUninstalledDevice = mock(Account.class);
{
final Device previouslyDisabledUninstalledDevice = mock(Device.class);
when(previouslyDisabledUninstalledDevice.isEnabled()).thenReturn(false);
when(previouslyDisabledUninstalledDevice.getUninstalledFeedbackTimestamp())
.thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2)).toEpochMilli());
when(accountWithAlreadyDisabledUninstalledDevice.getDevices()).thenReturn(List.of(previouslyDisabledUninstalledDevice));
}
processPushNotificationFeedbackCommand.crawlAccounts( processPushNotificationFeedbackCommand.crawlAccounts(
Flux.just(accountWithActiveDevice, accountWithUninstalledDevice).parallel()); Flux.just(accountWithActiveDevice, accountWithUninstalledDevice, accountWithAlreadyDisabledUninstalledDevice)
.parallel());
if (isDryRun) { if (isDryRun) {
verify(accountsManager, never()).updateAsync(any(), any()); verify(accountsManager, never()).updateAsync(any(), any());
} else { } else {
verify(accountsManager, never()).updateAsync(eq(accountWithActiveDevice), any()); verify(accountsManager, never()).updateAsync(eq(accountWithActiveDevice), any());
verify(accountsManager).updateAsync(eq(accountWithUninstalledDevice), any()); verify(accountsManager).updateAsync(eq(accountWithUninstalledDevice), any());
verify(accountsManager, never()).updateAsync(eq(accountWithAlreadyDisabledUninstalledDevice), any());
} }
} }
@Test @Test
void deviceNeedsUpdate() { void pushFeedbackIntervalElapsed() {
{ {
final Device deviceWithMaturePushNotificationFeedback = mock(Device.class); final Device deviceWithMaturePushNotificationFeedback = mock(Device.class);
when(deviceWithMaturePushNotificationFeedback.getUninstalledFeedbackTimestamp()) when(deviceWithMaturePushNotificationFeedback.getUninstalledFeedbackTimestamp())
.thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2)).toEpochMilli()); .thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2)).toEpochMilli());
assertTrue(processPushNotificationFeedbackCommand.deviceNeedsUpdate(deviceWithMaturePushNotificationFeedback)); assertTrue(processPushNotificationFeedbackCommand.pushFeedbackIntervalElapsed(deviceWithMaturePushNotificationFeedback));
} }
{ {
@ -134,13 +151,13 @@ class ProcessPushNotificationFeedbackCommandTest {
when(deviceWithRecentPushNotificationFeedback.getUninstalledFeedbackTimestamp()) when(deviceWithRecentPushNotificationFeedback.getUninstalledFeedbackTimestamp())
.thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.dividedBy(2)).toEpochMilli()); .thenReturn(clock.instant().minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.dividedBy(2)).toEpochMilli());
assertFalse(processPushNotificationFeedbackCommand.deviceNeedsUpdate(deviceWithRecentPushNotificationFeedback)); assertFalse(processPushNotificationFeedbackCommand.pushFeedbackIntervalElapsed(deviceWithRecentPushNotificationFeedback));
} }
{ {
final Device deviceWithoutPushNotificationFeedback = mock(Device.class); final Device deviceWithoutPushNotificationFeedback = mock(Device.class);
assertFalse(processPushNotificationFeedbackCommand.deviceNeedsUpdate(deviceWithoutPushNotificationFeedback)); assertFalse(processPushNotificationFeedbackCommand.pushFeedbackIntervalElapsed(deviceWithoutPushNotificationFeedback));
} }
} }
@ -164,6 +181,72 @@ class ProcessPushNotificationFeedbackCommandTest {
} }
} }
@ParameterizedTest
@MethodSource
void deviceNeedsUpdate(final Device device, final boolean expectNeedsUpdate) {
assertEquals(expectNeedsUpdate, processPushNotificationFeedbackCommand.deviceNeedsUpdate(device));
}
private static List<Arguments> deviceNeedsUpdate() {
final long maturePushFeedbackTimestamp =
CURRENT_TIME.minus(ProcessPushNotificationFeedbackCommand.MAX_TOKEN_REFRESH_DELAY.multipliedBy(2))
.toEpochMilli();
final List<Arguments> arguments = new ArrayList<>();
{
// Device is active, enabled, and has no push feedback
final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(true);
when(device.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli());
arguments.add(Arguments.of(device, false));
}
{
// Device is active, but not enabled, and has no push feedback
final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(false);
when(device.getUninstalledFeedbackTimestamp()).thenReturn(0L);
when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli());
arguments.add(Arguments.of(device, false));
}
{
// Device is active, enabled, and has "mature" push feedback
final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(true);
when(device.getUninstalledFeedbackTimestamp()).thenReturn(maturePushFeedbackTimestamp);
when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli());
arguments.add(Arguments.of(device, true));
}
{
// Device is active, but not enabled, and has "mature" push feedback
final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(false);
when(device.getUninstalledFeedbackTimestamp()).thenReturn(maturePushFeedbackTimestamp);
when(device.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli());
arguments.add(Arguments.of(device, true));
}
{
// Device is inactive, not enabled, and has "mature" push feedback
final Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(false);
when(device.getUninstalledFeedbackTimestamp()).thenReturn(maturePushFeedbackTimestamp);
when(device.getLastSeen()).thenReturn(maturePushFeedbackTimestamp);
arguments.add(Arguments.of(device, false));
}
return arguments;
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@ParameterizedTest @ParameterizedTest
@MethodSource @MethodSource