diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c9a123e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: java
+
+services:
+ - docker
+
+script: mvn clean verify
diff --git a/pom.xml b/pom.xml
index c3a3925..189d50c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,7 +47,7 @@
connect-api
2.6.0
provided
-
+
org.apache.kafka
connect-json
@@ -84,6 +84,15 @@
1.7.25
test
+
+
+
+
+ org.testcontainers
+ testcontainers
+ 1.17.3
+ test
+
@@ -97,18 +106,44 @@
-Xlint:unchecked
+
+
maven-surefire-plugin
- 3.0.0-M1
+ 3.0.0-M7
+ ${surefire.jacoco.args}
${project.version}
+
+
+
+ maven-failsafe-plugin
+ 3.0.0-M7
+
+ ${failsafe.jacoco.args}
+
+ ${project.version}
+
+
+
+
+ integration-tests
+
+ integration-test
+ verify
+
+
+
+
+
+
maven-assembly-plugin
- 3.1.1
+ 3.4.1
package
@@ -123,6 +158,110 @@
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.3.0
+
+
+ add-test-source
+ process-test-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.8
+
+
+ before-unit-test-execution
+
+ prepare-agent
+
+
+ ${project.build.directory}/jacoco-output/jacoco-unit-tests.exec
+ surefire.jacoco.args
+
+
+
+ after-unit-test-execution
+ test
+
+ report
+
+
+ ${project.build.directory}/jacoco-output/jacoco-unit-tests.exec
+ ${project.reporting.outputDirectory}/jacoco-unit-test-coverage-report
+
+
+
+ before-integration-test-execution
+ pre-integration-test
+
+ prepare-agent
+
+
+ ${project.build.directory}/jacoco-output/jacoco-integration-tests.exec
+ failsafe.jacoco.args
+
+
+
+ after-integration-test-execution
+ post-integration-test
+
+ report
+
+
+ ${project.build.directory}/jacoco-output/jacoco-integration-tests.exec
+ ${project.reporting.outputDirectory}/jacoco-integration-test-coverage-report
+
+
+
+ merge-unit-and-integration
+ post-integration-test
+
+ merge
+
+
+
+
+ ${project.build.directory}/jacoco-output/
+
+ *.exec
+
+
+
+ ${project.build.directory}/jacoco-output/merged.exec
+
+
+
+ create-merged-report
+ post-integration-test
+
+ report
+
+
+ ${project.build.directory}/jacoco-output/merged.exec
+ ${project.reporting.outputDirectory}/jacoco-merged-test-coverage-report
+
+
+
+
diff --git a/src/integration/java/com/ibm/eventstreams/connect/mqsource/AbstractJMSContextIT.java b/src/integration/java/com/ibm/eventstreams/connect/mqsource/AbstractJMSContextIT.java
new file mode 100644
index 0000000..36c04e3
--- /dev/null
+++ b/src/integration/java/com/ibm/eventstreams/connect/mqsource/AbstractJMSContextIT.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2022 IBM Corporation
+ *
+ * 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 com.ibm.eventstreams.connect.mqsource;
+
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+import javax.jms.Connection;
+import javax.jms.Destination;
+import javax.jms.JMSContext;
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.MessageProducer;
+import javax.jms.Session;
+
+import org.junit.ClassRule;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.WaitingConsumer;
+
+import com.ibm.mq.jms.MQConnectionFactory;
+import com.ibm.msg.client.jms.JmsConnectionFactory;
+import com.ibm.msg.client.jms.JmsFactoryFactory;
+import com.ibm.msg.client.wmq.WMQConstants;
+
+/**
+ * Helper class for integration tests that have a dependency on JMSContext.
+ *
+ * It starts a queue manager in a test container, and uses it to create
+ * a JMSContext instance, that can be used in tests.
+ */
+public class AbstractJMSContextIT {
+
+ private static final String QMGR_NAME = "MYQMGR";
+ private static final String CHANNEL_NAME = "DEV.APP.SVRCONN";
+
+ @ClassRule
+ public static GenericContainer> MQ_CONTAINER = new GenericContainer<>("icr.io/ibm-messaging/mq:latest")
+ .withEnv("LICENSE", "accept")
+ .withEnv("MQ_QMGR_NAME", QMGR_NAME)
+ .withEnv("MQ_ENABLE_EMBEDDED_WEB_SERVER", "false")
+ .withExposedPorts(1414);
+
+ private JMSContext jmsContext;
+
+
+ /**
+ * Returns a JMS context pointing at a developer queue manager running in a
+ * test container.
+ */
+ public JMSContext getJmsContext() throws Exception {
+ if (jmsContext == null) {
+ waitForQueueManagerStartup();
+
+ MQConnectionFactory mqcf = new MQConnectionFactory();
+ mqcf.setTransportType(WMQConstants.WMQ_CM_CLIENT);
+ mqcf.setChannel(CHANNEL_NAME);
+ mqcf.setQueueManager(QMGR_NAME);
+ mqcf.setConnectionNameList(getConnectionName());
+
+ jmsContext = mqcf.createContext();
+ }
+
+ return jmsContext;
+ }
+
+
+ /**
+ * Gets the host port that has been mapped to the default MQ 1414 port in the test container.
+ */
+ public Integer getMQPort() {
+ return MQ_CONTAINER.getMappedPort(1414);
+ }
+
+ public String getQmgrName() {
+ return QMGR_NAME;
+ }
+ public String getChannelName() {
+ return CHANNEL_NAME;
+ }
+ public String getConnectionName() {
+ return "localhost(" + getMQPort().toString() + ")";
+ }
+
+
+ /**
+ * Waits until we see a log line in the queue manager test container that indicates
+ * the queue manager is ready.
+ */
+ private void waitForQueueManagerStartup() throws TimeoutException {
+ WaitingConsumer logConsumer = new WaitingConsumer();
+ MQ_CONTAINER.followOutput(logConsumer);
+ logConsumer.waitUntil(logline -> logline.getUtf8String().contains("AMQ5975I"));
+ }
+
+
+
+
+ /**
+ * Puts all messages to the specified MQ queue. Used in tests to
+ * give the Connector something to get.
+ */
+ public void putAllMessagesToQueue(String queueName, List messages) throws JMSException {
+ Connection connection = null;
+ Session session = null;
+ Destination destination = null;
+ MessageProducer producer = null;
+
+ JmsFactoryFactory ff = JmsFactoryFactory.getInstance(WMQConstants.WMQ_PROVIDER);
+
+ JmsConnectionFactory cf = ff.createConnectionFactory();
+ cf.setStringProperty(WMQConstants.WMQ_HOST_NAME, "localhost");
+ cf.setIntProperty(WMQConstants.WMQ_PORT, getMQPort());
+ cf.setStringProperty(WMQConstants.WMQ_CHANNEL, getChannelName());
+ cf.setIntProperty(WMQConstants.WMQ_CONNECTION_MODE, WMQConstants.WMQ_CM_CLIENT);
+ cf.setStringProperty(WMQConstants.WMQ_QUEUE_MANAGER, getQmgrName());
+ cf.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, false);
+
+ connection = cf.createConnection();
+ session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+
+ destination = session.createQueue(queueName);
+ producer = session.createProducer(destination);
+
+ connection.start();
+
+ for (Message message : messages) {
+ message.setJMSDestination(destination);
+ producer.send(message);
+ }
+
+ connection.close();
+ }
+}
diff --git a/src/integration/java/com/ibm/eventstreams/connect/mqsource/MQSourceTaskAuthIT.java b/src/integration/java/com/ibm/eventstreams/connect/mqsource/MQSourceTaskAuthIT.java
new file mode 100644
index 0000000..d785cfa
--- /dev/null
+++ b/src/integration/java/com/ibm/eventstreams/connect/mqsource/MQSourceTaskAuthIT.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright 2022 IBM Corporation
+ *
+ * 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 com.ibm.eventstreams.connect.mqsource;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.kafka.connect.data.Schema;
+import org.apache.kafka.connect.source.SourceRecord;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.WaitingConsumer;
+
+import com.ibm.mq.MQException;
+import com.ibm.mq.MQMessage;
+import com.ibm.mq.MQQueue;
+import com.ibm.mq.MQQueueManager;
+import com.ibm.mq.constants.MQConstants;
+
+public class MQSourceTaskAuthIT {
+
+ private static final String QMGR_NAME = "MYAUTHQMGR";
+ private static final String QUEUE_NAME = "DEV.QUEUE.2";
+ private static final String CHANNEL_NAME = "DEV.APP.SVRCONN";
+ private static final String APP_PASSWORD = "MySuperSecretPassword";
+
+
+ @ClassRule
+ public static GenericContainer> MQ_CONTAINER = new GenericContainer<>("icr.io/ibm-messaging/mq:latest")
+ .withEnv("LICENSE", "accept")
+ .withEnv("MQ_QMGR_NAME", QMGR_NAME)
+ .withEnv("MQ_ENABLE_EMBEDDED_WEB_SERVER", "false")
+ .withEnv("MQ_APP_PASSWORD", APP_PASSWORD)
+ .withExposedPorts(1414);
+
+
+ @Test
+ public void testAuthenticatedQueueManager() throws Exception {
+ waitForQueueManagerStartup();
+
+ Map connectorProps = new HashMap<>();
+ connectorProps.put("mq.queue.manager", QMGR_NAME);
+ connectorProps.put("mq.connection.mode", "client");
+ connectorProps.put("mq.connection.name.list", "localhost(" + MQ_CONTAINER.getMappedPort(1414).toString() + ")");
+ connectorProps.put("mq.channel.name", CHANNEL_NAME);
+ connectorProps.put("mq.queue", QUEUE_NAME);
+ connectorProps.put("mq.user.authentication.mqcsp", "true");
+ connectorProps.put("mq.user.name", "app");
+ connectorProps.put("mq.password", APP_PASSWORD);
+ connectorProps.put("mq.message.body.jms", "false");
+ connectorProps.put("mq.record.builder", "com.ibm.eventstreams.connect.mqsource.builders.DefaultRecordBuilder");
+
+ MQSourceTask newConnectTask = new MQSourceTask();
+ newConnectTask.start(connectorProps);
+
+ MQMessage message1 = new MQMessage();
+ message1.writeString("hello");
+ MQMessage message2 = new MQMessage();
+ message2.writeString("world");
+ putAllMessagesToQueue(Arrays.asList(message1, message2));
+
+ List kafkaMessages = newConnectTask.poll();
+ assertEquals(2, kafkaMessages.size());
+ for (SourceRecord kafkaMessage : kafkaMessages) {
+ assertNull(kafkaMessage.key());
+ assertEquals(Schema.OPTIONAL_BYTES_SCHEMA, kafkaMessage.valueSchema());
+ }
+
+ assertArrayEquals("hello".getBytes(), (byte[]) kafkaMessages.get(0).value());
+ assertArrayEquals("world".getBytes(), (byte[]) kafkaMessages.get(1).value());
+
+ newConnectTask.stop();
+ }
+
+
+ private void waitForQueueManagerStartup() throws TimeoutException {
+ WaitingConsumer logConsumer = new WaitingConsumer();
+ MQ_CONTAINER.followOutput(logConsumer);
+ logConsumer.waitUntil(logline -> logline.getUtf8String().contains("AMQ5975I"));
+ }
+
+
+ private void putAllMessagesToQueue(List messages) throws MQException {
+ Hashtable