Skip to content

Commit

Permalink
[sinttest] Additional tests for s7 of XEP-0045
Browse files Browse the repository at this point in the history
  • Loading branch information
Fishbowler authored and guusdk committed Apr 30, 2024
1 parent 2e94599 commit 855f01e
Show file tree
Hide file tree
Showing 8 changed files with 1,393 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ public MucConfigFormManager setModerated(boolean isModerated) throws MucConfigur
}


/**
* Check if the room supports its visibility being controlled vioa configuration.
*
* @return <code>true</code> if supported, <code>false</code> if not.
*/
public boolean supportsPublicRoom() {
return answerForm.hasField(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM);
}

/**
* Make the room publicly searchable.
*
Expand Down Expand Up @@ -251,7 +260,7 @@ public MucConfigFormManager makeHidden() throws MucConfigurationNotSupportedExce
* @throws MucConfigurationNotSupportedException if the requested MUC configuration is not supported by the MUC service.
*/
public MucConfigFormManager setPublic(boolean isPublic) throws MucConfigurationNotSupportedException {
if (!supportsModeration()) {
if (!supportsPublicRoom()) {
throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM);
}
answerForm.setAnswer(MUC_ROOMCONFIG_PUBLICLYSEARCHABLEROOM, isPublic);
Expand Down Expand Up @@ -299,7 +308,7 @@ public MucConfigFormManager makePasswordProtected() throws MucConfigurationNotSu
*/
public MucConfigFormManager setIsPasswordProtected(boolean isPasswordProtected)
throws MucConfigurationNotSupportedException {
if (!supportsMembersOnly()) {
if (!supportsPasswordProtected()) {
throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM);
}
answerForm.setAnswer(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM, isPasswordProtected);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
*
* Copyright 2021 Guus der Kinderen
*
* 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.igniterealtime.smack.inttest.util;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.jivesoftware.smack.util.Objects;

public class MultiResultSyncPoint<R, E extends Exception> {

private final List<R> results;
private E exception;
private final int expectedResultCount;

public MultiResultSyncPoint(int expectedResultCount) {
this.expectedResultCount = expectedResultCount;
this.results = new ArrayList<>(expectedResultCount);
}

public synchronized List<R> waitForResults(long timeout) throws E, InterruptedException, TimeoutException {
long now = System.currentTimeMillis();
final long deadline = now + timeout;
while (results.size() < expectedResultCount && exception == null && now < deadline) {
wait(deadline - now);
now = System.currentTimeMillis();
}
if (now >= deadline) throw new TimeoutException("Timeout waiting " + timeout + " millis");
if (exception != null) throw exception;
return new ArrayList<>(results);
}

public synchronized void signal(R result) {
this.results.add(Objects.requireNonNull(result));
if (expectedResultCount <= results.size()) {
notifyAll();
}
}

public synchronized void signal(E exception) {
this.exception = Objects.requireNonNull(exception);
notifyAll();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.Form;

import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
Expand Down Expand Up @@ -140,4 +142,75 @@ static void createHiddenMuc(MultiUserChat muc, Resourcepart resourceName)
.makeHidden()
.submitConfigurationForm();
}

/**
* Creates a non-anonymous room.
*
* <p>From XEP-0045 § 10.1.3:</p>
* <blockquote>
* Note: The _whois configuration option specifies whether the room is non-anonymous (a value of "anyone"),
* semi-anonymous (a value of "moderators"), or fully anonmyous (a value of "none", not shown here).
* </blockquote>
*/
static void createNonAnonymousMuc(MultiUserChat muc, Resourcepart resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException {
muc.create(resourceName);
Form configForm = muc.getConfigurationForm();
FillableForm answerForm = configForm.getFillableForm();
answerForm.setAnswer("muc#roomconfig_whois", "anyone");
muc.sendConfigurationForm(answerForm);
}

/**
* Creates a semi-anonymous room.
*
* <p>From XEP-0045 § 10.1.3:</p>
* <blockquote>
* Note: The _whois configuration option specifies whether the room is non-anonymous (a value of "anyone"),
* semi-anonymous (a value of "moderators"), or fully anonmyous (a value of "none", not shown here).
* </blockquote>
*/
static void createSemiAnonymousMuc(MultiUserChat muc, Resourcepart resourceName) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException {
muc.create(resourceName);
Form configForm = muc.getConfigurationForm();
FillableForm answerForm = configForm.getFillableForm();
answerForm.setAnswer("muc#roomconfig_whois", "moderators");
muc.sendConfigurationForm(answerForm);
}

/**
* Creates a password-protected room.
*/
static void createPasswordProtectedMuc(MultiUserChat muc, Resourcepart resourceName, String password) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, MultiUserChatException.MucAlreadyJoinedException, SmackException.NotConnectedException, MultiUserChatException.MissingMucCreationAcknowledgeException, MultiUserChatException.NotAMucServiceException {
muc.create(resourceName);
Form configForm = muc.getConfigurationForm();
FillableForm answerForm = configForm.getFillableForm();
answerForm.setAnswer("muc#roomconfig_passwordprotectedroom", true);
answerForm.setAnswer("muc#roomconfig_roomsecret", password);
muc.sendConfigurationForm(answerForm);
}

static void createLockedMuc(MultiUserChat muc, Resourcepart resourceName) throws
SmackException.NoResponseException, XMPPException.XMPPErrorException,
InterruptedException, MultiUserChatException.MucAlreadyJoinedException,
SmackException.NotConnectedException,
MultiUserChatException.MissingMucCreationAcknowledgeException,
MultiUserChatException.NotAMucServiceException {
muc.create(resourceName);
// Note the absence of handle.makeInstant() here. The room is still being created at this point, until a
// configuration is set.
}

static void setMaxUsers(MultiUserChat muc, int maxUsers) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException {
Form configForm = muc.getConfigurationForm();
FillableForm answerForm = configForm.getFillableForm();
answerForm.setAnswer("muc#roomconfig_maxusers", maxUsers);
muc.sendConfigurationForm(answerForm);
}

static void setPublicLogging(MultiUserChat muc, boolean publicLogging) throws SmackException.NoResponseException, XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException {
Form configForm = muc.getConfigurationForm();
FillableForm answerForm = configForm.getFillableForm();
answerForm.setAnswer("muc#roomconfig_enablelogging", publicLogging);
muc.sendConfigurationForm(answerForm);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;

import org.jivesoftware.smack.SmackException;
Expand All @@ -30,11 +31,13 @@
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;

import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.annotations.SpecificationReference;

import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
Expand All @@ -49,20 +52,34 @@ public MultiUserChatEntityIntegrationTest(SmackIntegrationTestEnvironment enviro
super(environment);
}

/**
* Asserts that a MUC service can be discovered.
*
* @throws Exception when errors occur
*/
@SmackIntegrationTest(section = "6.1", quote =
"An entity often discovers a MUC service by sending a Service Discovery items (\"disco#items\") request to " +
"its own server. The server then returns the services that are associated with it.")
public void mucTestForDiscoveringMuc() throws Exception {
// This repeats some logic from the `AbstractMultiUserChatIntegrationTest` constructor, but is preserved here
// as an explicit test, because that might not always be true.
List<DomainBareJid> services = ServiceDiscoveryManager.getInstanceFor(conOne).findServices(MUCInitialPresence.NAMESPACE, true, false);
assertFalse(services.isEmpty(), "Expected to be able to find MUC services on the domain that '" + conOne.getUser() + "' is connecting to (but could not).");
}

/**
* Asserts that a MUC service can have its features discovered.
*
* @throws Exception when errors occur
*/
@SmackIntegrationTest(section = "6.2", quote =
"An entity may wish to discover if a service implements the Multi-User Chat protocol; in order to do so, it " +
"sends a service discovery information (\"disco#info\") query to the MUC service's JID. The service MUST " +
"return its identity and the features it supports.")
"sends a service discovery information (\"disco#info\") query to the MUC service's JID. The service MUST " +
"return its identity and the features it supports.")
public void mucTestForDiscoveringFeatures() throws Exception {
final DomainBareJid mucServiceAddress = mucManagerOne.getMucServiceDomains().get(0);
DiscoverInfo info = mucManagerOne.getMucServiceDiscoInfo(mucServiceAddress);
assertFalse(info.getIdentities().isEmpty(), "Expected the service discovery information for service " + mucServiceAddress + " to include identities (but it did not).");
assertFalse(info.getFeatures().isEmpty(), "Expected the service discovery information for service " + mucServiceAddress + " to include features (but it did not).");
DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(conOne).discoverInfo(mucService);
assertFalse(info.getIdentities().isEmpty(), "Expected the service discovery information for service " + mucService + " to include identities (but it did not).");
assertFalse(info.getFeatures().isEmpty(), "Expected the service discovery information for service " + mucService + " to include features (but it did not).");
}

/**
Expand Down Expand Up @@ -120,6 +137,7 @@ public void mucTestForDiscoveringRoomInfo() throws Exception {

assertFalse(discoInfo.getIdentities().isEmpty(), "Expected the service discovery information for room " + mucAddress + " to include identities (but it did not).");
assertFalse(discoInfo.getFeatures().isEmpty(), "Expected the service discovery information for room " + mucAddress + " to include features (but it did not).");
assertTrue(discoInfo.getFeatures().stream().anyMatch(feature -> MultiUserChatConstants.NAMESPACE.equals(feature.getVar())), "Expected the service discovery information for room " + mucAddress + " to include the '" + MultiUserChatConstants.NAMESPACE + "' feature (but it did not).");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@
package org.jivesoftware.smackx.muc;

import static org.junit.jupiter.api.Assertions.assertEquals;
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.concurrent.TimeoutException;

import org.jivesoftware.smack.MessageListener;
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.smackx.muc.packet.MUCUser;

import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
Expand All @@ -48,62 +43,6 @@ public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment)
super(environment);
}

/**
* Asserts that when a user joins a room, they are themselves included on the list of users notified (self-presence).
*
* @throws Exception when errors occur
*/
@SmackIntegrationTest(section = "7.2.2", quote =
"... the service MUST also send presence from the new participant's occupant JID to the full JIDs of all the " +
"occupants (including the new occupant)")
public void mucJoinTest() throws Exception {
EntityBareJid mucAddress = getRandomRoom("smack-inttest-join");

MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress);
try {
Presence reflectedJoinPresence = muc.join(Resourcepart.from("nick-one"));

MUCUser mucUser = MUCUser.from(reflectedJoinPresence);

assertNotNull(mucUser, "Expected, but unable, to create a MUCUser instance from reflected join presence: " + reflectedJoinPresence);
assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Expected the reflected join presence of " + conOne.getUser() + " of room " + mucAddress + " to include 'presence-to-self' (" + MUCUser.Status.PRESENCE_TO_SELF_110 + ") but it did not.");
assertEquals(mucAddress + "/nick-one", reflectedJoinPresence.getFrom().toString(), "Unexpected 'from' attribute value in the reflected join presence of " + conOne.getUser() + " of room " + mucAddress);
assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedJoinPresence.getTo().toString(), "Unexpected 'to' attribute value in the reflected join presence of " + conOne.getUser() + " of room " + mucAddress);
} finally {
tryDestroy(muc);
}
}

/**
* Asserts that when a user leaves a room, they are themselves included on the list of users notified (self-presence).
*
* @throws Exception when errors occur
*/
@SmackIntegrationTest(section = "7.14", quote =
"The service MUST then send a presence stanzas of type \"unavailable\" from the departing user's occupant " +
"JID to the departing occupant's full JIDs, including a status code of \"110\" to indicate that this " +
"notification is \"self-presence\"")
public void mucLeaveTest() throws Exception {
EntityBareJid mucAddress = getRandomRoom("smack-inttest-leave");

MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress);
try {
muc.join(Resourcepart.from("nick-one"));

Presence reflectedLeavePresence = muc.leave();

MUCUser mucUser = MUCUser.from(reflectedLeavePresence);
assertNotNull(mucUser, "Expected, but unable, to create a MUCUser instance from reflected leave presence: " + reflectedLeavePresence);

assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110), "Expected the reflected leave presence of " + conOne.getUser() + " of room " + mucAddress + " to include 'presence-to-self' (" + MUCUser.Status.PRESENCE_TO_SELF_110 + ") but it did not.");
assertEquals(mucAddress + "/nick-one", reflectedLeavePresence.getFrom().toString(), "Unexpected 'from' attribute value in the reflected leave presence of " + conOne.getUser() + " of room " + mucAddress);
assertEquals(conOne.getUser().asEntityFullJidIfPossible().toString(), reflectedLeavePresence.getTo().toString(), "Unexpected 'to' attribute value in the reflected leave presence of " + conOne.getUser() + " of room " + mucAddress);
} 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-message");
Expand Down Expand Up @@ -150,7 +89,7 @@ public void mucDestroyTest() throws TimeoutException, Exception {
EntityBareJid mucAddress = getRandomRoom("smack-inttest-destroy");

MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress);
muc.join(Resourcepart.from("nick-one"));
createMuc(muc, Resourcepart.from("one-" + randomString));

final SimpleResultSyncPoint mucDestroyed = new SimpleResultSyncPoint();

Expand All @@ -165,8 +104,8 @@ public void roomDestroyed(MultiUserChat alternateMUC, String reason) {
muc.addUserStatusListener(userStatusListener);

// These would be a test implementation bug, not assertion failure.
if (mucManagerOne.getJoinedRooms().size() != 1) {
throw new IllegalStateException("Expected user to have joined a room.");
if (mucManagerOne.getJoinedRooms().stream().noneMatch(room -> room.equals(mucAddress))) {
throw new IllegalStateException("Expected user to have joined a room '" + mucAddress + "' (but does not appear to have done so).");
}

try {
Expand Down
Loading

0 comments on commit 855f01e

Please sign in to comment.