diff --git a/documentation/developer/building.md b/documentation/developer/building.md index 84163fc7f..2e35e634b 100644 --- a/documentation/developer/building.md +++ b/documentation/developer/building.md @@ -48,3 +48,16 @@ git clone git@github.com:igniterealtime/Smack.git cd Smack gradle assemble ``` + +IDE Config +---------- + +### Eclipse + +Import IDE settings from `./resources/eclipse/` to configure proper ordering of imports and correct formatting that should pass the CheckStyle rules. + +### IntelliJ IDEA + +Import Java Code Style settings from `./resources/intellij/smack_formatter.xml` to configure import optimisation and code formatting to pass the CheckStyle rules when building or submitting PRs. + +_We've noticed, at time of writing, that IntelliJ often requires a restart when applying new rules - no amount of OK/Apply will do the trick._ diff --git a/documentation/developer/integrationtest.md b/documentation/developer/integrationtest.md index fe006b58c..60f3142b8 100644 --- a/documentation/developer/integrationtest.md +++ b/documentation/developer/integrationtest.md @@ -4,8 +4,8 @@ Smack's Integration Test Framework Introduction ------------ -Smack's Integration Test Framwork is used to run a set of tests against a real XMPP service. -The framework discovers on startup the available tests by reflection. +Smack's Integration Test Framework is used to run a set of tests against a real XMPP service. +The framework discovers on start-up the available tests by reflection. Quickstart ---------- @@ -120,7 +120,7 @@ The methods are supposed to throw an exception if their integration test fails. ### `TestNotPossibleException` -Can be thrown by test methods or constructors to signal that their test it no possible, e.g. because the service does not support the required feature. +Can be thrown by test methods or constructors to signal that their test is not possible, e.g. because the service does not support the required feature. Running the integration tests ----------------------------- @@ -130,6 +130,7 @@ Smack's Gradle build system is configured with a special task called `integratio ```bash $ gradle integrationTest -Dsinttest.service=my.xmppservice.org ``` + If one of `accountOneUsername`, `accountOnePassword`, `accountTwoUsername` or `accountTwoPassword` is not configured, then the framework will automatically create the accounts on the service. Of course this requires account registration (IBR) to be enabled. If the accounts got created automatically by the framework, then they will also be deleted at the end of the test. @@ -161,6 +162,19 @@ The test methods can declare as many parameters as they need to, but every param The framework will automatically create, register and login the connections. After the test is finished, the connections will be unregistered with the XMPP service and terminated. +Debugging Integration Tests +------------------------------ + +A test, like any other code, may not be perfect on the first attempt, and you may require more information in order to ascertain quite what's wrong. + +### Smack Debugger options + +As listed in the main Smack [Debugging](../debugging.md) doc, there are two built-in debuggers that could surface you more information. Using the 'enhanced' debugger config option listed above, you'll get the Smack Debug Window launching when your tests launch, and you'll get a stanza-by-stanza account of what happened on each connection, hopefully enough to diagnose what went wrong. + +### Debugging in the IDE + +If the output isn't enough, you may need to debug and inspect running code within the IDE. Depending on the IDE, in order to get execution to pause at your breakpoints, you may need to switch your configuration. Instead of running `gradle integrationTest`, instead run the `SmackIntegrationTestFramework` class directly with the same command-line options. + Running your own integration tests ---------------------------------- diff --git a/documentation/extensions/muc.md b/documentation/extensions/muc.md index a89f2b22f..40d7e2024 100644 --- a/documentation/extensions/muc.md +++ b/documentation/extensions/muc.md @@ -472,7 +472,7 @@ muc = manager.getMultiUserChat("myroom@conference.jabber.org"); muc.create("testbot"); // User1 (which is the room owner) configures the room as a moderated room Form form = muc.getConfigurationForm(); -Form answerForm = form.createAnswerForm(); +FillableForm answerForm = configForm.getFillableForm(); answerForm.setAnswer("muc#roomconfig_moderatedroom", "1"); muc.sendConfigurationForm(answerForm); @@ -612,7 +612,7 @@ muc = manager.getMultiUserChat("myroom@conference.jabber.org"); muc.create("testbot"); // User1 (which is the room owner) configures the room as a moderated room Form form = muc.getConfigurationForm(); -Form answerForm = form.createAnswerForm(); +FillableForm answerForm = configForm.getFillableForm(); answerForm.setAnswer("muc#roomconfig_moderatedroom", "1"); muc.sendConfigurationForm(answerForm); diff --git a/resources/intellij/Smack Import Order.xml b/resources/intellij/Smack Import Order.xml deleted file mode 100644 index 3209e4f5f..000000000 --- a/resources/intellij/Smack Import Order.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/resources/intellij/smack_formatter.xml b/resources/intellij/smack_formatter.xml new file mode 100644 index 000000000..961dda2b6 --- /dev/null +++ b/resources/intellij/smack_formatter.xml @@ -0,0 +1,51 @@ + + \ No newline at end of file diff --git a/resources/releasedocs/changelog.html b/resources/releasedocs/changelog.html index eda807c81..8d8e20366 100644 --- a/resources/releasedocs/changelog.html +++ b/resources/releasedocs/changelog.html @@ -141,6 +141,17 @@ hr {
+

4.4.3 -- 2021-07-06

+ +

Bug +

+ +

4.4.2 -- 2021-03-25

