From 819ed87a178e71c6c40b70d6a5f079751337b51f Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Mon, 12 Oct 2020 16:58:33 +0200 Subject: [PATCH] [sinttest] Wait for notification filter to propagate The UserTuneIntegrationTest, in rapid succession: - add a listener for PEP-published usertune data - publishes a usertune - waits for a notification to arrive Implicit to adding the listener is the publication of a change in Pubsub notification filtering. This can involve a stanza handshake, as CAPS is involved. A race condition exists where the usertune data can be published before the notification filter has been properly applied. The changes in this commit add a synchronzation point that ensures that the notification filter is in place, before the usertune data is published. Co-authored-by: Paul Schaub --- .../GeolocationIntegrationTest.java | 186 ++++++++++++++-- .../smackx/mood/MoodIntegrationTest.java | 172 +++++++++++++-- .../usertune/UserTuneIntegrationTest.java | 208 ++++++++++++++---- 3 files changed, 485 insertions(+), 81 deletions(-) diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java index 32f1d32b3..7e149ff3b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/geolocation/GeolocationIntegrationTest.java @@ -23,8 +23,9 @@ import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.disco.EntityCapabilitiesChangedListener; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.geoloc.GeoLocationManager; import org.jivesoftware.smackx.geoloc.packet.GeoLocation; import org.jivesoftware.smackx.pep.PepEventListener; @@ -35,7 +36,7 @@ import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.jxmpp.jid.EntityBareJid; +import org.junit.jupiter.api.Assertions; import org.jxmpp.util.XmppDateTime; public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { @@ -49,10 +50,21 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { glm2 = GeoLocationManager.getInstanceFor(conTwo); } + @AfterClass + public void unsubscribe() throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + } + + /** + * Verifies that a notification is sent when a publication is received, assuming that notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ @SmackIntegrationTest - public void test() throws TimeoutException, Exception { + public void testNotification() throws Exception { GeoLocation.Builder builder = GeoLocation.builder(); - GeoLocation geoLocation1 = builder.setAccuracy(23d) + GeoLocation data = builder.setAccuracy(23d) .setAlt(1000d) .setAltAccuracy(10d) .setArea("Delhi") @@ -77,31 +89,163 @@ public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest { .build(); IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); - final SimpleResultSyncPoint geoLocationReceived = new SimpleResultSyncPoint(); - final PepEventListener geoLocationListener = new PepEventListener() { - @Override - public void onPepEvent(EntityBareJid jid, GeoLocation geoLocation, String id, Message message) { - if (geoLocation.equals(geoLocation1)) { - geoLocationReceived.signal(); - } else { - geoLocationReceived.signalFailure("Received non matching GeoLocation"); - } + final SimpleResultSyncPoint geoLocationReceived = new SimpleResultSyncPoint(); + + final PepEventListener geoLocationListener = (jid, geoLocation, id, message) -> { + if (geoLocation.equals(data)) { + geoLocationReceived.signal(); } }; - glm2.addGeoLocationListener(geoLocationListener); - try { - glm1.publishGeoLocation(geoLocation1); - geoLocationReceived.waitForResult(timeout); + // Register ConTwo's interest in receiving geolocation notifications, and wait for that interest to have been propagated. + registerListenerAndWait(glm2, ServiceDiscoveryManager.getInstanceFor(conTwo), geoLocationListener); + + // Publish the data. + glm1.publishGeoLocation(data); // for the purpose of this test, this needs not be blocking/use publishAndWait(); + + // Wait for the data to be received. + try { + Object result = geoLocationReceived.waitForResult(timeout); + + // Explicitly assert the success case. + Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } } finally { - glm2.removeGeoLocationListener(geoLocationListener); + unregisterListener(glm2, geoLocationListener); } } - @AfterClass - public void unsubscribe() throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); + /** + * Verifies that a notification for a previously sent publication is received as soon as notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ + @SmackIntegrationTest + public void testNotificationAfterFilterChange() throws Exception { + GeoLocation.Builder builder = GeoLocation.builder(); + GeoLocation data = builder.setAccuracy(12d) + .setAlt(999d) + .setAltAccuracy(9d) + .setArea("Amsterdam") + .setBearing(9d) + .setBuilding("Test Building") + .setCountry("Netherlands") + .setCountryCode("NL") + .setDescription("My Description") + .setFloor("middle") + .setLat(25.098345d) + .setLocality("brilliant") + .setLon(77.992034) + .setPostalcode("110085") + .setRegion("North") + .setRoom("small") + .setSpeed(250.0d) + .setStreet("Wall Street") + .setText("Unit Testing GeoLocation 2") + .setTimestamp(XmppDateTime.parseDate("2007-02-19")) + .setTzo("+5:30") + .setUri(new URI("http://xmpp.org")) + .build(); + + IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); + + final SimpleResultSyncPoint geoLocationReceived = new SimpleResultSyncPoint(); + + final PepEventListener geoLocationListener = (jid, geoLocation, id, message) -> { + if (geoLocation.equals(data)) { + geoLocationReceived.signal(); + } + }; + + // TODO Ensure that pre-existing filtering notification excludes geolocation. + try { + // Publish the data + publishAndWait(glm1, ServiceDiscoveryManager.getInstanceFor(conOne), data); + + // Adds listener, which implicitly publishes a disco/info filter for geolocation notification. + registerListenerAndWait(glm2, ServiceDiscoveryManager.getInstanceFor(conTwo), geoLocationListener); + + // Wait for the data to be received. + try { + Object result = geoLocationReceived.waitForResult(timeout); + + // Explicitly assert the success case. + Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } + } finally { + unregisterListener(glm2, geoLocationListener); + } + } + + /** + * Registers a listener for GeoLocation data. This implicitly publishes a CAPS update to include a notification + * filter for the geolocation node. This method blocks until the server has indicated that this update has been + * received. + * + * @param geoManager The GeoLocationManager instance for the connection that is expected to receive data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param listener A listener instance for GeoLocation data that is to be registered. + * + * @throws Exception if the test fails + */ + public void registerListenerAndWait(GeoLocationManager geoManager, ServiceDiscoveryManager discoManager, PepEventListener listener) throws Exception { + final SimpleResultSyncPoint notificationFilterReceived = new SimpleResultSyncPoint(); + final EntityCapabilitiesChangedListener notificationFilterReceivedListener = info -> { + if (info.containsFeature(GeoLocationManager.GEOLOCATION_NODE + "+notify")) { + notificationFilterReceived.signal(); + } + }; + + discoManager.addEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + try { + geoManager.addGeoLocationListener(listener); + notificationFilterReceived.waitForResult(timeout); + } finally { + discoManager.removeEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + } + } + + /** + * The functionally reverse of {@link #registerListenerAndWait(GeoLocationManager, ServiceDiscoveryManager, PepEventListener)} + * with the difference of not being a blocking operation. + * + * @param geoManager The GeoLocationManager instance for the connection that was expected to receive data. + * @param listener A listener instance for GeoLocation data that is to be removed. + */ + public void unregisterListener(GeoLocationManager geoManager, PepEventListener listener) { + // Does it make sense to have a method implementation that's one line? This is provided to allow for symmetry in the API. + geoManager.removeGeoLocationListener(listener); + } + + /** + * Publish data using PEP, and block until the server has echoed the publication back to the publishing user. + * + * @param geoManager The GeoLocationManager instance for the connection that is expected to publish data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param data The data to be published. + * + * @throws Exception if the test fails + */ + public void publishAndWait(GeoLocationManager geoManager, ServiceDiscoveryManager discoManager, GeoLocation data) throws Exception { + final SimpleResultSyncPoint publicationEchoReceived = new SimpleResultSyncPoint(); + final PepEventListener publicationEchoListener = (jid, geoLocation, id, message) -> { + if (geoLocation.equals(data)) { + publicationEchoReceived.signal(); + } + }; + try { + registerListenerAndWait(geoManager, discoManager, publicationEchoListener); + geoManager.addGeoLocationListener(publicationEchoListener); + geoManager.publishGeoLocation(data); + } finally { + geoManager.removeGeoLocationListener(publicationEchoListener); + } } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index d2eff1080..1a83085dc 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -16,9 +16,13 @@ */ package org.jivesoftware.smackx.mood; +import java.util.concurrent.TimeoutException; + import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.disco.EntityCapabilitiesChangedListener; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.mood.element.MoodElement; import org.jivesoftware.smackx.pep.PepEventListener; @@ -28,6 +32,7 @@ import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; +import org.junit.jupiter.api.Assertions; public class MoodIntegrationTest extends AbstractSmackIntegrationTest { @@ -40,32 +45,155 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { mm2 = MoodManager.getInstanceFor(conTwo); } - @SmackIntegrationTest - public void test() throws Exception { - IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); - - final SimpleResultSyncPoint moodReceived = new SimpleResultSyncPoint(); - - final PepEventListener moodListener = (jid, moodElement, id, message) -> { - if (moodElement.getMood() == Mood.satisfied) { - moodReceived.signal(); - } - }; - mm2.addMoodListener(moodListener); - - try { - mm1.setMood(Mood.satisfied); - - moodReceived.waitForResult(timeout); - } finally { - mm2.removeMoodListener(moodListener); - } - } - @AfterClass public void unsubscribe() throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } + + /** + * Verifies that a notification is sent when a publication is received, assuming that notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ + @SmackIntegrationTest + public void testNotification() throws Exception { + Mood data = Mood.satisfied; + + IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); + + final SimpleResultSyncPoint moodReceived = new SimpleResultSyncPoint(); + + final PepEventListener moodListener = (jid, moodElement, id, message) -> { + if (moodElement.getMood().equals(data)) { + moodReceived.signal(); + } + }; + + try { + // Register ConTwo's interest in receiving mood notifications, and wait for that interest to have been propagated. + registerListenerAndWait(mm2, ServiceDiscoveryManager.getInstanceFor(conTwo), moodListener); + + // Publish the data. + mm1.setMood(data); // for the purpose of this test, this needs not be blocking/use publishAndWait(); + + // Wait for the data to be received. + try { + moodReceived.waitForResult(timeout); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } + } finally { + unregisterListener(mm2, moodListener); + } + } + + /** + * Verifies that a notification for a previously sent publication is received as soon as notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ + @SmackIntegrationTest + public void testNotificationAfterFilterChange() throws Exception { + Mood data = Mood.cautious; + + IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); + + final SimpleResultSyncPoint moodReceived = new SimpleResultSyncPoint(); + + final PepEventListener moodListener = (jid, moodElement, id, message) -> { + if (moodElement.getMood().equals(data)) { + moodReceived.signal(); + } + }; + + // TODO Ensure that pre-existing filtering notification excludes mood. + try { + // Publish the data + publishAndWait(mm1, ServiceDiscoveryManager.getInstanceFor(conOne), data); + + // Adds listener, which implicitly publishes a disco/info filter for mood notification. + registerListenerAndWait(mm2, ServiceDiscoveryManager.getInstanceFor(conTwo), moodListener); + + // Wait for the data to be received. + try { + Object result = moodReceived.waitForResult(timeout); + + // Explicitly assert the success case. + Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } + } finally { + unregisterListener(mm2, moodListener); + } + } + + /** + * Registers a listener for User Tune data. This implicitly publishes a CAPS update to include a notification + * filter for the mood node. This method blocks until the server has indicated that this update has been + * received. + * + * @param moodManager The MoodManager instance for the connection that is expected to receive data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param listener A listener instance for Mood data that is to be registered. + * + * @throws Exception if the test fails + */ + public void registerListenerAndWait(MoodManager moodManager, ServiceDiscoveryManager discoManager, PepEventListener listener) throws Exception { + final SimpleResultSyncPoint notificationFilterReceived = new SimpleResultSyncPoint(); + final EntityCapabilitiesChangedListener notificationFilterReceivedListener = info -> { + if (info.containsFeature(MoodManager.MOOD_NODE + "+notify")) { + notificationFilterReceived.signal(); + } + }; + + discoManager.addEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + try { + moodManager.addMoodListener(listener); + notificationFilterReceived.waitForResult(timeout); + } finally { + discoManager.removeEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + } + } + + /** + * The functionally reverse of {@link #registerListenerAndWait(MoodManager, ServiceDiscoveryManager, PepEventListener)} + * with the difference of not being a blocking operation. + * + * @param moodManager The MoodManager instance for the connection that was expected to receive data. + * @param listener A listener instance for Mood data that is to be removed. + */ + public void unregisterListener(MoodManager moodManager, PepEventListener listener) { + // Does it make sense to have a method implementation that's one line? This is provided to allow for symmetry in the API. + moodManager.removeMoodListener(listener); + } + + /** + * Publish data using PEP, and block until the server has echoed the publication back to the publishing user. + * + * @param moodManager The MoodManager instance for the connection that is expected to publish data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param data The data to be published. + * + * @throws Exception if the test fails + */ + public void publishAndWait(MoodManager moodManager, ServiceDiscoveryManager discoManager, Mood data) throws Exception { + final SimpleResultSyncPoint publicationEchoReceived = new SimpleResultSyncPoint(); + final PepEventListener publicationEchoListener = (jid, moodElement, id, message) -> { + if (moodElement.getMood().equals(data)) { + publicationEchoReceived.signal(); + } + }; + try { + registerListenerAndWait(moodManager, discoManager, publicationEchoListener); + moodManager.addMoodListener(publicationEchoListener); + moodManager.setMood(data); + } finally { + moodManager.removeMoodListener(publicationEchoListener); + } + } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java index 0309f9af0..ac3fa0ac0 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java @@ -17,12 +17,14 @@ package org.jivesoftware.smackx.usertune; import java.net.URI; +import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.disco.EntityCapabilitiesChangedListener; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.usertune.element.UserTuneElement; @@ -32,7 +34,7 @@ import org.igniterealtime.smack.inttest.annotations.AfterClass; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.jxmpp.jid.EntityBareJid; +import org.junit.jupiter.api.Assertions; public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { @@ -45,46 +47,176 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { utm2 = UserTuneManager.getInstanceFor(conTwo); } - @SmackIntegrationTest - public void test() throws Exception { - URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#9"); - UserTuneElement.Builder builder = UserTuneElement.getBuilder(); - UserTuneElement userTuneElement1 = builder.setArtist("Yes") - .setLength(686) - .setRating(8) - .setSource("Yessongs") - .setTitle("Heart of the Sunrise") - .setTrack("3") - .setUri(uri) - .build(); - - IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); - - final SimpleResultSyncPoint userTuneReceived = new SimpleResultSyncPoint(); - - final PepEventListener userTuneListener = new PepEventListener() { - @Override - public void onPepEvent(EntityBareJid jid, UserTuneElement userTuneElement, String id, Message message) { - if (userTuneElement.equals(userTuneElement1)) { - userTuneReceived.signal(); - } - } - }; - - utm2.addUserTuneListener(userTuneListener); - - try { - utm1.publishUserTune(userTuneElement1); - userTuneReceived.waitForResult(timeout); - } finally { - utm2.removeUserTuneListener(userTuneListener); - } - } - @AfterClass public void unsubscribe() throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); } + + /** + * Verifies that a notification is sent when a publication is received, assuming that notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ + @SmackIntegrationTest + public void testNotification() throws Exception { + URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#9"); + UserTuneElement.Builder builder = UserTuneElement.getBuilder(); + UserTuneElement data = builder.setArtist("Yes") + .setLength(686) + .setRating(8) + .setSource("Yessongs") + .setTitle("Heart of the Sunrise") + .setTrack("3") + .setUri(uri) + .build(); + + IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); + + final SimpleResultSyncPoint userTuneReceived = new SimpleResultSyncPoint(); + + final PepEventListener userTuneListener = (jid, userTune, id, message) -> { + if (userTune.equals(data)) { + userTuneReceived.signal(); + } + }; + + try { + // Register ConTwo's interest in receiving user tune notifications, and wait for that interest to have been propagated. + registerListenerAndWait(utm2, ServiceDiscoveryManager.getInstanceFor(conTwo), userTuneListener); + + // Publish the data. + utm1.publishUserTune(data); // for the purpose of this test, this needs not be blocking/use publishAndWait(); + + // Wait for the data to be received. + try { + Object result = userTuneReceived.waitForResult(timeout); + + // Explicitly assert the success case. + Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } + } finally { + unregisterListener(utm2, userTuneListener); + } + } + + /** + * Verifies that a notification for a previously sent publication is received as soon as notification filtering + * has been adjusted to allow for the notification to be delivered. + * + * @throws Exception if the test fails + */ + @SmackIntegrationTest + public void testNotificationAfterFilterChange() throws Exception { + URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#8"); + UserTuneElement.Builder builder = UserTuneElement.getBuilder(); + UserTuneElement data = builder.setArtist("No") + .setLength(306) + .setRating(3) + .setSource("NoSongs") + .setTitle("Sunrise of the Heart") + .setTrack("2") + .setUri(uri) + .build(); + + IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout); + + final SimpleResultSyncPoint userTuneReceived = new SimpleResultSyncPoint(); + + final PepEventListener userTuneListener = (jid, userTune, id, message) -> { + if (userTune.equals(data)) { + userTuneReceived.signal(); + } + }; + + // TODO Ensure that pre-existing filtering notification excludes userTune. + try { + // Publish the data + publishAndWait(utm1, ServiceDiscoveryManager.getInstanceFor(conOne), data); + + // Adds listener, which implicitly publishes a disco/info filter for userTune notification. + registerListenerAndWait(utm2, ServiceDiscoveryManager.getInstanceFor(conTwo), userTuneListener); + + // Wait for the data to be received. + try { + Object result = userTuneReceived.waitForResult(timeout); + + // Explicitly assert the success case. + Assertions.assertNotNull(result, "Expected to receive a PEP notification, but did not."); + } catch (TimeoutException e) { + Assertions.fail("Expected to receive a PEP notification, but did not."); + } + } finally { + unregisterListener(utm2, userTuneListener); + } + } + + /** + * Registers a listener for User Tune data. This implicitly publishes a CAPS update to include a notification + * filter for the usertune node. This method blocks until the server has indicated that this update has been + * received. + * + * @param userTuneManager The UserTuneManager instance for the connection that is expected to receive data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param listener A listener instance for UserTune data that is to be registered. + * + * @throws Exception if the test fails + */ + public void registerListenerAndWait(UserTuneManager userTuneManager, ServiceDiscoveryManager discoManager, PepEventListener listener) throws Exception { + final SimpleResultSyncPoint notificationFilterReceived = new SimpleResultSyncPoint(); + final EntityCapabilitiesChangedListener notificationFilterReceivedListener = info -> { + if (info.containsFeature(UserTuneManager.USERTUNE_NODE + "+notify")) { + notificationFilterReceived.signal(); + } + }; + + discoManager.addEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + try { + userTuneManager.addUserTuneListener(listener); + notificationFilterReceived.waitForResult(timeout); + } finally { + discoManager.removeEntityCapabilitiesChangedListener(notificationFilterReceivedListener); + } + } + + /** + * The functionally reverse of {@link #registerListenerAndWait(UserTuneManager, ServiceDiscoveryManager, PepEventListener)} + * with the difference of not being a blocking operation. + * + * @param userTuneManager The UserTuneManager instance for the connection that was expected to receive data. + * @param listener A listener instance for UserTune data that is to be removed. + */ + public void unregisterListener(UserTuneManager userTuneManager, PepEventListener listener) { + // Does it make sense to have a method implementation that's one line? This is provided to allow for symmetry in the API. + userTuneManager.removeUserTuneListener(listener); + } + + /** + * Publish data using PEP, and block until the server has echoed the publication back to the publishing user. + * + * @param userTuneManager The UserTuneManager instance for the connection that is expected to publish data. + * @param discoManager The ServiceDiscoveryManager instance for the connection that is expected to publish data. + * @param data The data to be published. + * + * @throws Exception if the test fails + */ + public void publishAndWait(UserTuneManager userTuneManager, ServiceDiscoveryManager discoManager, UserTuneElement data) throws Exception { + final SimpleResultSyncPoint publicationEchoReceived = new SimpleResultSyncPoint(); + final PepEventListener publicationEchoListener = (jid, userTune, id, message) -> { + if (userTune.equals(data)) { + publicationEchoReceived.signal(); + } + }; + try { + registerListenerAndWait(userTuneManager, discoManager, publicationEchoListener); + userTuneManager.addUserTuneListener(publicationEchoListener); + userTuneManager.publishUserTune(data); + } finally { + userTuneManager.removeUserTuneListener(publicationEchoListener); + } + } }