[sinttest] Add UnconnectedConnectionSource for low-level tests

Previously low-level tests where run, potentially multiple times, with
the default connection descriptor.

Reported-by: Guus der Kinderen <guus@goodbytes.nl>
This commit is contained in:
Florian Schmaus 2023-02-09 20:36:25 +01:00
parent 92f1fe647b
commit c5bb15c631
5 changed files with 180 additions and 102 deletions

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2015-2020 Florian Schmaus
* Copyright 2015-2023 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,20 +17,19 @@
package org.igniterealtime.smack.inttest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jxmpp.jid.DomainBareJid;
public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest {
public interface UnconnectedConnectionSource {
AbstractXMPPConnection getUnconnectedConnection();
}
private final SmackIntegrationTestEnvironment environment;
/**
@ -47,25 +46,23 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack
this.service = configuration.service;
}
protected AbstractXMPPConnection getConnectedConnection() throws InterruptedException, XMPPException, SmackException, IOException {
AbstractXMPPConnection connection = getUnconnectedConnection();
connection.connect().login();
return connection;
}
protected AbstractXMPPConnection getUnconnectedConnection()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return environment.connectionManager.constructConnection();
}
protected List<AbstractXMPPConnection> getUnconnectedConnections(int count)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
List<AbstractXMPPConnection> connections = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
AbstractXMPPConnection connection = getUnconnectedConnection();
connections.add(connection);
}
return connections;
/**
* Get a connected connection. Note that this method will return a connection constructed via the default connection
* descriptor. It is primarily meant for integration tests to discover if the XMPP service supports a certain
* feature, that the integration test requires to run. This feature discovery is typically done in the constructor of the
* integration tests. And if the discovery fails a {@link TestNotPossibleException} should be thrown by he constructor.
*
* <p> Please ensure that you invoke {@link #recycle(AbstractXMPPConnection connection)} once you are done with this connection.
*
* @return a connected connection.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws IOException if an I/O error occurred.
* @throws XMPPException if an XMPP protocol error was received.
*/
protected AbstractXMPPConnection getConnectedConnection()
throws InterruptedException, SmackException, IOException, XMPPException {
return environment.connectionManager.constructConnectedConnection();
}
protected void recycle(AbstractXMPPConnection connection) {

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2015-2021 Florian Schmaus
* Copyright 2015-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -54,7 +54,9 @@ import org.jivesoftware.smack.Smack;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.TLSUtils;
@ -510,10 +512,11 @@ public class SmackIntegrationTestFramework {
private static void verifyLowLevelTestMethod(Method method,
Class<? extends AbstractXMPPConnection> connectionClass) {
if (!testMethodParametersIsListOfConnections(method, connectionClass)
&& !testMethodParametersVarargsConnections(method, connectionClass)) {
throw new IllegalArgumentException(method + " is not a valid low level test method");
if (determineTestMethodParameterType(method, connectionClass) != null) {
return;
}
throw new IllegalArgumentException(method + " is not a valid low level test method");
}
private List<ConcreteTest> invokeLowLevel(LowLevelTestMethod lowLevelTestMethod, AbstractSmackLowLevelIntegrationTest test) {
@ -824,47 +827,69 @@ public class SmackIntegrationTestFramework {
}
private final class LowLevelTestMethod {
private final Method testMethod;
private final SmackIntegrationTest smackIntegrationTestAnnotation;
private final boolean parameterListOfConnections;
private final TestMethodParameterType parameterType;
private LowLevelTestMethod(Method testMethod) {
this.testMethod = testMethod;
smackIntegrationTestAnnotation = testMethod.getAnnotation(SmackIntegrationTest.class);
assert smackIntegrationTestAnnotation != null;
parameterListOfConnections = testMethodParametersIsListOfConnections(testMethod);
parameterType = determineTestMethodParameterType(testMethod);
}
private void invoke(AbstractSmackLowLevelIntegrationTest test,
XmppConnectionDescriptor<?, ?, ?> connectionDescriptor)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
InterruptedException, SmackException, IOException, XMPPException {
final int connectionCount;
if (parameterListOfConnections) {
connectionCount = smackIntegrationTestAnnotation.connectionCount();
if (connectionCount < 1) {
throw new IllegalArgumentException(testMethod + " is annotated to use less than one connection ('"
+ connectionCount + ')');
switch (parameterType) {
case singleConnectedConnection:
case collectionOfConnections:
case parameterListOfConnections:
final boolean collectionOfConnections = parameterType == TestMethodParameterType.collectionOfConnections;
final int connectionCount;
if (collectionOfConnections) {
connectionCount = smackIntegrationTestAnnotation.connectionCount();
if (connectionCount < 1) {
throw new IllegalArgumentException(testMethod + " is annotated to use less than one connection ('"
+ connectionCount + ')');
}
} else {
connectionCount = testMethod.getParameterCount();
}
} else {
connectionCount = testMethod.getParameterCount();
}
List<? extends AbstractXMPPConnection> connections = connectionManager.constructConnectedConnections(
connectionDescriptor, connectionCount);
List<? extends AbstractXMPPConnection> connections = connectionManager.constructConnectedConnections(
connectionDescriptor, connectionCount);
if (parameterListOfConnections) {
testMethod.invoke(test, connections);
} else {
Object[] connectionsArray = new Object[connectionCount];
for (int i = 0; i < connectionsArray.length; i++) {
connectionsArray[i] = connections.remove(0);
if (collectionOfConnections) {
testMethod.invoke(test, connections);
} else {
Object[] connectionsArray = new Object[connectionCount];
for (int i = 0; i < connectionsArray.length; i++) {
connectionsArray[i] = connections.remove(0);
}
testMethod.invoke(test, connectionsArray);
}
testMethod.invoke(test, connectionsArray);
}
connectionManager.recycle(connections);
connectionManager.recycle(connections);
break;
case unconnectedConnectionSource:
AbstractSmackLowLevelIntegrationTest.UnconnectedConnectionSource source = () -> {
try {
return environment.connectionManager.constructConnection(connectionDescriptor);
} catch (NoResponseException | XMPPErrorException | NotConnectedException
| InterruptedException e) {
// TODO: Ideally we would wrap the exceptions in an unchecked exceptions, catch those unchecked
// exceptions below and throw the wrapped checked exception.
throw new RuntimeException(e);
}
};
testMethod.invoke(test, source);
break;
}
}
@Override
@ -873,46 +898,80 @@ public class SmackIntegrationTestFramework {
}
}
private static boolean testMethodParametersIsListOfConnections(Method testMethod) {
return testMethodParametersIsListOfConnections(testMethod, AbstractXMPPConnection.class);
enum TestMethodParameterType {
/**
* testMethod(Connection connection)
*/
singleConnectedConnection,
/**
* testMethod(Collection&lt;Connection&gt;)
* <p> It can also be a subclass of collection like List. In fact, the type of the parameter being List is expected to be the common case.
*/
collectionOfConnections,
/**
* testMethod(Connection a, Connection b, Connection c)
*/
parameterListOfConnections,
/**
* testMethod(UnconnectedConnectionSource unconnectedConnectionSource)
*/
unconnectedConnectionSource,
};
static TestMethodParameterType determineTestMethodParameterType(Method testMethod) {
return determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
}
static boolean testMethodParametersIsListOfConnections(Method testMethod, Class<? extends AbstractXMPPConnection> connectionClass) {
Type[] parameterTypes = testMethod.getGenericParameterTypes();
if (parameterTypes.length != 1) {
return false;
}
Class<?> soleParameter = testMethod.getParameterTypes()[0];
if (!Collection.class.isAssignableFrom(soleParameter)) {
return false;
}
ParameterizedType soleParameterizedType = (ParameterizedType) parameterTypes[0];
Type[] actualTypeArguments = soleParameterizedType.getActualTypeArguments();
if (actualTypeArguments.length != 1) {
return false;
}
Type soleActualTypeArgument = actualTypeArguments[0];
if (!(soleActualTypeArgument instanceof Class<?>)) {
return false;
}
Class<?> soleActualTypeArgumentAsClass = (Class<?>) soleActualTypeArgument;
if (!connectionClass.isAssignableFrom(soleActualTypeArgumentAsClass)) {
return false;
}
return true;
}
static boolean testMethodParametersVarargsConnections(Method testMethod, Class<? extends AbstractXMPPConnection> connectionClass) {
static TestMethodParameterType determineTestMethodParameterType(Method testMethod, Class<? extends AbstractXMPPConnection> connectionClass) {
Class<?>[] parameterTypes = testMethod.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
if (!parameterType.isAssignableFrom(connectionClass)) {
return false;
}
if (parameterTypes.length == 0) {
return null;
}
return true;
if (parameterTypes.length > 1) {
// If there are more parameters, then all must be assignable from the connection class.
for (Class<?> parameterType : parameterTypes) {
if (!connectionClass.isAssignableFrom(parameterType)) {
return null;
}
}
return TestMethodParameterType.parameterListOfConnections;
}
// This method has exactly a single parameter.
Class<?> soleParameter = parameterTypes[0];
if (Collection.class.isAssignableFrom(soleParameter)) {
// The sole parameter is assignable from collection, which means that it is a parameterized generic type.
ParameterizedType soleParameterizedType = (ParameterizedType) testMethod.getGenericParameterTypes()[0];
Type[] actualTypeArguments = soleParameterizedType.getActualTypeArguments();
if (actualTypeArguments.length != 1) {
// The parameter list of the Collection has more than one type.
return null;
}
Type soleActualTypeArgument = actualTypeArguments[0];
if (!(soleActualTypeArgument instanceof Class<?>)) {
return null;
}
Class<?> soleActualTypeArgumentAsClass = (Class<?>) soleActualTypeArgument;
if (!connectionClass.isAssignableFrom(soleActualTypeArgumentAsClass)) {
return null;
}
return TestMethodParameterType.collectionOfConnections;
} else if (connectionClass.isAssignableFrom(soleParameter)) {
return TestMethodParameterType.singleConnectedConnection;
} else if (AbstractSmackLowLevelIntegrationTest.UnconnectedConnectionSource.class.isAssignableFrom(soleParameter)) {
return TestMethodParameterType.unconnectedConnectionSource;
}
return null;
}
}

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2018-2021 Florian Schmaus
* Copyright 2018-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -421,6 +421,11 @@ public class XmppConnectionManager {
return constructConnection(defaultConnectionDescriptor);
}
AbstractXMPPConnection constructConnectedConnection()
throws InterruptedException, SmackException, IOException, XMPPException {
return constructConnectedConnection(defaultConnectionDescriptor);
}
<C extends AbstractXMPPConnection> C constructConnection(
XmppConnectionDescriptor<C, ? extends ConnectionConfiguration, ? extends ConnectionConfiguration.Builder<?, ?>> connectionDescriptor)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2015-2020 Florian Schmaus
* Copyright 2015-2023 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,6 +42,7 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest {
* Check that the server is returning the correct error when trying to login using an invalid
* (i.e. non-existent) user.
*
* @param unconnectedConnectionSource the unconnected connections source that is used.
* @throws InterruptedException if the calling thread was interrupted.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
@ -50,12 +51,12 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest {
* @throws KeyManagementException if there was a key mangement error.
*/
@SmackIntegrationTest
public void testInvalidLogin() throws SmackException, IOException, XMPPException,
public void testInvalidLogin(UnconnectedConnectionSource unconnectedConnectionSource) throws SmackException, IOException, XMPPException,
InterruptedException, KeyManagementException, NoSuchAlgorithmException {
final String nonExistentUserString = StringUtils.insecureRandomString(24);
final String invalidPassword = "invalidPassword";
AbstractXMPPConnection connection = getUnconnectedConnection();
AbstractXMPPConnection connection = unconnectedConnectionSource.getUnconnectedConnection();
connection.connect();
try {

View File

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2023 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,14 +16,16 @@
*/
package org.igniterealtime.smack.inttest;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.lang.reflect.Method;
import java.util.List;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework.TestMethodParameterType;
import org.junit.jupiter.api.Test;
public class SmackIntegrationTestFrameWorkTest {
@ -69,28 +71,42 @@ public class SmackIntegrationTestFrameWorkTest {
@Test
public void testValidLowLevelList() {
Method testMethod = getTestMethod(ValidLowLevelList.class);
assertTrue(SmackIntegrationTestFramework.testMethodParametersIsListOfConnections(testMethod,
AbstractXMPPConnection.class));
TestMethodParameterType determinedParameterType = SmackIntegrationTestFramework.determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
assertEquals(TestMethodParameterType.collectionOfConnections, determinedParameterType);
}
@Test
public void testInvalidLowLevelList() {
Method testMethod = getTestMethod(InvalidLowLevelList.class);
assertFalse(SmackIntegrationTestFramework.testMethodParametersIsListOfConnections(testMethod,
AbstractXMPPConnection.class));
TestMethodParameterType determinedParameterType = SmackIntegrationTestFramework.determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
assertNull(determinedParameterType);
}
@Test
public void testValidLowLevelVarargs() {
Method testMethod = getTestMethod(ValidLowLevelVarargs.class);
assertTrue(SmackIntegrationTestFramework.testMethodParametersVarargsConnections(testMethod,
AbstractXMPPConnection.class));
TestMethodParameterType determinedParameterType = SmackIntegrationTestFramework.determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
assertEquals(TestMethodParameterType.parameterListOfConnections, determinedParameterType);
}
@Test
public void testInvalidLowLevelVargs() {
Method testMethod = getTestMethod(InvalidLowLevelVarargs.class);
assertFalse(SmackIntegrationTestFramework.testMethodParametersVarargsConnections(testMethod,
AbstractXMPPConnection.class));
TestMethodParameterType determinedParameterType = SmackIntegrationTestFramework.determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
assertNull(determinedParameterType);
}
private static class ValidUnconnectedConnectionSource {
@SuppressWarnings("unused")
public void test(AbstractSmackLowLevelIntegrationTest.UnconnectedConnectionSource source) {
}
}
@Test
public void testValidUnconnectedConnectionSource() {
Method testMethod = getTestMethod(ValidUnconnectedConnectionSource.class);
TestMethodParameterType determinedParameterType = SmackIntegrationTestFramework.determineTestMethodParameterType(testMethod, AbstractXMPPConnection.class);
assertEquals(TestMethodParameterType.unconnectedConnectionSource, determinedParameterType);
}
}