Bug diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java index 03982d647..74e20ba50 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java @@ -54,7 +54,7 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn } try { - initialStateDescriptorVertex = StateDescriptorGraph.constructStateDescriptorGraph(backwardEdgeStateDescriptors); + initialStateDescriptorVertex = StateDescriptorGraph.constructStateDescriptorGraph(backwardEdgeStateDescriptors, builder.failOnUnknownStates); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { @@ -91,6 +91,8 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn private final Map, ModularXmppClientToServerConnectionModuleDescriptor> modulesDescriptors = new HashMap<>(); + private boolean failOnUnknownStates; + private Builder() { SmackConfiguration.addAllKnownModulesTo(this); } @@ -163,6 +165,17 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn return getThis(); } + /** + * Fail if there are unknown states in Smack's state descriptor graph. This method is used mostly for testing + * the internals of Smack. Users can safely ignore it. + * + * @return a reference to this builder. + */ + public Builder failOnUnknownStates() { + failOnUnknownStates = true; + return getThis(); + } + @Override protected Builder getThis() { return this; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java index bbece8898..35144f73a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Logger; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.DisconnectedStateDescriptor; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; @@ -47,8 +46,6 @@ import org.jivesoftware.smack.util.MultiMap; */ public class StateDescriptorGraph { - private static final Logger LOGGER = Logger.getLogger(StateDescriptorGraph.class.getName()); - private static GraphVertex addNewStateDescriptorGraphVertex( Class stateDescriptorClass, Map, GraphVertex> graphVertexes) @@ -102,7 +99,8 @@ public class StateDescriptorGraph { } private static void handleStateDescriptorGraphVertex(GraphVertex node, - HandleStateDescriptorGraphVertexContext context) + HandleStateDescriptorGraphVertexContext context, + boolean failOnUnknownStates) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { Class stateDescriptorClass = node.element.getClass(); boolean alreadyHandled = context.recurseInto(stateDescriptorClass); @@ -126,7 +124,7 @@ public class StateDescriptorGraph { case 1: GraphVertex soleSuccessorNode = successorStateDescriptors.values().iterator().next(); node.addOutgoingEdge(soleSuccessorNode); - handleStateDescriptorGraphVertex(soleSuccessorNode, context); + handleStateDescriptorGraphVertex(soleSuccessorNode, context, failOnUnknownStates); return; } @@ -144,9 +142,8 @@ public class StateDescriptorGraph { StateDescriptor successorStateDescriptor = successorStateDescriptorGraphNode.element; Class successorStateDescriptorClass = successorStateDescriptor.getClass(); for (Class subordinateClass : successorStateDescriptor.getSubordinates()) { - if (!successorClasses.contains(subordinateClass)) { - LOGGER.severe(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set"); - continue; + if (failOnUnknownStates && !successorClasses.contains(subordinateClass)) { + throw new IllegalStateException(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set"); } GraphVertex> superiorClassNode = lookupAndCreateIfRequired( @@ -157,10 +154,9 @@ public class StateDescriptorGraph { superiorClassNode.addOutgoingEdge(subordinateClassNode); } for (Class superiorClass : successorStateDescriptor.getSuperiors()) { - if (!successorClasses.contains(superiorClass)) { - LOGGER.severe(successorStateDescriptor + " points to a superior '" + superiorClass + if (failOnUnknownStates && !successorClasses.contains(superiorClass)) { + throw new IllegalStateException(successorStateDescriptor + " points to a superior '" + superiorClass + "' which is not part of the successor set"); - continue; } GraphVertex> subordinateClassNode = lookupAndCreateIfRequired( @@ -193,11 +189,13 @@ public class StateDescriptorGraph { node.addOutgoingEdge(successorVertex); // Recurse further. - handleStateDescriptorGraphVertex(successorVertex, context); + handleStateDescriptorGraphVertex(successorVertex, context, failOnUnknownStates); } } - public static GraphVertex constructStateDescriptorGraph(Set> backwardEdgeStateDescriptors) + public static GraphVertex constructStateDescriptorGraph( + Set> backwardEdgeStateDescriptors, + boolean failOnUnknownStates) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { Map, GraphVertex> graphVertexes = new HashMap<>(); @@ -219,7 +217,7 @@ public class StateDescriptorGraph { } HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges); - handleStateDescriptorGraphVertex(initialNode, context); + handleStateDescriptorGraphVertex(initialNode, context, failOnUnknownStates); return initialNode; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java index b35dfb4a0..24b73b8b0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,4 +82,11 @@ public class CollectionUtil { } return new HashSet<>(collection); } + + public static List emptyOrSingletonListFrom(T element) { + if (element == null) { + return Collections.emptyList(); + } + return Collections.singletonList(element); + } } diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java index 9822b59f9..5755207d8 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java @@ -140,12 +140,12 @@ public final class EnhancedDebuggerWindow { if (frame == null) { createDebug(); } - debugger.tabbedPane.setName("XMPPConnection_" + tabbedPane.getComponentCount()); - tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1); + debugger.tabbedPane.setName("XMPPConnection_" + tabbedPane.getTabCount()); + tabbedPane.add(debugger.tabbedPane, -1); tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon); tabbedPane.setSelectedIndex(tabbedPane.indexOfComponent(debugger.tabbedPane)); frame.setTitle( - "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1)); + "Smack Debug Window -- Total connections: " + (tabbedPane.getTabCount() - 1)); // Keep the added debugger for later access debuggers.add(debugger); } @@ -286,7 +286,7 @@ public final class EnhancedDebuggerWindow { @Override public void actionPerformed(ActionEvent e) { // Remove the selected tab pane if it's not the Smack info pane - if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) { + if (tabbedPane.getSelectedIndex() < tabbedPane.getTabCount() - 1) { int index = tabbedPane.getSelectedIndex(); // Notify to the debugger to stop debugging EnhancedDebugger debugger = debuggers.get(index); @@ -297,7 +297,7 @@ public final class EnhancedDebuggerWindow { // Update the root window title frame.setTitle( "Smack Debug Window -- Total connections: " - + (tabbedPane.getComponentCount() - 1)); + + (tabbedPane.getTabCount() - 1)); } } }); @@ -309,7 +309,7 @@ public final class EnhancedDebuggerWindow { public void actionPerformed(ActionEvent e) { ArrayList debuggersToRemove = new ArrayList<>(); // Remove all the debuggers of which their connections are no longer valid - for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) { + for (int index = 0; index < tabbedPane.getTabCount() - 1; index++) { EnhancedDebugger debugger = debuggers.get(index); if (!debugger.isConnectionActive()) { // Notify to the debugger to stop debugging @@ -325,7 +325,7 @@ public final class EnhancedDebuggerWindow { // Update the root window title frame.setTitle( "Smack Debug Window -- Total connections: " - + (tabbedPane.getComponentCount() - 1)); + + (tabbedPane.getTabCount() - 1)); } }); menu.add(menuItem); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/provider/IoTSetRequestProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/provider/IoTSetRequestProvider.java index 029e1f15e..952ebcdd8 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/provider/IoTSetRequestProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/provider/IoTSetRequestProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016-2019 Florian Schmaus + * Copyright © 2016-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,9 +39,9 @@ public class IoTSetRequestProvider extends IQProvider { List data = new ArrayList<>(4); outerloop: while (true) { final XmlPullParser.Event eventType = parser.next(); - final String name = parser.getName(); switch (eventType) { case START_ELEMENT: + final String name = parser.getName(); switch (name) { case "bool": { String valueName = parser.getAttributeValue(null, "name"); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/provider/IoTFieldsExtensionProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/provider/IoTFieldsExtensionProvider.java index c6d28c370..82a0e866f 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/provider/IoTFieldsExtensionProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/data/provider/IoTFieldsExtensionProvider.java @@ -110,9 +110,9 @@ public class IoTFieldsExtensionProvider extends ExtensionElementProvider fields = new ArrayList<>(); outerloop: while (true) { final XmlPullParser.Event eventType = parser.next(); - final String name = parser.getName(); switch (eventType) { case START_ELEMENT: + final String name = parser.getName(); IoTDataField field = null; final String fieldName = parser.getAttributeValue(null, "name"); final String fieldValue = parser.getAttributeValue(null, "value"); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamPrefsIQProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamPrefsIQProvider.java index 632ae4946..2b4795036 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamPrefsIQProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamPrefsIQProvider.java @@ -59,9 +59,9 @@ public class MamPrefsIQProvider extends IQProvider { outerloop: while (true) { final XmlPullParser.Event eventType = parser.next(); - final String name = parser.getName(); switch (eventType) { case START_ELEMENT: + final String name = parser.getName(); switch (name) { case "always": alwaysJids = iterateJids(parser); @@ -92,9 +92,9 @@ public class MamPrefsIQProvider extends IQProvider { outerloop: while (true) { final XmlPullParser.Event eventType = parser.next(); - final String name = parser.getName(); switch (eventType) { case START_ELEMENT: + final String name = parser.getName(); switch (name) { case "jid": parser.next(); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamQueryIQProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamQueryIQProvider.java index 853f199f4..d0971c5de 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamQueryIQProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamQueryIQProvider.java @@ -47,10 +47,10 @@ public class MamQueryIQProvider extends IQProvider { outerloop: while (true) { final XmlPullParser.Event eventType = parser.next(); - final String name = parser.getName(); switch (eventType) { case START_ELEMENT: + final String name = parser.getName(); switch (name) { case DataForm.ELEMENT: dataForm = DataFormProvider.INSTANCE.parse(parser); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamResultProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamResultProvider.java index 430b628a1..21012f873 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamResultProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/provider/MamResultProvider.java @@ -49,9 +49,9 @@ public class MamResultProvider extends ExtensionElementProvider recipients = new ArrayList<>(numRecipients); - if (to != null) { - recipients.addAll(to); - } - if (cc != null) { - recipients.addAll(cc); - } - if (bcc != null) { - recipients.addAll(bcc); - } + recipients.addAll(to); + recipients.addAll(cc); + recipients.addAll(bcc); final List stanzasToSend = new ArrayList<>(numRecipients); for (Jid recipient : recipients) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java index d376fab97..52fdf44bb 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java @@ -55,8 +55,8 @@ public class BytestreamsProvider extends IQProvider { String elementName; while (!done) { eventType = parser.next(); - elementName = parser.getName(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + elementName = parser.getName(); if (elementName.equals(Bytestream.StreamHost.ELEMENT)) { JID = ParserUtils.getJidAttribute(parser); host = parser.getAttributeValue("", "host"); @@ -70,6 +70,7 @@ public class BytestreamsProvider extends IQProvider { } } else if (eventType == XmlPullParser.Event.END_ELEMENT) { + elementName = parser.getName(); if (elementName.equals("streamhost")) { if (port == null) { toReturn.addStreamHost(JID, host); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java index b134427ed..034d0f9dd 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java @@ -700,7 +700,7 @@ public final class EntityCapsManager extends Manager { for (FormField f : fs) { sb.append(f.getFieldName()); sb.append('<'); - formFieldValuesToCaps(f.getValues(), sb); + formFieldValuesToCaps(f.getRawValues(), sb); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java index 4aeb9a700..13cb0b3cb 100755 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/commands/provider/AdHocCommandDataProvider.java @@ -78,9 +78,9 @@ public class AdHocCommandDataProvider extends IQProvider { } while (!done) { eventType = parser.next(); - elementName = parser.getName(); namespace = parser.getNamespace(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + elementName = parser.getName(); if (parser.getName().equals("actions")) { String execute = parser.getAttributeValue("", "execute"); if (execute != null) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/provider/JingleS5BTransportProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/provider/JingleS5BTransportProvider.java index 7f85961db..c88651661 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/provider/JingleS5BTransportProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_s5b/provider/JingleS5BTransportProvider.java @@ -31,7 +31,6 @@ import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; -import org.jivesoftware.smackx.jingle.element.JingleContentTransport; import org.jivesoftware.smackx.jingle.element.JingleContentTransportCandidate; import org.jivesoftware.smackx.jingle.provider.JingleContentTransportProvider; import org.jivesoftware.smackx.jingle.transports.jingle_s5b.elements.JingleS5BTransport; @@ -62,9 +61,9 @@ public class JingleS5BTransportProvider extends JingleContentTransportProvider { outerloop: while (true) { XmlPullParser.Event tag = parser.next(); - String name = parser.getName(); - String namespace = parser.getNamespace(); switch (tag) { case START_ELEMENT: + String name = parser.getName(); + String namespace = parser.getNamespace(); if (MoodElement.ELEM_TEXT.equals(name)) { text = parser.nextText(); continue outerloop; @@ -74,7 +74,7 @@ public class MoodProvider extends ExtensionElementProvider { } case END_ELEMENT: - if (MoodElement.ELEMENT.equals(name)) { + if (MoodElement.ELEMENT.equals(parser.getName())) { MoodElement.MoodSubjectElement subjectElement = (mood == null && concretisation == null) ? null : new MoodElement.MoodSubjectElement(mood, concretisation); return new MoodElement(subjectElement, text); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java index f1719682c..6da4cafc9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/si/provider/StreamInitiationProvider.java @@ -46,8 +46,6 @@ public class StreamInitiationProvider extends IQProvider { @Override public StreamInitiation parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { - boolean done = false; - // si String id = parser.getAttributeValue("", "id"); String mimeType = parser.getAttributeValue("", "mime-type"); @@ -66,13 +64,11 @@ public class StreamInitiationProvider extends IQProvider { DataForm form = null; DataFormProvider dataFormProvider = new DataFormProvider(); - String elementName; - String namespace; - while (!done) { + outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); - elementName = parser.getName(); - namespace = parser.getNamespace(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); if (elementName.equals("file")) { name = parser.getAttributeValue("", "name"); size = parser.getAttributeValue("", "size"); @@ -87,9 +83,10 @@ public class StreamInitiationProvider extends IQProvider { form = dataFormProvider.parse(parser); } } else if (eventType == XmlPullParser.Event.END_ELEMENT) { - if (elementName.equals("si")) { - done = true; - } else if (elementName.equals("file")) { + if (parser.getDepth() == initialDepth) { + break outerloop; + } + if (parser.getName().equals("file")) { long fileSize = 0; if (size != null && size.trim().length() != 0) { try { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java index 31c778a87..4b1c092b3 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractMultiFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus + * Copyright 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,12 @@ public class AbstractMultiFormField extends FormField { private final List values; + private final List rawValues; + protected AbstractMultiFormField(Builder builder) { super(builder); values = CollectionUtil.cloneAndSeal(builder.values); + rawValues = CollectionUtil.cloneAndSeal(builder.rawValues); } @Override @@ -39,11 +42,16 @@ public class AbstractMultiFormField extends FormField { return values; } + @Override + public final List getRawValues() { + return rawValues; + } - public abstract static class Builder> + public abstract static class Builder> extends FormField.Builder { private List values; + private List rawValues; protected Builder(AbstractMultiFormField formField) { super(formField); @@ -57,6 +65,7 @@ public class AbstractMultiFormField extends FormField { private void ensureValuesAreInitialized() { if (values == null) { values = new ArrayList<>(); + rawValues = new ArrayList<>(); } } @@ -70,7 +79,9 @@ public class AbstractMultiFormField extends FormField { public B addValueVerbatim(CharSequence value) { ensureValuesAreInitialized(); - values.add(value.toString()); + String valueString = value.toString(); + values.add(valueString); + rawValues.add(valueString); return getThis(); } @@ -83,7 +94,7 @@ public class AbstractMultiFormField extends FormField { ensureValuesAreInitialized(); for (CharSequence value : values) { - this.values.add(value.toString()); + addValueVerbatim(value); } return getThis(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java index 27fca4cde..4530d3968 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/AbstractSingleStringValueFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,8 @@ public class AbstractSingleStringValueFormField extends SingleValueFormField { return Integer.valueOf(value); } - public abstract static class Builder> extends FormField.Builder { + public abstract static class Builder> + extends SingleValueFormField.Builder { private String value; @@ -74,18 +75,16 @@ public class AbstractSingleStringValueFormField extends SingleValueFormField { } public B setValue(CharSequence value) { - this.value = value.toString(); + this.rawValue = this.value = value.toString(); return getThis(); } public B setValue(Enum value) { - this.value = value.toString(); - return getThis(); + return setValue(value.toString()); } public B setValue(int value) { - this.value = Integer.toString(value); - return getThis(); + return setValue(Integer.toString(value)); } public B setValue(URL value) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java index 2b036b162..e87278ae9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/BooleanFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class BooleanFormField extends SingleValueFormField { return new Builder(this); } - public static final class Builder extends FormField.Builder { + public static final class Builder extends SingleValueFormField.Builder { private Boolean value; private Builder(BooleanFormField booleanFormField) { @@ -57,6 +57,7 @@ public class BooleanFormField extends SingleValueFormField { @Override protected void resetInternal() { + super.resetInternal(); value = null; } @@ -74,11 +75,13 @@ public class BooleanFormField extends SingleValueFormField { } public Builder setValue(CharSequence value) { - boolean valueBoolean = ParserUtils.parseXmlBoolean(value.toString()); + rawValue = value.toString(); + boolean valueBoolean = ParserUtils.parseXmlBoolean(rawValue); return setValue(valueBoolean); } public Builder setValue(boolean value) { + rawValue = Boolean.toString(value); this.value = value; return this; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java index 10a259acf..8f4e9aafd 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2019-2020 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2019-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -271,6 +271,8 @@ public abstract class FormField implements XmlElement { */ public abstract List getValues(); + public abstract List getRawValues(); + public boolean hasValueSet() { List values = getValues(); return !values.isEmpty(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidMultiFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidMultiFormField.java index e1e93c0d3..a5c869c18 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidMultiFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidMultiFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,14 +23,18 @@ import java.util.List; import org.jivesoftware.smack.util.CollectionUtil; import org.jxmpp.jid.Jid; +import org.jxmpp.jid.util.JidUtil; -public class JidMultiFormField extends FormField { +public final class JidMultiFormField extends FormField { private final List values; - protected JidMultiFormField(Builder builder) { + private final List rawValues; + + JidMultiFormField(Builder builder) { super(builder); values = CollectionUtil.cloneAndSeal(builder.values); + rawValues = CollectionUtil.cloneAndSeal(builder.rawValues); } @Override @@ -38,6 +42,11 @@ public class JidMultiFormField extends FormField { return values; } + @Override + public List getRawValues() { + return rawValues; + } + public Builder asBuilder() { return new Builder(this); } @@ -45,6 +54,8 @@ public class JidMultiFormField extends FormField { public static final class Builder extends FormField.Builder { private List values; + private List rawValues; + private Builder(JidMultiFormField jidMultiFormField) { super(jidMultiFormField); values = CollectionUtil.newListWith(jidMultiFormField.getValues()); @@ -57,18 +68,30 @@ public class JidMultiFormField extends FormField { private void ensureValuesAreInitialized() { if (values == null) { values = new ArrayList<>(); + rawValues = new ArrayList<>(); } } @Override protected void resetInternal() { values = null; + rawValues = null; } public Builder addValue(Jid jid) { + return addValue(jid, null); + } + + public Builder addValue(Jid jid, String rawValue) { + if (rawValue == null) { + rawValue = jid.toString(); + } + ensureValuesAreInitialized(); values.add(jid); + rawValues.add(rawValue); + return this; } @@ -76,6 +99,7 @@ public class JidMultiFormField extends FormField { ensureValuesAreInitialized(); values.addAll(jids); + rawValues.addAll(JidUtil.toStringList(jids)); return this; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidSingleFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidSingleFormField.java index d1b0237de..7948971a3 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidSingleFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/JidSingleFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ public class JidSingleFormField extends SingleValueFormField { return new Builder(this); } - public static final class Builder extends FormField.Builder { + public static final class Builder extends SingleValueFormField.Builder { private Jid value; private Builder(JidSingleFormField jidSingleFormField) { @@ -50,11 +50,21 @@ public class JidSingleFormField extends SingleValueFormField { @Override protected void resetInternal() { + super.resetInternal(); value = null; } public Builder setValue(Jid value) { + return setValue(value, null); + } + + public Builder setValue(Jid value, String rawValue) { this.value = value; + if (rawValue != null) { + this.rawValue = rawValue; + } else { + this.rawValue = value.toString(); + } return this; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/SingleValueFormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/SingleValueFormField.java index 2c039f1ab..796abd654 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/SingleValueFormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/SingleValueFormField.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,23 +19,35 @@ package org.jivesoftware.smackx.xdata; import java.util.Collections; import java.util.List; +import org.jivesoftware.smack.util.CollectionUtil; + public abstract class SingleValueFormField extends FormField { + private final String rawValue; + protected SingleValueFormField(Builder builder) { super(builder); + rawValue = builder.rawValue; } @Override public final List getValues() { CharSequence value = getValue(); - if (value == null) { - return Collections.emptyList(); - } - return Collections.singletonList(value); + return CollectionUtil.emptyOrSingletonListFrom(value); } public abstract CharSequence getValue(); + public final String getRawValue() { + return rawValue; + } + + @Override + public final List getRawValues() { + String rawValue = getRawValue(); + return CollectionUtil.emptyOrSingletonListFrom(rawValue); + } + @Override protected void populateExtraXmlChildElements() { CharSequence value = getValue(); @@ -45,4 +57,24 @@ public abstract class SingleValueFormField extends FormField { extraXmlChildElements = Collections.singletonList(new Value(value)); } + + public abstract static class Builder> + extends FormField.Builder { + + protected Builder(String fieldName, Type type) { + super(fieldName, type); + } + + protected Builder(FormField formField) { + super(formField); + } + + protected String rawValue; + + @Override + protected void resetInternal() { + rawValue = null; + }; + + } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java index 9954e8a2e..159c20544 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/form/FillableForm.java @@ -58,7 +58,13 @@ public class FillableForm extends FilledForm { if (formField.isRequired()) { String fieldName = formField.getFieldName(); requiredFields.add(fieldName); - missingRequiredFields.add(fieldName); + + if (formField.hasValueSet()) { + // This is a form field with a default value. + write(formField); + } else { + missingRequiredFields.add(fieldName); + } } } this.requiredFields = Collections.unmodifiableSet(requiredFields); @@ -223,9 +229,6 @@ public class FillableForm extends FilledForm { if (!getDataForm().hasField(fieldName)) { throw new IllegalArgumentException(); } - if (filledFields.containsKey(fieldName)) { - throw new IllegalArgumentException(); - } // Perform validation, e.g. using XEP-0122. // TODO: We could also perform list-* option validation, but this has to take xep122's into account. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/DataFormProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/DataFormProvider.java index 6465efacb..f6362660c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/DataFormProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/provider/DataFormProvider.java @@ -237,8 +237,9 @@ public class DataFormProvider extends ExtensionElementProvider { case jid_multi: JidMultiFormField.Builder jidMultiBuilder = FormField.jidMultiBuilder(fieldName); for (FormField.Value value : values) { - Jid jid = JidCreate.from(value.getValue()); - jidMultiBuilder.addValue(jid); + String rawValue = value.getValue().toString(); + Jid jid = JidCreate.from(rawValue); + jidMultiBuilder.addValue(jid, rawValue); } builder = jidMultiBuilder; break; @@ -246,9 +247,9 @@ public class DataFormProvider extends ExtensionElementProvider { ensureAtMostSingleValue(type, values); JidSingleFormField.Builder jidSingleBuilder = FormField.jidSingleBuilder(fieldName); if (!values.isEmpty()) { - CharSequence jidCharSequence = values.get(0).getValue(); - Jid jid = JidCreate.from(jidCharSequence); - jidSingleBuilder.setValue(jid); + String rawValue = values.get(0).getValue().toString(); + Jid jid = JidCreate.from(rawValue); + jidSingleBuilder.setValue(jid, rawValue); } builder = jidSingleBuilder; break; @@ -300,6 +301,7 @@ public class DataFormProvider extends ExtensionElementProvider { private static FormField.Builder parseBooleanFormField(String fieldName, List values) throws SmackParsingException { BooleanFormField.Builder builder = FormField.booleanBuilder(fieldName); + ensureAtMostSingleValue(builder.getType(), values); if (values.size() == 1) { String value = values.get(0).getValue().toString(); builder.setValue(value); @@ -321,7 +323,8 @@ public class DataFormProvider extends ExtensionElementProvider { private static AbstractMultiFormField.Builder parseMultiKindFormField(AbstractMultiFormField.Builder builder, List values) { for (FormField.Value value : values) { - builder.addValue(value.getValue()); + String rawValue = value.getValue().toString(); + builder.addValue(rawValue); } return builder; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/provider/XHTMLExtensionProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/provider/XHTMLExtensionProvider.java index 71f15fe53..a09741400 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/provider/XHTMLExtensionProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xhtmlim/provider/XHTMLExtensionProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2014-2019 Florian Schmaus + * Copyright 2003-2007 Jive Software, 2014-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,8 +40,8 @@ public class XHTMLExtensionProvider extends ExtensionElementProvider services = mucManagerOne.getMucServiceDomains(); + if (services.isEmpty()) { + throw new TestNotPossibleException("No MUC (XEP-45) service found"); + } + else { + mucService = services.get(0); + } + } + + /** + * Gets a random room name. + * + * @param prefix A prefix to add to the room name for descriptive purposes + * @return the bare JID of a random room + * @throws XmppStringprepException if the prefix isn't a valid XMPP Localpart + */ + public EntityBareJid getRandomRoom(String prefix) throws XmppStringprepException { + final String roomNameLocal = String.join("-", prefix, testRunId, StringUtils.insecureRandomString(6)); + return JidCreate.entityBareFrom(Localpart.from(roomNameLocal), mucService.getDomain()); + } + + /** + * Destroys a MUC room, ignoring any exceptions. + * + * @param muc The room to destroy (can be null). + * @throws InterruptedException if the calling thread was interrupted. + * @throws SmackException.NotConnectedException if the XMPP connection is not connected. + * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. + * @throws SmackException.NoResponseException if there was no response from the remote entity. + */ + static void tryDestroy(final MultiUserChat muc) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException { + if (muc == null) { + return; + } + muc.destroy("test fixture teardown", null); + } + + static void createMuc(MultiUserChat muc, String resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException, XmppStringprepException { + MultiUserChat.MucCreateConfigFormHandle handle = muc.create(Resourcepart.from(resourceName)); + if (handle != null) { + handle.makeInstant(); + } + } + + static void createModeratedMuc(MultiUserChat muc, String resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException, XmppStringprepException { + muc.create(Resourcepart.from(resourceName)); + Form configForm = muc.getConfigurationForm(); + FillableForm answerForm = configForm.getFillableForm(); + answerForm.setAnswer("muc#roomconfig_moderatedroom", true); //TODO Add this to the MucConfigFormManager? + muc.sendConfigurationForm(answerForm); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java index 3c4a8f5d4..36fa47815 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java @@ -16,66 +16,35 @@ */ package org.jivesoftware.smackx.muc; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.List; import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.MessageListener; -import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; -import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; import org.jivesoftware.smackx.muc.packet.MUCUser; -import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.ResultSyncPoint; import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; -import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; -import org.jxmpp.jid.EntityFullJid; -import org.jxmpp.jid.Jid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.jid.parts.Localpart; import org.jxmpp.jid.parts.Resourcepart; -import org.jxmpp.stringprep.XmppStringprepException; -public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { - - private final String randomString = StringUtils.insecureRandomString(6); - - private final MultiUserChatManager mucManagerOne; - private final MultiUserChatManager mucManagerTwo; - private final MultiUserChatManager mucManagerThree; - private final DomainBareJid mucService; +public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrationTest { public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) - throws NoResponseException, XMPPErrorException, NotConnectedException, - InterruptedException, TestNotPossibleException { + throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + InterruptedException, TestNotPossibleException { super(environment); - mucManagerOne = MultiUserChatManager.getInstanceFor(conOne); - mucManagerTwo = MultiUserChatManager.getInstanceFor(conTwo); - mucManagerThree = MultiUserChatManager.getInstanceFor(conThree); - - List services = mucManagerOne.getMucServiceDomains(); - if (services.isEmpty()) { - throw new TestNotPossibleException("No MUC (XEP-45) service found"); - } - else { - mucService = services.get(0); - } } /** @@ -91,7 +60,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { */ @SmackIntegrationTest public void mucJoinTest() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-join-leave"); + EntityBareJid mucAddress = getRandomRoom("smack-inttest-join"); MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); try { @@ -122,7 +91,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { */ @SmackIntegrationTest public void mucLeaveTest() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-join-leave"); + EntityBareJid mucAddress = getRandomRoom("smack-inttest-leave"); MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); try { @@ -137,13 +106,14 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { assertEquals(mucAddress + "/nick-one", reflectedLeavePresence.getFrom().toString()); assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedLeavePresence.getTo().toString()); } finally { + muc.join(Resourcepart.from("nick-one")); // We need to be in the room to destroy the room tryDestroy(muc); } } @SmackIntegrationTest public void mucTest() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + EntityBareJid mucAddress = getRandomRoom("smack-inttest-message"); MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); @@ -161,7 +131,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { } }); - createMUC(mucAsSeenByOne, "one-" + randomString); + createMuc(mucAsSeenByOne, "one-" + randomString); mucAsSeenByTwo.join(Resourcepart.from("two-" + randomString)); mucAsSeenByOne.sendMessage(mucMessage); try { @@ -173,581 +143,9 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { } } - /** - * Asserts that a user who undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 9.6:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the addition of - * moderator status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForReceivingModerator() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { - @Override - public void moderatorGranted() { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - - // This implicitly tests "The service MUST add the user to the moderator list and then inform the admin of - // success" in §9.6, since it'll throw on either an error IQ or on no response. - mucAsSeenByOne.grantModerator(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 9.6:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the addition of - * moderator status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForWitnessingModerator() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void moderatorGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - - mucAsSeenByOne.grantModerator(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - - } - - /** - * Asserts that a user who undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 9.7:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * moderator status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForRemovingModerator() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { - @Override - public void moderatorRevoked() { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - - mucAsSeenByOne.grantModerator(nicknameTwo); - mucAsSeenByOne.revokeModerator(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 9.7:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * moderator status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForWitnessingModeratorRemoval() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void moderatorRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - - mucAsSeenByOne.grantModerator(nicknameTwo); - mucAsSeenByOne.revokeModerator(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user in an unmoderated room who undergoes an afilliation change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 8.4:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * voice privileges... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForRevokingVoice() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { - @Override - public void voiceRevoked() { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByOne.revokeVoice(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.1.3:

- *
- * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change - * to all occupants... - *
- * - *

From XEP-0045 § 8.4:

- *
- * The service MUST then send updated presence from this individual to all occupants, indicating the removal of - * voice privileges... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucRoleTestForWitnessingRevokingVoice() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void voiceRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - - mucAsSeenByOne.revokeVoice(nicknameTwo); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who undergoes an affiliation change receives that change as a presence update - * - *

From XEP-0045 § 5.2.2:

- *
- * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that - * to all occupants... - *
- * - *

From XEP-0045 § 10.6:

- *
- * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the granting of admin status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucAffiliationTestForReceivingAdmin() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - - mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { - @Override - public void adminGranted() { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - - // This implicitly tests "The service MUST add the user to the admin list and then inform the owner of success" in §10.6, since it'll throw on either an error IQ or on no response. - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user undergoes an affiliation change receives that change as a - * presence update - * - *

From XEP-0045 § 5.2.2:

- *
- * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that - * to all occupants... - *
- * - *

From XEP-0045 § 10.6:

- *
- * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the granting of admin status... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucAffiliationTestForWitnessingAdmin() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void adminGranted(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.2.2:

- *
- * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that to - * all occupants... - *
- * - *

From XEP-0045 § 10.7:

- *
- * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the loss of admin status by sending a presence element... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucAffiliationTestForRemovingAdmin() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { - @Override - public void adminRevoked() { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update - * - *

From XEP-0045 § 5.2.2:

- *
- * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that to - * all occupants... - *
- * - *

From XEP-0045 § 10.7:

- *
- * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, - * indicating the loss of admin status by sending a presence element... - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucAffiliationTestForWitnessingAdminRemoval() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - - mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { - @Override - public void adminRevoked(EntityFullJid participant) { - resultSyncPoint.signal("done"); - } - }); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); - - mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); - try { - resultSyncPoint.waitForResult(timeout); - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who gets kicked receives that change as a presence update - * - *

From XEP-0045 § 8.2:

- *
- * The kick is performed based on the occupant's room nickname and is completed by setting the role of a - * participant or visitor to a value of "none". - * - * The service MUST remove the kicked occupant by sending a presence stanza of type "unavailable" to each kicked - * occupant, including status code 307 in the extended presence information, optionally along with the reason (if - * provided) and the roomnick or bare JID of the user who initiated the kick. - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucPresenceTestForGettingKicked() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - mucAsSeenByTwo.addParticipantListener(kickPresence -> resultSyncPoint.signal(kickPresence)); - - mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); - try { - Presence kickPresence = resultSyncPoint.waitForResult(timeout); - MUCUser mucUser = MUCUser.from(kickPresence); - assertNotNull(mucUser); - assertAll( - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Missing self-presence status code in kick presence"), - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), - () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") - ); - Jid itemJid = mucUser.getItem().getJid(); - if (itemJid != null) { - assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); - } - } finally { - tryDestroy(mucAsSeenByOne); - } - } - - /** - * Asserts that a user who is present when another user gets kicked receives that change as a presence update - * - *

From XEP-0045 § 8.2:

- *
- * ...the service MUST then inform all of the remaining occupants that the kicked occupant is no longer in the room - * by sending presence stanzas of type "unavailable" from the individual's roomnick (<room@service/nick>) to all - * the remaining occupants (just as it does when occupants exit the room of their own volition), including the - * status code and optionally the reason and actor. - *
- * - * @throws Exception when errors occur - */ - @SmackIntegrationTest - public void mucPresenceTestForWitnessingKick() throws Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest"); - - MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); - MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); - - createMUC(mucAsSeenByOne, "one-" + randomString); - final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); - final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); - mucAsSeenByTwo.join(nicknameTwo); - mucAsSeenByThree.join(nicknameThree); - - final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); - mucAsSeenByThree.addParticipantListener(kickPresence -> resultSyncPoint.signal(kickPresence)); - - mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); - try { - Presence kickPresence = resultSyncPoint.waitForResult(timeout); - MUCUser mucUser = MUCUser.from(kickPresence); - assertNotNull(mucUser); - assertAll( - () -> assertFalse(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Incorrect self-presence status code in kick presence"), - () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), - () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") - ); - Jid itemJid = mucUser.getItem().getJid(); - if (itemJid != null) { - assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); - } - } finally { - tryDestroy(mucAsSeenByOne); - } - - } /** - * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update + * Asserts that a user is notified when a room is destroyed * *

From XEP-0045 § 10.9:

*
@@ -760,7 +158,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { @SmackIntegrationTest public void mucDestroyTest() throws TimeoutException, Exception { - EntityBareJid mucAddress = getRandomRoom("smack-inttest-join-leave"); + EntityBareJid mucAddress = getRandomRoom("smack-inttest-destroy"); MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); muc.join(Resourcepart.from("nick-one")); @@ -793,37 +191,6 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { assertNull(muc.getNickname()); } - /** - * Gets a random room name - * - * @param prefix A prefix to add to the room name for descriptive purposes - */ - private EntityBareJid getRandomRoom(String prefix) throws XmppStringprepException { - final String roomNameLocal = String.join("-", prefix, testRunId, StringUtils.insecureRandomString(6)); - return JidCreate.entityBareFrom(Localpart.from(roomNameLocal), mucService.getDomain()); - } - /** - * Destroys a MUC room, ignoring any exceptions. - * - * @param muc The room to destroy (can be null). - * @throws InterruptedException if the calling thread was interrupted. - * @throws NotConnectedException if the XMPP connection is not connected. - * @throws XMPPErrorException if there was an XMPP error returned. - * @throws NoResponseException if there was no response from the remote entity. - */ - static void tryDestroy(final MultiUserChat muc) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - if (muc == null) { - return; - } - muc.destroy("test fixture teardown", null); - } - - private static void createMUC(MultiUserChat muc, String resourceName) throws NoResponseException, XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException, XmppStringprepException { - MucCreateConfigFormHandle handle = muc.create(Resourcepart.from(resourceName)); - if (handle != null) { - handle.makeInstant(); - } - } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java new file mode 100644 index 000000000..966ec3e97 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatRolesAffiliationsPrivilegesIntegrationTest.java @@ -0,0 +1,622 @@ +/** + * + * Copyright 2021 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.muc; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smackx.muc.packet.MUCUser; + +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.util.ResultSyncPoint; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.parts.Resourcepart; + + +public class MultiUserChatRolesAffiliationsPrivilegesIntegrationTest extends AbstractMultiUserChatIntegrationTest{ + + public MultiUserChatRolesAffiliationsPrivilegesIntegrationTest(SmackIntegrationTestEnvironment environment) + throws SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, + InterruptedException, TestNotPossibleException { + super(environment); + } + + /** + * Asserts that a user who undergoes a role change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 9.6:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the addition of + * moderator status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForReceivingModerator() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { + @Override + public void moderatorGranted() { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + + // This implicitly tests "The service MUST add the user to the moderator list and then inform the admin of + // success" in §9.6, since it'll throw on either an error IQ or on no response. + mucAsSeenByOne.grantModerator(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 9.6:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the addition of + * moderator status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForWitnessingModerator() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void moderatorGranted(EntityFullJid participant) { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + + mucAsSeenByOne.grantModerator(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + + } + + /** + * Asserts that a user who undergoes a role change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 9.7:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the removal of + * moderator status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForRemovingModerator() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { + @Override + public void moderatorRevoked() { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + + mucAsSeenByOne.grantModerator(nicknameTwo); + mucAsSeenByOne.revokeModerator(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 9.7:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the removal of + * moderator status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForWitnessingModeratorRemoval() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void moderatorRevoked(EntityFullJid participant) { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + + mucAsSeenByOne.grantModerator(nicknameTwo); + mucAsSeenByOne.revokeModerator(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user in an unmoderated room who undergoes an afilliation change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 8.4:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the removal of + * voice privileges... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForRevokingVoice() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { + @Override + public void voiceRevoked() { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByOne.revokeVoice(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user undergoes a role change receives that change as a presence update + * + *

From XEP-0045 § 5.1.3:

+ *
+ * ...a MUC service implementation MUST change the occupant's role to reflect the change and communicate the change + * to all occupants... + *
+ * + *

From XEP-0045 § 8.4:

+ *
+ * The service MUST then send updated presence from this individual to all occupants, indicating the removal of + * voice privileges... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucRoleTestForWitnessingRevokingVoice() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void voiceRevoked(EntityFullJid participant) { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + + mucAsSeenByOne.revokeVoice(nicknameTwo); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who undergoes an affiliation change receives that change as a presence update + * + *

From XEP-0045 § 5.2.2:

+ *
+ * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that + * to all occupants... + *
+ * + *

From XEP-0045 § 10.6:

+ *
+ * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, + * indicating the granting of admin status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucAffiliationTestForReceivingAdmin() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + + mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { + @Override + public void adminGranted() { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + + // This implicitly tests "The service MUST add the user to the admin list and then inform the owner of success" in §10.6, since it'll throw on either an error IQ or on no response. + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user undergoes an affiliation change receives that change as a + * presence update + * + *

From XEP-0045 § 5.2.2:

+ *
+ * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that + * to all occupants... + *
+ * + *

From XEP-0045 § 10.6:

+ *
+ * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, + * indicating the granting of admin status... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucAffiliationTestForWitnessingAdmin() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void adminGranted(EntityFullJid participant) { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who undergoes an affiliation change receives that change as a presence update + * + *

From XEP-0045 § 5.2.2:

+ *
+ * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that to + * all occupants... + *
+ * + *

From XEP-0045 § 10.7:

+ *
+ * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, + * indicating the loss of admin status by sending a presence element... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucAffiliationTestForRemovingAdmin() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByTwo.addUserStatusListener(new UserStatusListener() { + @Override + public void adminRevoked() { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user undergoes an affiliation change receives that change as a presence update + * + *

From XEP-0045 § 5.2.2:

+ *
+ * ...a MUC service implementation MUST change the user's affiliation to reflect the change and communicate that to + * all occupants... + *
+ * + *

From XEP-0045 § 10.7:

+ *
+ * If the user is in the room, the service MUST then send updated presence from this individual to all occupants, + * indicating the loss of admin status by sending a presence element... + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucAffiliationTestForWitnessingAdminRemoval() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + + mucAsSeenByThree.addParticipantStatusListener(new ParticipantStatusListener() { + @Override + public void adminRevoked(EntityFullJid participant) { + resultSyncPoint.signal("done"); + } + }); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + mucAsSeenByOne.grantAdmin(conTwo.getUser().asBareJid()); + + mucAsSeenByOne.revokeAdmin(conTwo.getUser().asEntityBareJid()); + try { + resultSyncPoint.waitForResult(timeout); + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who gets kicked receives that change as a presence update + * + *

From XEP-0045 § 8.2:

+ *
+ * The kick is performed based on the occupant's room nickname and is completed by setting the role of a + * participant or visitor to a value of "none". + * + * The service MUST remove the kicked occupant by sending a presence stanza of type "unavailable" to each kicked + * occupant, including status code 307 in the extended presence information, optionally along with the reason (if + * provided) and the roomnick or bare JID of the user who initiated the kick. + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucPresenceTestForGettingKicked() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByTwo.addParticipantListener(kickPresence -> resultSyncPoint.signal(kickPresence)); + + mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); + try { + Presence kickPresence = resultSyncPoint.waitForResult(timeout); + MUCUser mucUser = MUCUser.from(kickPresence); + assertNotNull(mucUser); + assertAll( + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Missing self-presence status code in kick presence"), + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), + () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") + ); + Jid itemJid = mucUser.getItem().getJid(); + if (itemJid != null) { + assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); + } + } finally { + tryDestroy(mucAsSeenByOne); + } + } + + /** + * Asserts that a user who is present when another user gets kicked receives that change as a presence update + * + *

From XEP-0045 § 8.2:

+ *
+ * ...the service MUST then inform all of the remaining occupants that the kicked occupant is no longer in the room + * by sending presence stanzas of type "unavailable" from the individual's roomnick (<room@service/nick>) to all + * the remaining occupants (just as it does when occupants exit the room of their own volition), including the + * status code and optionally the reason and actor. + *
+ * + * @throws Exception when errors occur + */ + @SmackIntegrationTest + public void mucPresenceTestForWitnessingKick() throws Exception { + EntityBareJid mucAddress = getRandomRoom("smack-inttest"); + + MultiUserChat mucAsSeenByOne = mucManagerOne.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByTwo = mucManagerTwo.getMultiUserChat(mucAddress); + MultiUserChat mucAsSeenByThree = mucManagerThree.getMultiUserChat(mucAddress); + + createMuc(mucAsSeenByOne, "one-" + randomString); + final Resourcepart nicknameTwo = Resourcepart.from("two-" + randomString); + final Resourcepart nicknameThree = Resourcepart.from("three-" + randomString); + mucAsSeenByTwo.join(nicknameTwo); + mucAsSeenByThree.join(nicknameThree); + + final ResultSyncPoint resultSyncPoint = new ResultSyncPoint<>(); + mucAsSeenByThree.addParticipantListener(kickPresence -> resultSyncPoint.signal(kickPresence)); + + mucAsSeenByOne.kickParticipant(nicknameTwo, "Nothing personal. Just a test."); + try { + Presence kickPresence = resultSyncPoint.waitForResult(timeout); + MUCUser mucUser = MUCUser.from(kickPresence); + assertNotNull(mucUser); + assertAll( + () -> assertFalse(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Incorrect self-presence status code in kick presence"), + () -> assertTrue(mucUser.getStatus().contains(MUCUser.Status.KICKED_307), "Missing kick status code in kick presence"), + () -> assertEquals(MUCRole.none, mucUser.getItem().getRole(), "Role other than 'none' in kick presence") + ); + Jid itemJid = mucUser.getItem().getJid(); + if (itemJid != null) { + assertEquals(conTwo.getUser().asEntityFullJidIfPossible(), itemJid, "Incorrect kicked user in kick presence"); + } + } finally { + tryDestroy(mucAsSeenByOne); + } + + } + +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java index 1057e695c..1720c0725 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.IOException; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; @@ -63,7 +64,7 @@ public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemo } @AfterClass - public void cleanUp() throws IOException { + public void cleanUp() throws IOException, NotConnectedException, InterruptedException { alice.stopStanzaAndPEPListeners(); bob.stopStanzaAndPEPListeners(); OmemoManagerSetupHelper.cleanUpPubSub(alice); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java index 467650409..1a8093d63 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -22,8 +22,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.HashMap; +import java.util.List; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.roster.Roster; @@ -31,12 +33,9 @@ import org.jivesoftware.smack.roster.RosterEntry; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; -import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; -import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.pubsub.PubSubException; -import org.jivesoftware.smackx.pubsub.PubSubManager; import com.google.common.collect.Maps; @@ -124,48 +123,10 @@ public class OmemoManagerSetupHelper { } } - public static void cleanUpPubSub(OmemoManager omemoManager) throws IOException { - PubSubManager pm = PubSubManager.getInstanceFor(omemoManager.getConnection(), omemoManager.getOwnJid()); - try { - omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid()); - } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { - // ignore - } - - OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() - .loadCachedDeviceList(omemoManager.getOwnDevice(), omemoManager.getOwnJid()); - - for (int id : deviceList.getAllDevices()) { - try { - pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); - } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | - PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | - PubSubException.NotAPubSubNodeException e) { - // Silent - } - - try { - pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); - } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException - | XMPPException.XMPPErrorException e) { - // Silent - } - } - - try { - pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); - } catch (InterruptedException | SmackException.NoResponseException | SmackException.NotConnectedException | - PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | - PubSubException.NotAPubSubNodeException e) { - // Silent - } - - try { - pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); - } catch (SmackException.NoResponseException | InterruptedException | SmackException.NotConnectedException | - XMPPException.XMPPErrorException e) { - // Silent - } + public static void cleanUpPubSub(OmemoManager omemoManager) + throws IOException, NotConnectedException, InterruptedException { + List exceptions = omemoManager.purgeEverything(); + assertTrue(exceptions.isEmpty(), "There where exceptions while purging OMEMO: " + exceptions); } public static void cleanUpRoster(OmemoManager omemoManager) { diff --git a/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java b/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java index 624679481..961e7527d 100644 --- a/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java +++ b/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus + * Copyright 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.jivesoftware.smack.full; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.io.PrintWriter; @@ -25,6 +26,8 @@ import java.io.StringWriter; import java.net.URL; import java.nio.charset.StandardCharsets; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.compression.CompressionModuleDescriptor; import org.jivesoftware.smack.util.EqualsUtil; import org.jivesoftware.smack.util.HashCode; @@ -34,6 +37,7 @@ import org.jgrapht.graph.DirectedPseudograph; import org.jgrapht.io.DOTImporter; import org.jgrapht.io.ImportException; import org.junit.jupiter.api.Test; +import org.jxmpp.stringprep.XmppStringprepException; public class ModularXmppClientToServerConnectionStateGraphTest { @@ -80,4 +84,24 @@ public class ModularXmppClientToServerConnectionStateGraphTest { assertEquals(expectedStateGraph, currentStateGraph); } + @Test + public void testNoUnknownStates() throws XmppStringprepException { + ModularXmppClientToServerConnectionConfiguration.builder() + .setUsernameAndPassword("user", "password") + .setXmppDomain("example.org") + .failOnUnknownStates() // This is the actual option that enableds this test. + .build(); + } + + @Test + public void throwsOnUnknownStates() throws XmppStringprepException { + assertThrows(IllegalStateException.class, () -> + ModularXmppClientToServerConnectionConfiguration.builder() + .setUsernameAndPassword("user", "password") + .setXmppDomain("example.org") + .removeModule(CompressionModuleDescriptor.class) + .failOnUnknownStates() // This is the actual option that enableds this test. + .build() + ); + } } diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/RTPBridge.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/RTPBridge.java index 0d3131231..6bc5efcde 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/RTPBridge.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/nat/RTPBridge.java @@ -340,7 +340,6 @@ public class RTPBridge extends IQ { boolean done = false; XmlPullParser.Event eventType; - String elementName; if (!parser.getNamespace().equals(RTPBridge.NAMESPACE)) // TODO: Should be SmackParseException. @@ -356,9 +355,9 @@ public class RTPBridge extends IQ { // Start processing sub-elements while (!done) { eventType = parser.next(); - elementName = parser.getName(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + String elementName = parser.getName(); if (elementName.equals("candidate")) { for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeName(i).equals("ip")) diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleContentDescriptionProvider.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleContentDescriptionProvider.java index 4980dc726..c6980994a 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleContentDescriptionProvider.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleContentDescriptionProvider.java @@ -66,14 +66,13 @@ public abstract class JingleContentDescriptionProvider extends ExtensionElementP public JingleContentDescription parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException { - boolean done = false; JingleContentDescription desc = getInstance(); - while (!done) { + outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); - String name = parser.getName(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + String name = parser.getName(); if (name.equals(JingleContentDescription.JinglePayloadType.NODENAME)) { desc.addJinglePayloadType(parsePayload(parser)); } else { @@ -81,8 +80,8 @@ public abstract class JingleContentDescriptionProvider extends ExtensionElementP throw new IOException("Unknow element \"" + name + "\" in content."); } } else if (eventType == XmlPullParser.Event.END_ELEMENT) { - if (name.equals(JingleContentDescription.NODENAME)) { - done = true; + if (parser.getDepth() == initialDepth) { + break outerloop; } } } diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleDescriptionProvider.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleDescriptionProvider.java index 3b15e4f90..2d912e020 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleDescriptionProvider.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleDescriptionProvider.java @@ -65,14 +65,12 @@ public abstract class JingleDescriptionProvider extends ExtensionElementProvider */ @Override public JingleDescription parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException { - boolean done = false; JingleDescription desc = getInstance(); - while (!done) { + outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); - String name = parser.getName(); - if (eventType == XmlPullParser.Event.START_ELEMENT) { + String name = parser.getName(); if (name.equals(PayloadType.NODENAME)) { desc.addPayloadType(parsePayload(parser)); } else { @@ -80,8 +78,8 @@ public abstract class JingleDescriptionProvider extends ExtensionElementProvider throw new IOException("Unknow element \"" + name + "\" in content."); } } else if (eventType == XmlPullParser.Event.END_ELEMENT) { - if (name.equals(JingleDescription.NODENAME)) { - done = true; + if (parser.getDepth() == initialDepth) { + break outerloop; } } } diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleProvider.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleProvider.java index 0d6003351..9f4627e26 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleProvider.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleProvider.java @@ -84,10 +84,10 @@ public class JingleProvider extends IQProvider { // Start processing sub-elements while (!done) { eventType = parser.next(); - elementName = parser.getName(); - namespace = parser.getNamespace(); if (eventType == XmlPullParser.Event.START_ELEMENT) { + elementName = parser.getName(); + namespace = parser.getNamespace(); // Parse some well know subelements, depending on the namespaces // and element names... diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java index 1fe825482..0edb7d887 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/provider/JingleTransportProvider.java @@ -54,14 +54,13 @@ public abstract class JingleTransportProvider extends ExtensionElementProvider purgeEverything() throws NotConnectedException, InterruptedException, IOException { + List exceptions = new ArrayList<>(5); + PubSubManager pm = PubSubManager.getInstanceFor(getConnection(), getOwnJid()); + try { + requestDeviceListUpdateFor(getOwnJid()); + } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException + | XMPPException.XMPPErrorException e) { + exceptions.add(e); + } + + OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() + .loadCachedDeviceList(getOwnDevice(), getOwnJid()); + + for (int id : deviceList.getAllDevices()) { + try { + pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); + } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException + | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { + exceptions.add(e); + } + + try { + pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); + } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { + exceptions.add(e); + } + } + + try { + pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); + } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException + | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { + exceptions.add(e); + } + + try { + pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); + } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { + exceptions.add(e); + } + + return exceptions; + } + /** * Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and * then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java index e45d35a48..2a3b1739e 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/provider/OmemoBundleVAxolotlProvider.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smackx.omemo.provider; -import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.BUNDLE; import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.IDENTITY_KEY; import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEYS; import static org.jivesoftware.smackx.omemo.element.OmemoBundleElement.PRE_KEY_ID; @@ -43,7 +42,6 @@ import org.jivesoftware.smackx.omemo.element.OmemoBundleElement_VAxolotl; public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider { @Override public OmemoBundleElement_VAxolotl parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException { - boolean stop = false; boolean inPreKeys = false; int signedPreKeyId = -1; @@ -52,11 +50,11 @@ public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider preKeys = new HashMap<>(); - while (!stop) { + outerloop: while (true) { XmlPullParser.Event tag = parser.next(); - String name = parser.getName(); switch (tag) { case START_ELEMENT: + String name = parser.getName(); final int attributeCount = parser.getAttributeCount(); // if (name.equals(SIGNED_PRE_KEY_PUB)) { @@ -91,8 +89,8 @@ public class OmemoBundleVAxolotlProvider extends ExtensionElementProvider deviceListIds = new HashSet<>(); - boolean stop = false; - while (!stop) { + outerloop: while (true) { XmlPullParser.Event tag = parser.next(); - String name = parser.getName(); switch (tag) { case START_ELEMENT: + String name = parser.getName(); if (name.equals(DEVICE)) { for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeName(i).equals(ID)) { @@ -57,8 +55,8 @@ public class OmemoDeviceListVAxolotlProvider extends ExtensionElementProvider keys = new ArrayList<>(); byte[] iv = null; byte[] payload = null; - while (inEncrypted) { + outerloop: while (true) { XmlPullParser.Event tag = parser.next(); - String name = parser.getName(); switch (tag) { case START_ELEMENT: + String name = parser.getName(); switch (name) { case OmemoHeaderElement.ELEMENT: for (int i = 0; i < parser.getAttributeCount(); i++) { @@ -82,8 +80,8 @@ public class OmemoVAxolotlProvider extends ExtensionElementProvider