From 49ccbba2e31a0fce4a94d1a182d563bbf63664ad Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 10 Sep 2021 12:39:45 -0400 Subject: [PATCH] Generalize the "watch for websockets that need to be refreshed" listener --- .../textsecuregcm/WhisperServerService.java | 8 +- ...hEnablementRefreshRequirementProvider.java | 127 +++++++++++++++ .../AuthEnablementRequestEventListener.java | 152 ------------------ ...ocketRefreshApplicationEventListener.java} | 13 +- .../WebsocketRefreshRequestEventListener.java | 69 ++++++++ .../WebsocketRefreshRequirementProvider.java | 34 ++++ ...lementRefreshRequirementProviderTest.java} | 12 +- 7 files changed, 248 insertions(+), 167 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListener.java rename service/src/main/java/org/whispersystems/textsecuregcm/auth/{AuthEnablementApplicationEventListener.java => WebsocketRefreshApplicationEventListener.java} (51%) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java rename service/src/test/java/org/whispersystems/textsecuregcm/auth/{AuthEnablementRequestEventListenerTest.java => AuthEnablementRefreshRequirementProviderTest.java} (97%) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index dbf45bf2b..dc1162ab1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -62,7 +62,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; -import org.whispersystems.textsecuregcm.auth.AuthEnablementApplicationEventListener; +import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.CertificateGenerator; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; @@ -614,7 +614,7 @@ public class WhisperServerService extends Application( ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))); - environment.jersey().register(new AuthEnablementApplicationEventListener(clientPresenceManager)); + environment.jersey().register(new WebsocketRefreshApplicationEventListener(clientPresenceManager)); environment.jersey().register(new TimestampResponseFilter()); environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales())); @@ -626,7 +626,7 @@ public class WhisperServerService extends Application provisioningEnvironment = new WebSocketEnvironment<>(environment, webSocketEnvironment.getRequestLog(), 60000); - provisioningEnvironment.jersey().register(new AuthEnablementApplicationEventListener(clientPresenceManager)); + provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(clientPresenceManager)); provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager)); provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET)); provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager)); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java new file mode 100644 index 000000000..4d308e7bd --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java @@ -0,0 +1,127 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.ws.rs.core.SecurityContext; +import org.glassfish.jersey.server.ContainerRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; + +/** + * This {@link WebsocketRefreshRequirementProvider} observes intra-request changes in {@link Account#isEnabled()} and + * {@link Device#isEnabled()}. + *

+ * If a change in {@link Account#isEnabled()} is observed, then any active WebSocket connections for the account must be + * closed, in order for clients to get a refreshed {@link io.dropwizard.auth.Auth} object. + *

+ * If a change in {@link Device#isEnabled()} is observed, including deletion of the {@link Device}, then any active + * WebSocket connections for the device must be closed and re-authenticated. + * + * @see AuthenticatedAccount + * @see DisabledPermittedAuthenticatedAccount + */ +public class AuthEnablementRefreshRequirementProvider implements WebsocketRefreshRequirementProvider { + + private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRefreshRequirementProvider.class); + + private static final String ACCOUNT_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".accountEnabled"; + private static final String DEVICES_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".devicesEnabled"; + + private Optional findAccount(final ContainerRequest containerRequest) { + return Optional.ofNullable(containerRequest.getSecurityContext()) + .map(SecurityContext::getUserPrincipal) + .map(principal -> { + if (principal instanceof AccountAndAuthenticatedDeviceHolder) { + return ((AccountAndAuthenticatedDeviceHolder) principal).getAccount(); + } + return null; + }); + } + + @VisibleForTesting + Map buildDevicesEnabledMap(final Account account) { + return account.getDevices().stream() + .collect(() -> new HashMap<>(account.getDevices().size()), + (map, device) -> map.put(device.getId(), device.isEnabled()), HashMap::putAll); + } + + @Override + public void handleRequestStart(final ContainerRequest request) { + // The authenticated principal, if any, will be available after filters have run. + // Now that the account is known, capture a snapshot of `isEnabled` for the account and its devices, + // before carrying out the request’s business logic. + findAccount(request) + .ifPresent( + account -> { + request.setProperty(ACCOUNT_ENABLED, account.isEnabled()); + request.setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account)); + }); + } + + @Override + public List> handleRequestFinished(final ContainerRequest request) { + // Now that the request is finished, check whether `isEnabled` changed for any of the devices, or the account + // as a whole. If the value did change, the affected device(s) must disconnect and reauthenticate. + // If a device was removed, it must also disconnect. + if (request.getProperty(ACCOUNT_ENABLED) != null && + request.getProperty(DEVICES_ENABLED) != null) { + + final boolean accountInitiallyEnabled = (boolean) request.getProperty(ACCOUNT_ENABLED); + @SuppressWarnings("unchecked") final Map initialDevicesEnabled = + (Map) request.getProperty(DEVICES_ENABLED); + + return findAccount(request).map(account -> { + final Set deviceIdsToDisplace; + + if (account.isEnabled() != accountInitiallyEnabled) { + // the @Auth for all active connections must change when account.isEnabled() changes + deviceIdsToDisplace = account.getDevices().stream() + .map(Device::getId).collect(Collectors.toSet()); + + deviceIdsToDisplace.addAll(initialDevicesEnabled.keySet()); + + } else if (!initialDevicesEnabled.isEmpty()) { + + deviceIdsToDisplace = new HashSet<>(); + final Map currentDevicesEnabled = buildDevicesEnabledMap(account); + + initialDevicesEnabled.forEach((deviceId, enabled) -> { + // `null` indicates the device was removed from the account. Any active presence should be removed. + final boolean enabledMatches = Objects.equals(enabled, + currentDevicesEnabled.getOrDefault(deviceId, null)); + + if (!enabledMatches) { + deviceIdsToDisplace.add(deviceId); + } + }); + } else { + deviceIdsToDisplace = Collections.emptySet(); + } + + return deviceIdsToDisplace.stream().map(deviceId -> new Pair<>(account.getUuid(), deviceId)) + .collect(Collectors.toList()); + }).orElseGet(() -> { + logger.error("Request had account, but it is no longer present"); + return Collections.emptyList(); + }); + } else + return Collections.emptyList(); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListener.java deleted file mode 100644 index e4f16054f..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListener.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.auth; - -import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; - -import com.google.common.annotations.VisibleForTesting; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.Metrics; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import javax.ws.rs.core.SecurityContext; -import org.glassfish.jersey.server.ContainerRequest; -import org.glassfish.jersey.server.monitoring.RequestEvent; -import org.glassfish.jersey.server.monitoring.RequestEvent.Type; -import org.glassfish.jersey.server.monitoring.RequestEventListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.whispersystems.textsecuregcm.push.ClientPresenceManager; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.Device; - -/** - * This {@link RequestEventListener} observes intra-request changes in {@link Account#isEnabled()} and {@link - * Device#isEnabled()}. - *

- * If a change in {@link Account#isEnabled()} is observed, then any active WebSocket connections for the account must be - * closed, in order for clients to get a refreshed {@link io.dropwizard.auth.Auth} object. - *

- * If a change in {@link Device#isEnabled()} is observed, including deletion of the {@link Device}, then any active - * WebSocket connections for the device must be closed and re-authenticated. - * - * @see AuthenticatedAccount - * @see DisabledPermittedAuthenticatedAccount - */ -public class AuthEnablementRequestEventListener implements RequestEventListener { - - private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRequestEventListener.class); - - private static final String ACCOUNT_ENABLED = AuthEnablementRequestEventListener.class.getName() + ".accountEnabled"; - private static final String DEVICES_ENABLED = AuthEnablementRequestEventListener.class.getName() + ".devicesEnabled"; - - private static final Counter DISPLACED_ACCOUNTS = Metrics.counter( - name(AuthEnablementRequestEventListener.class, "displacedAccounts")); - private static final Counter DISPLACED_DEVICES = Metrics.counter( - name(AuthEnablementRequestEventListener.class, "displacedDevices")); - - private final ClientPresenceManager clientPresenceManager; - - public AuthEnablementRequestEventListener(final ClientPresenceManager clientPresenceManager) { - this.clientPresenceManager = clientPresenceManager; - } - - @Override - public void onEvent(final RequestEvent event) { - - if (event.getType() == Type.REQUEST_FILTERED) { - // The authenticated principal, if any, will be available after filters have run. - // Now that the account is known, capture a snapshot of `isEnabled` for the account and its devices, - // before carrying out the request’s business logic. - findAccount(event.getContainerRequest()) - .ifPresent( - account -> { - event.getContainerRequest().setProperty(ACCOUNT_ENABLED, account.isEnabled()); - event.getContainerRequest().setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account)); - }); - - } else if (event.getType() == Type.FINISHED) { - // Now that the request is finished, check whether `isEnabled` changed for any of the devices, or the account - // as a whole. If the value did change, the affected device(s) must disconnect and reauthenticate. - // If a device was removed, it must also disconnect. - if (event.getContainerRequest().getProperty(ACCOUNT_ENABLED) != null && - event.getContainerRequest().getProperty(DEVICES_ENABLED) != null) { - - final boolean accountInitiallyEnabled = (boolean) event.getContainerRequest().getProperty(ACCOUNT_ENABLED); - @SuppressWarnings("unchecked") final Map initialDevicesEnabled = (Map) event.getContainerRequest() - .getProperty(DEVICES_ENABLED); - - findAccount(event.getContainerRequest()).ifPresentOrElse(account -> { - final Set deviceIdsToDisplace; - - if (account.isEnabled() != accountInitiallyEnabled) { - // the @Auth for all active connections must change when account.isEnabled() changes - deviceIdsToDisplace = account.getDevices().stream() - .map(Device::getId).collect(Collectors.toSet()); - - deviceIdsToDisplace.addAll(initialDevicesEnabled.keySet()); - - DISPLACED_ACCOUNTS.increment(); - - } else if (!initialDevicesEnabled.isEmpty()) { - - deviceIdsToDisplace = new HashSet<>(); - final Map currentDevicesEnabled = buildDevicesEnabledMap(account); - - initialDevicesEnabled.forEach((deviceId, enabled) -> { - // `null` indicates the device was removed from the account. Any active presence should be removed. - final boolean enabledMatches = Objects.equals(enabled, - currentDevicesEnabled.getOrDefault(deviceId, null)); - - if (!enabledMatches) { - deviceIdsToDisplace.add(deviceId); - - DISPLACED_DEVICES.increment(); - } - }); - } else { - deviceIdsToDisplace = Collections.emptySet(); - } - - deviceIdsToDisplace.forEach(deviceId -> { - try { - // displacing presence will cause a reauthorization for the device’s active connections - clientPresenceManager.displacePresence(account.getUuid(), deviceId); - } catch (final Exception e) { - logger.error("Could not displace device presence", e); - } - }); - }, - () -> logger.error("Request had account, but it is no longer present") - ); - } - } - } - - private Optional findAccount(final ContainerRequest containerRequest) { - return Optional.ofNullable(containerRequest.getSecurityContext()) - .map(SecurityContext::getUserPrincipal) - .map(principal -> { - if (principal instanceof AccountAndAuthenticatedDeviceHolder) { - return ((AccountAndAuthenticatedDeviceHolder) principal).getAccount(); - } - return null; - }); - } - - @VisibleForTesting - Map buildDevicesEnabledMap(final Account account) { - return account.getDevices().stream() - .collect(() -> new HashMap<>(account.getDevices().size()), - (map, device) -> map.put(device.getId(), device.isEnabled()), HashMap::putAll); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementApplicationEventListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java similarity index 51% rename from service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementApplicationEventListener.java rename to service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java index 941ec58be..68dac01d0 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementApplicationEventListener.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java @@ -12,14 +12,15 @@ import org.glassfish.jersey.server.monitoring.RequestEventListener; import org.whispersystems.textsecuregcm.push.ClientPresenceManager; /** - * Delegates request events to a listener that handles auth-enablement changes + * Delegates request events to a listener that watches for intra-request changes that require websocket refreshes */ -public class AuthEnablementApplicationEventListener implements ApplicationEventListener { +public class WebsocketRefreshApplicationEventListener implements ApplicationEventListener { - private final AuthEnablementRequestEventListener authEnablementRequestEventListener; + private final WebsocketRefreshRequestEventListener websocketRefreshRequestEventListener; - public AuthEnablementApplicationEventListener(final ClientPresenceManager clientPresenceManager) { - this.authEnablementRequestEventListener = new AuthEnablementRequestEventListener(clientPresenceManager); + public WebsocketRefreshApplicationEventListener(final ClientPresenceManager clientPresenceManager) { + this.websocketRefreshRequestEventListener = new WebsocketRefreshRequestEventListener(clientPresenceManager, + new AuthEnablementRefreshRequirementProvider()); } @Override @@ -28,6 +29,6 @@ public class AuthEnablementApplicationEventListener implements ApplicationEventL @Override public RequestEventListener onRequest(final RequestEvent requestEvent) { - return authEnablementRequestEventListener; + return websocketRefreshRequestEventListener; } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java new file mode 100644 index 000000000..8d85fef92 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class WebsocketRefreshRequestEventListener implements RequestEventListener { + + private final ClientPresenceManager clientPresenceManager; + private final WebsocketRefreshRequirementProvider[] providers; + + private static final Counter DISPLACED_ACCOUNTS = Metrics.counter( + name(WebsocketRefreshRequestEventListener.class, "displacedAccounts")); + + private static final Counter DISPLACED_DEVICES = Metrics.counter( + name(WebsocketRefreshRequestEventListener.class, "displacedDevices")); + + private static final Logger logger = LoggerFactory.getLogger(WebsocketRefreshRequestEventListener.class); + + public WebsocketRefreshRequestEventListener( + final ClientPresenceManager clientPresenceManager, + final WebsocketRefreshRequirementProvider... providers) { + + this.clientPresenceManager = clientPresenceManager; + this.providers = providers; + } + + @Override + public void onEvent(final RequestEvent event) { + if (event.getType() == Type.REQUEST_FILTERED) { + for (final WebsocketRefreshRequirementProvider provider : providers) { + provider.handleRequestStart(event.getContainerRequest()); + } + } else if (event.getType() == Type.FINISHED) { + final AtomicInteger displacedDevices = new AtomicInteger(0); + + Arrays.stream(providers) + .flatMap(provider -> provider.handleRequestFinished(event.getContainerRequest()).stream()) + .distinct() + .forEach(pair -> { + try { + displacedDevices.incrementAndGet(); + clientPresenceManager.displacePresence(pair.first(), pair.second()); + } catch (final Exception e) { + logger.error("Could not displace device presence", e); + } + }); + + if (displacedDevices.get() > 0) { + DISPLACED_ACCOUNTS.increment(); + DISPLACED_DEVICES.increment(displacedDevices.get()); + } + } + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java new file mode 100644 index 000000000..f036907f4 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.List; +import java.util.UUID; +import org.glassfish.jersey.server.ContainerRequest; +import org.whispersystems.textsecuregcm.util.Pair; + +/** + * A websocket refresh requirement provider watches for intra-request changes (e.g. to authentication status) that + * require a websocket refresh. + */ +public interface WebsocketRefreshRequirementProvider { + + /** + * Processes a request after filters have run and the request has been mapped to a destination controller. + * + * @param request the request to observe + */ + void handleRequestStart(ContainerRequest request); + + /** + * Processes a request after all normal request handling has been completed. + * + * @param request the request to observe + * @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a + * result of the observed request + */ + List> handleRequestFinished(ContainerRequest request); +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListenerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java similarity index 97% rename from service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListenerTest.java rename to service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java index d2b82d10a..5c1d3bf7b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRequestEventListenerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java @@ -85,7 +85,7 @@ import org.whispersystems.websocket.messages.protobuf.SubProtocol; import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; @ExtendWith(DropwizardExtensionsSupport.class) -class AuthEnablementRequestEventListenerTest { +class AuthEnablementRefreshRequirementProviderTest { private final ApplicationEventListener applicationEventListener = mock(ApplicationEventListener.class); @@ -109,12 +109,14 @@ class AuthEnablementRequestEventListenerTest { private ClientPresenceManager clientPresenceManager; - private AuthEnablementRequestEventListener listener; + private WebsocketRefreshRequestEventListener listener; + private AuthEnablementRefreshRequirementProvider provider; @BeforeEach void setup() { clientPresenceManager = mock(ClientPresenceManager.class); - listener = new AuthEnablementRequestEventListener(clientPresenceManager); + provider = new AuthEnablementRefreshRequirementProvider(); + listener = new WebsocketRefreshRequestEventListener(clientPresenceManager, provider); when(applicationEventListener.onRequest(any())).thenReturn(listener); final UUID uuid = UUID.randomUUID(); @@ -146,7 +148,7 @@ class AuthEnablementRequestEventListenerTest { devices.add(device); }); - final Map devicesEnabled = listener.buildDevicesEnabledMap(account); + final Map devicesEnabled = provider.buildDevicesEnabledMap(account); assertEquals(4, devicesEnabled.size()); @@ -372,7 +374,7 @@ class AuthEnablementRequestEventListenerTest { } @ParameterizedTest - @MethodSource("org.whispersystems.textsecuregcm.auth.AuthEnablementRequestEventListenerTest#testAccountEnabledChanged") + @MethodSource("org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementProviderTest#testAccountEnabledChanged") void testAccountEnabledChangedWebSocket(final long authenticatedDeviceId, final boolean initialEnabled, final boolean finalEnabled) throws Exception {