1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-22 12:02:05 +01:00

Merge pull request #482 from Fishbowler/xep-0045-coverage-part4

[sinttest] Additional tests covering s7 of XEP-0045
This commit is contained in:
Florian Schmaus 2024-04-30 10:08:39 +02:00 committed by GitHub
commit 36d6ff2995
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1393 additions and 72 deletions

View file

@ -223,6 +223,15 @@ public class MucConfigFormManager {
}
/**
* 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.
*
@ -251,7 +260,7 @@ public class MucConfigFormManager {
* @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);
@ -299,7 +308,7 @@ public class MucConfigFormManager {
*/
public MucConfigFormManager setIsPasswordProtected(boolean isPasswordProtected)
throws MucConfigurationNotSupportedException {
if (!supportsMembersOnly()) {
if (!supportsPasswordProtected()) {
throw new MucConfigurationNotSupportedException(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM);
}
answerForm.setAnswer(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM, isPasswordProtected);

View file

@ -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();
}
}

View file

@ -22,6 +22,8 @@ import java.util.List;
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;
@ -140,4 +142,75 @@ public abstract class AbstractMultiUserChatIntegrationTest extends AbstractSmack
.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);
}
}

View file

@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
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;
@ -30,11 +31,13 @@ import org.jivesoftware.smack.packet.StanzaError;
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;
@ -49,6 +52,21 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt
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.
*
@ -56,13 +74,12 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt
*/
@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).");
}
/**
@ -120,6 +137,7 @@ public class MultiUserChatEntityIntegrationTest extends AbstractMultiUserChatInt
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).");
}
/**

View file

@ -17,9 +17,7 @@
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;
@ -27,9 +25,6 @@ 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;
@ -48,62 +43,6 @@ public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrati
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");
@ -150,7 +89,7 @@ public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrati
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();
@ -165,8 +104,8 @@ public class MultiUserChatIntegrationTest extends AbstractMultiUserChatIntegrati
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 {

View file

@ -0,0 +1,108 @@
/**
*
* Copyright 2024 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 static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;
import org.jivesoftware.smack.util.Async;
import org.junit.jupiter.api.Test;
public class MultiResultSyncPointTest {
@Test
public void testResultSyncPoint() throws Exception {
final String result1 = "r1";
final String result2 = "r2";
final CyclicBarrier barrier = new CyclicBarrier(2);
final MultiResultSyncPoint<String, Exception> rsp = new MultiResultSyncPoint<>(2);
Async.go(new Async.ThrowingRunnable() {
@Override
public void runOrThrow() throws InterruptedException, BrokenBarrierException {
barrier.await();
rsp.signal(result1);
rsp.signal(result2);
}
});
barrier.await();
List<String> receivedResult = rsp.waitForResults(60 * 1000);
assertTrue(receivedResult.contains(result1));
assertTrue(receivedResult.contains(result2));
}
@Test
public void exceptionTestResultSyncPoint() throws Exception {
final CyclicBarrier barrier = new CyclicBarrier(2);
final ResultSyncPoint<String, MultiResultSyncPointTest.TestException> rsp = new ResultSyncPoint<>();
Async.go(new Async.ThrowingRunnable() {
@Override
public void runOrThrow() throws InterruptedException, BrokenBarrierException {
barrier.await();
rsp.signal(new MultiResultSyncPointTest.TestException());
}
});
barrier.await();
assertThrows(MultiResultSyncPointTest.TestException.class, () -> rsp.waitForResult(60 * 1000));
}
@Test
public void testTimeout() throws Exception {
final MultiResultSyncPoint<String, Exception> rsp = new MultiResultSyncPoint<>(2);
try {
rsp.waitForResults(100);
fail("A timeout exception should have been thrown.");
} catch (TimeoutException e) {
// Expected
}
}
@Test
public void testTimeoutWithOneResult() throws Exception {
final String result1 = "partial";
final CyclicBarrier barrier = new CyclicBarrier(2);
final MultiResultSyncPoint<String, Exception> rsp = new MultiResultSyncPoint<>(2);
Async.go(new Async.ThrowingRunnable() {
@Override
public void runOrThrow() throws InterruptedException, BrokenBarrierException {
barrier.await();
rsp.signal(result1);
}
});
barrier.await();
try {
rsp.waitForResults(100);
fail("A timeout exception should have been thrown.");
} catch (TimeoutException e) {
// Expected
}
}
private static class TestException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
}
}

View file

@ -18,9 +18,11 @@ package org.igniterealtime.smack.inttest.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;
import org.jivesoftware.smack.util.Async;
@ -60,6 +62,17 @@ public class ResultSyncPointTest {
assertThrows(TestException.class, () -> rsp.waitForResult(60 * 1000));
}
@Test
public void testTimeout() throws Exception {
final MultiResultSyncPoint<String, Exception> rsp = new MultiResultSyncPoint<>(2);
try {
rsp.waitForResults(100);
fail("A timeout exception should have been thrown.");
} catch (TimeoutException e) {
// Expected
}
}
private static class TestException extends Exception {
/**