Compare commits

...

22 Commits
draft ... main

Author SHA1 Message Date
Radek Davidek
70c4540c4a added kafka to tests 2026-03-26 10:28:48 +01:00
Radek Davidek
b94a071fda added kafka 2026-03-26 10:21:48 +01:00
Radek Davidek
51ee61e328 changed constants 2026-03-24 17:27:10 +01:00
Radek Davidek
c43a0eacb0 refactor tests 2026-03-24 16:59:34 +01:00
Radek Davidek
923f43e008 IBM MQ SSL keystore rewritten 2026-03-18 14:15:02 +01:00
Radek Davidek
4923d498ed vault implemented 2026-03-17 20:38:42 +01:00
Radek Davidek
4629a2fae7 fixed vault 2026-03-17 20:05:00 +01:00
Radek Davidek
26b6354875 jms browser implemented 2026-03-17 18:59:45 +01:00
Radek Davidek
e7d9acf13b CorrelationId added 2026-03-17 18:45:05 +01:00
Radek Davidek
23a5e9972d fixed iterative payload creation 2026-03-17 17:42:15 +01:00
Radek Davidek
320bde9a39 all basic JMS tests 2026-03-17 16:41:04 +01:00
Radek Davidek
486950c2b4 received jms message is typed 2026-03-17 16:34:39 +01:00
Radek Davidek
97b77a911f fixed JMS Consumer 2026-03-17 16:08:09 +01:00
Radek Davidek
0833bcff06 rewritten connector 2026-03-17 15:27:50 +01:00
Radek Davidek
579246d772 Merge branch 'main' of https://gitea.kamma.cz/kamma/harness.git into main 2026-03-17 15:08:14 +01:00
Radek Davidek
e6dd8c286b first test 2026-03-17 15:08:05 +01:00
Radek Davidek
ae4bb84eef for test purposes removed vault 2026-03-17 15:07:47 +01:00
Radek Davidek
c4492913ad for test purposes removed vault 2026-03-17 15:07:14 +01:00
Radek Davidek
ce54e336ba IBM MQ test drafts 2026-03-17 09:24:25 +01:00
Radek Davidek
ef4562ab74 added IBM MQ connector 2026-03-16 18:59:21 +01:00
Radek Davidek
6cf0aab157 poms modified for local dev 2026-03-16 10:43:07 +01:00
rdavidek
7d0a1b953f initial from moneta 2026-03-14 14:10:31 +01:00
277 changed files with 7837 additions and 2589 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.moneta.demo</groupId>
<artifactId>messaging-connection-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kafka.version>3.7.1</kafka.version>
<confluent.version>7.6.1</confluent.version>
<ibm.mq.version>9.4.2.0</ibm.mq.version>
<jakarta.jms.version>3.1.0</jakarta.jms.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>com.ibm.mq</groupId>
<artifactId>com.ibm.mq.allclient</artifactId>
<version>${ibm.mq.version}</version>
</dependency>
<dependency>
<groupId>jakarta.jms</groupId>
<artifactId>jakarta.jms-api</artifactId>
<version>${jakarta.jms.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>confluent</id>
<name>Confluent Maven Repository</name>
<url>https://packages.confluent.io/maven/</url>
</repository>
</repositories>
</project>

View File

@ -1,69 +0,0 @@
package cz.moneta.demo;
import com.ibm.msg.client.jms.JmsConnectionFactory;
import com.ibm.msg.client.jms.JmsFactoryFactory;
import com.ibm.msg.client.wmq.WMQConstants;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import javax.jms.JMSContext;
import java.util.Properties;
public class MessagingConnectionApp {
public static KafkaProducer<String, String> createKafkaConnection(String bootstrapServers,
String apiKey,
String apiSecret) {
Properties kafkaProps = new Properties();
kafkaProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
kafkaProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
kafkaProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
kafkaProps.put("security.protocol", "SASL_SSL");
kafkaProps.put("sasl.mechanism", "PLAIN");
kafkaProps.put("sasl.jaas.config",
String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
apiKey, apiSecret));
return new KafkaProducer<>(kafkaProps);
}
public static JMSContext createMqConnection(String host,
int port,
String channel,
String queueManager,
String user,
String password) throws Exception {
JmsFactoryFactory factoryFactory = JmsFactoryFactory.getInstance(WMQConstants.WMQ_PROVIDER);
JmsConnectionFactory connectionFactory = factoryFactory.createConnectionFactory();
connectionFactory.setStringProperty(WMQConstants.WMQ_HOST_NAME, host);
connectionFactory.setIntProperty(WMQConstants.WMQ_PORT, port);
connectionFactory.setStringProperty(WMQConstants.WMQ_CHANNEL, channel);
connectionFactory.setStringProperty(WMQConstants.WMQ_QUEUE_MANAGER, queueManager);
connectionFactory.setIntProperty(WMQConstants.WMQ_CONNECTION_MODE, WMQConstants.WMQ_CM_CLIENT);
connectionFactory.setStringProperty(WMQConstants.USERID, user);
connectionFactory.setStringProperty(WMQConstants.PASSWORD, password);
return connectionFactory.createContext(user, password, JMSContext.AUTO_ACKNOWLEDGE);
}
public static void main(String[] args) throws Exception {
String kafkaBootstrap = System.getProperty("kafka.bootstrap", "localhost:9092");
String kafkaApiKey = System.getProperty("kafka.apiKey", "api-key");
String kafkaApiSecret = System.getProperty("kafka.apiSecret", "api-secret");
String mqHost = System.getProperty("mq.host", "localhost");
int mqPort = Integer.parseInt(System.getProperty("mq.port", "1414"));
String mqChannel = System.getProperty("mq.channel", "DEV.APP.SVRCONN");
String mqQueueManager = System.getProperty("mq.queueManager", "QM1");
String mqUser = System.getProperty("mq.user", "app");
String mqPassword = System.getProperty("mq.password", "pass");
try (KafkaProducer<String, String> kafkaProducer = createKafkaConnection(kafkaBootstrap, kafkaApiKey, kafkaApiSecret);
JMSContext mqContext = createMqConnection(mqHost, mqPort, mqChannel, mqQueueManager, mqUser, mqPassword)) {
System.out.println("Kafka connection created: " + (kafkaProducer != null));
System.out.println("IBM MQ connection created: " + (mqContext != null));
}
}
}

View File

@ -29,10 +29,11 @@
<commons-beanutils.version>1.9.3</commons-beanutils.version>
<commons-configuration.version>1.6</commons-configuration.version>
<cxf.version>4.0.3</cxf.version>
<kafka.version>3.7.1</kafka.version>
<confluent.version>7.6.1</confluent.version>
<ibm.mq.version>9.4.5.0</ibm.mq.version>
<jakarta.jms.version>3.1.0</jakarta.jms.version>
<javax.jms.version>2.0.1</javax.jms.version>
<kafka.clients.version>3.7.0</kafka.clients.version>
<confluent.version>7.6.0</confluent.version>
<assertj.version>3.24.2</assertj.version>
</properties>
<dependencies>
@ -270,48 +271,6 @@
<version>${appium-java-client.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-schema-registry-client</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.3</version>
</dependency>
<dependency>
<groupId>com.ibm.mq</groupId>
<artifactId>com.ibm.mq.allclient</artifactId>
<version>${ibm.mq.version}</version>
</dependency>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.jms</groupId>
<artifactId>jakarta.jms-api</artifactId>
<version>${jakarta.jms.version}</version>
</dependency>
<!-- Used for Web Services connector -->
<dependency>
<groupId>org.apache.cxf</groupId>
@ -331,6 +290,44 @@
<version>${cxf.version}</version>
</dependency>
<!-- Used in IbmMqConnector -->
<dependency>
<groupId>com.ibm.mq</groupId>
<artifactId>com.ibm.mq.allclient</artifactId>
<version>${ibm.mq.version}</version>
</dependency>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>${javax.jms.version}</version>
</dependency>
<!-- Kafka dependencies -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.clients.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.3</version>
</dependency>
<!-- AssertJ for advanced assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>
<!-- Used in Wso2ConnectorServlet -->
<dependency>
<groupId>javax.servlet</groupId>
@ -404,23 +401,6 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>

View File

@ -1,264 +1,502 @@
package cz.moneta.test.harness.connectors.messaging;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.jms.BytesMessage;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.JMSProducer;
import javax.jms.JMSRuntimeException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.TextMessage;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.ibm.mq.jms.MQConnectionFactory;
import com.ibm.msg.client.wmq.WMQConstants;
import cz.moneta.test.harness.connectors.Connector;
import cz.moneta.test.harness.exception.MessagingTimeoutException;
import cz.moneta.test.harness.messaging.model.MessageContentType;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import cz.moneta.test.harness.messaging.MessageContentType;
import cz.moneta.test.harness.messaging.MqMessageFormat;
import cz.moneta.test.harness.messaging.ReceivedMessage;
import cz.moneta.test.harness.messaging.exception.MessagingConnectionException;
import cz.moneta.test.harness.messaging.exception.MessagingDestinationException;
import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException;
import cz.moneta.test.harness.support.messaging.ImqRequest;
/**
* IBM MQ connector using JMS client with Jakarta JMS API. Supports
* multi-instance Queue Manager, SSL/TLS, and multiple message formats.
* <p>
* Supported formats: - JSON: JMS TextMessage with plain JSON string (default) -
* XML: JMS TextMessage with XML string - UTF-8 (CCSID 1208): JMS BytesMessage
* with UTF-8 encoding - EBCDIC (CCSID 870): JMS BytesMessage with EBCDIC
* IBM-870 encoding
*/
public class IbmMqConnector implements Connector {
private static final Charset EBCDIC_870 = Charset.forName("IBM870");
private static final Charset UTF_8 = StandardCharsets.UTF_8;
private final MQConnectionFactory connectionFactory;
private final String user;
private final String password;
private final Object contextLock = new Object();
private volatile JMSContext jmsContext;
private static final Logger LOG = LogManager.getLogger(IbmMqConnector.class);
public IbmMqConnector(String host,
int port,
String channel,
String queueManager,
String user,
String password,
String keystorePath,
String keystorePassword,
String cipherSuite) {
this.user = user;
this.password = password;
try {
if (keystorePath != null && !keystorePath.isBlank()) {
System.setProperty("javax.net.ssl.keyStore", keystorePath);
System.setProperty("javax.net.ssl.trustStore", keystorePath);
if (keystorePassword != null) {
System.setProperty("javax.net.ssl.keyStorePassword", keystorePassword);
System.setProperty("javax.net.ssl.trustStorePassword", keystorePassword);
}
}
private static final Charset EBCDIC_870 = Charset.forName("IBM870");
private static final Charset UTF_8 = StandardCharsets.UTF_8;
connectionFactory = new MQConnectionFactory();
connectionFactory.setHostName(host);
connectionFactory.setPort(port);
connectionFactory.setQueueManager(queueManager);
connectionFactory.setChannel(channel);
connectionFactory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
if (user != null && !user.isBlank()) {
connectionFactory.setStringProperty(WMQConstants.USERID, user);
}
if (password != null && !password.isBlank()) {
connectionFactory.setStringProperty(WMQConstants.PASSWORD, password);
}
private static final long DEFAULT_POLL_INTERVAL_MS = 100;
private static final long DEFAULT_MAX_POLL_INTERVAL_MS = 1000;
if (cipherSuite != null && !cipherSuite.isBlank()) {
connectionFactory.setSSLCipherSuite(cipherSuite);
}
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize IBM MQ connection factory", e);
}
}
private static final String TLS_VERSION = "TLSv1.2";
public void send(String queueName,
String payload,
MqMessageFormat format,
Map<String, String> properties) {
switch (Objects.requireNonNull(format, "format")) {
case JSON, XML -> sendTextMessage(queueName, payload, properties);
case EBCDIC_870 -> sendBytesMessage(queueName, payload, EBCDIC_870, 870, properties);
case UTF8_1208 -> sendBytesMessage(queueName, payload, UTF_8, 1208, properties);
}
}
private final MQConnectionFactory connectionFactory;
private JMSContext jmsContext;
private final String queueManager;
private final String user;
private final String password;
public ReceivedMessage receive(String queueName,
String messageSelector,
MqMessageFormat expectedFormat,
Duration timeout) {
JMSContext context = getContext();
Queue queue = context.createQueue("queue:///" + queueName);
try (JMSConsumer consumer = messageSelector == null || messageSelector.isBlank()
? context.createConsumer(queue)
: context.createConsumer(queue, messageSelector)) {
Message message = consumer.receive(Optional.ofNullable(timeout).orElse(Duration.ofSeconds(30)).toMillis());
if (message == null) {
throw new MessagingTimeoutException("Timeout waiting for IBM MQ message from queue: " + queueName);
}
return toReceivedMessage(message, queueName, expectedFormat);
}
}
/**
* Constructor with multi-instance Queue Manager support.
*
* @param connectionNameList Connection name list in format
* "host1(port1),host2(port2)"
* @param channel MQ channel name
* @param queueManager Queue Manager name
* @param user Username for authentication
* @param password Password for authentication
* @param keystorePath Path to SSL keystore (can be null for non-SSL)
* @param keystorePassword Password for SSL keystore
* @param sslCipherSuite SSL cipher suite to use (e.g.,
* "TLS_RSA_WITH_AES_256_CBC_SHA256")
*/
public IbmMqConnector(String connectionNameList, String channel, String queueManager, String user, String password,
String keystorePath, String keystorePassword, String sslCipherSuite) {
this.queueManager = queueManager;
this.user = user;
this.password = password;
public List<ReceivedMessage> browse(String queueName,
Predicate<ReceivedMessage> filter,
MqMessageFormat expectedFormat,
Duration timeout) {
long timeoutMillis = Optional.ofNullable(timeout).orElse(Duration.ofSeconds(30)).toMillis();
long deadline = System.currentTimeMillis() + timeoutMillis;
long backoff = 100;
JMSContext context = getContext();
Queue queue = context.createQueue("queue:///" + queueName);
try {
connectionFactory = new MQConnectionFactory();
connectionFactory.setConnectionNameList(connectionNameList);
connectionFactory.setQueueManager(queueManager);
connectionFactory.setChannel(channel);
connectionFactory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
if (user != null && !user.isBlank()) {
connectionFactory.setStringProperty(WMQConstants.USERID, user);
}
if (password != null && !password.isBlank()) {
connectionFactory.setStringProperty(WMQConstants.PASSWORD, password);
}
while (System.currentTimeMillis() < deadline) {
List<ReceivedMessage> matched = new ArrayList<>();
try (QueueBrowser browser = context.createBrowser(queue)) {
Enumeration<?> messages = browser.getEnumeration();
while (messages.hasMoreElements()) {
Message message = (Message) messages.nextElement();
ReceivedMessage receivedMessage = toReceivedMessage(message, queueName, expectedFormat);
if (filter == null || filter.test(receivedMessage)) {
matched.add(receivedMessage);
}
}
} catch (JMSException e) {
throw new IllegalStateException("Failed to browse IBM MQ queue: " + queueName, e);
}
if (keystorePath != null && !keystorePath.isBlank() && keystorePassword != null
&& !keystorePassword.isBlank()) {
connectionFactory.setSSLSocketFactory(getSslSocketFactory(keystorePath, keystorePassword));
}
if (!matched.isEmpty()) {
return matched;
}
if (sslCipherSuite != null && !sslCipherSuite.isBlank()) {
connectionFactory.setSSLCipherSuite(sslCipherSuite);
}
try {
TimeUnit.MILLISECONDS.sleep(backoff);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
break;
}
backoff = Math.min(backoff * 2, 1000);
}
throw new MessagingTimeoutException("Timeout waiting for IBM MQ message from queue: " + queueName);
}
// Initialize JMS context
connect();
@Override
public void close() {
JMSContext context = jmsContext;
if (context != null) {
context.close();
}
}
} catch (Exception e) {
throw new MessagingConnectionException("Failed to create IBM MQ connection to " + queueManager, e);
}
}
private void sendTextMessage(String queueName, String payload, Map<String, String> properties) {
JMSContext context = getContext();
JMSProducer producer = context.createProducer();
TextMessage message = context.createTextMessage(payload);
applyProperties(message, properties);
producer.send(context.createQueue("queue:///" + queueName), message);
}
/**
* Connect to IBM MQ.
*/
private void connect() {
try {
this.jmsContext = connectionFactory.createContext(user, password, JMSContext.AUTO_ACKNOWLEDGE);
this.jmsContext.start();
LOG.info("Connected to IBM MQ: {}", queueManager);
} catch (Exception e) {
throw new MessagingConnectionException(
"Failed to connect to IBM MQ: " + queueManager + " - " + e.getMessage(), e);
}
}
private void sendBytesMessage(String queueName,
String payload,
Charset charset,
int ccsid,
Map<String, String> properties) {
try {
JMSContext context = getContext();
JMSProducer producer = context.createProducer();
BytesMessage message = context.createBytesMessage();
message.writeBytes(Optional.ofNullable(payload).orElse("").getBytes(charset));
message.setIntProperty(WMQConstants.JMS_IBM_CHARACTER_SET, ccsid);
applyProperties(message, properties);
producer.send(context.createQueue("queue:///" + queueName), message);
} catch (JMSException e) {
throw new IllegalStateException("Failed to send bytes message to IBM MQ queue: " + queueName, e);
}
}
/**
* Send a JSON or XML message as TextMessage.
*/
private void sendTextMessage(String queueName, String payload, Map<String, String> properties) {
javax.jms.Queue queue = getQueue(queueName);
private void applyProperties(Message message, Map<String, String> properties) {
Optional.ofNullable(properties).orElseGet(Collections::emptyMap)
.forEach((key, value) -> {
try {
message.setStringProperty(key, String.valueOf(value));
} catch (JMSException e) {
throw new IllegalStateException("Failed to set JMS property: " + key, e);
}
});
}
TextMessage message = jmsContext.createTextMessage(payload);
private ReceivedMessage toReceivedMessage(Message message, String queueName, MqMessageFormat format) {
try {
Map<String, String> headers = new LinkedHashMap<>();
Enumeration<?> names = message.getPropertyNames();
while (names.hasMoreElements()) {
String name = String.valueOf(names.nextElement());
headers.put(name, String.valueOf(message.getObjectProperty(name)));
}
// Set JMS properties
if (properties != null) {
for (Map.Entry<String, String> entry : properties.entrySet()) {
try {
if (entry.getKey().equals(ImqRequest.PROP_JMS_CORRELATION_ID)) {
message.setJMSCorrelationID(entry.getValue());
continue;
} else if (entry.getKey().equals(ImqRequest.PROP_JMS_TYPE)) {
message.setJMSType(entry.getValue());
continue;
} else if (entry.getKey().equals(ImqRequest.PROP_JMS_MESSAGE_ID)) {
message.setJMSMessageID(entry.getValue());
continue;
}
message.setStringProperty(entry.getKey(), entry.getValue());
} catch (JMSException e) {
LOG.warn("Failed to set property: {}", entry.getKey(), e);
}
}
}
String body = decodeMessage(message, format);
MessageContentType contentType = resolveContentType(message, format);
return new ReceivedMessage(body, contentType, headers, message.getJMSTimestamp(), queueName);
} catch (JMSException e) {
throw new IllegalStateException("Failed to decode IBM MQ message", e);
}
}
try {
jmsContext.createProducer().send(queue, message);
} catch (RuntimeException e) {
throw new MessagingDestinationException("Failed to send message to queue: " + queueName, e);
}
LOG.debug("Sent JSON/XML message to queue: {}", queueName);
}
private MessageContentType resolveContentType(Message message, MqMessageFormat expectedFormat) {
if (message instanceof TextMessage) {
return expectedFormat == MqMessageFormat.XML ? MessageContentType.XML : MessageContentType.JSON;
}
if (expectedFormat == MqMessageFormat.XML) {
return MessageContentType.XML;
}
if (expectedFormat == MqMessageFormat.JSON) {
return MessageContentType.JSON;
}
return MessageContentType.RAW_TEXT;
}
/**
* Send a message as BytesMessage with specific encoding and CCSID.
*/
private void sendBytesMessage(String queueName, String payload, Charset charset, int ccsid,
Map<String, String> properties) {
javax.jms.Queue queue = getQueue(queueName);
private String decodeMessage(Message jmsMessage, MqMessageFormat format) {
try {
if (jmsMessage instanceof TextMessage textMessage) {
return textMessage.getText();
}
BytesMessage message = jmsContext.createBytesMessage();
if (jmsMessage instanceof BytesMessage bytesMessage) {
byte[] data = new byte[(int) bytesMessage.getBodyLength()];
bytesMessage.readBytes(data);
Charset charset = switch (format) {
case EBCDIC_870 -> EBCDIC_870;
case UTF8_1208, JSON, XML -> UTF_8;
};
return new String(data, charset);
}
return "";
} catch (Exception e) {
throw new IllegalStateException("Failed to decode JMS message", e);
}
}
// Convert payload to bytes using specified charset
byte[] bytes = payload.getBytes(charset);
try {
message.writeBytes(bytes);
message.setIntProperty("CCSID", ccsid);
} catch (JMSException e) {
throw new MessagingDestinationException("Failed to create bytes message", e);
}
private JMSContext getContext() {
JMSContext current = jmsContext;
if (current == null) {
synchronized (contextLock) {
current = jmsContext;
if (current == null) {
jmsContext = current = connectionFactory.createContext(user, password, JMSContext.AUTO_ACKNOWLEDGE);
}
}
}
return current;
}
// Set JMS properties
if (properties != null) {
for (Map.Entry<String, String> entry : properties.entrySet()) {
try {
if (entry.getKey().equals(ImqRequest.PROP_JMS_CORRELATION_ID)) {
message.setJMSCorrelationID(entry.getValue());
continue;
} else if (entry.getKey().equals(ImqRequest.PROP_JMS_TYPE)) {
message.setJMSType(entry.getValue());
continue;
} else if (entry.getKey().equals(ImqRequest.PROP_JMS_MESSAGE_ID)) {
message.setJMSMessageID(entry.getValue());
continue;
}
message.setStringProperty(entry.getKey(), entry.getValue());
} catch (JMSException e) {
LOG.warn("Failed to set property: {}", entry.getKey(), e);
}
}
}
try {
jmsContext.createProducer().send(queue, message);
} catch (RuntimeException e) {
throw new MessagingDestinationException("Failed to send message to queue: " + queueName, e);
}
LOG.debug("Sent {} message to queue: {}", charset, queueName);
}
/**
* Send a message to a queue with specified format.
*
* @param queueName Queue name
* @param payload Message payload
* @param format Message format (JSON, XML, EBCDIC_870, UTF8_1208)
* @param properties JMS properties to set
*/
public void send(String queueName, String payload, MqMessageFormat format, Map<String, String> properties) {
switch (format) {
case JSON, XML -> sendTextMessage(queueName, payload, properties);
case EBCDIC_870 -> sendBytesMessage(queueName, payload, EBCDIC_870, 870, properties);
case UTF8_1208 -> sendBytesMessage(queueName, payload, UTF_8, 1208, properties);
}
}
/**
* Receive a message from a queue with timeout.
*
* @param queueName Queue name
* @param messageSelector JMS message selector (optional)
* @param format Expected message format
* @param timeout Timeout duration
* @return Received message
*/
public ReceivedMessage receive(String queueName, String messageSelector, MqMessageFormat format,
java.time.Duration timeout) {
long timeoutMs = timeout.toMillis();
javax.jms.Queue queue = getQueue(queueName);
JMSConsumer consumer = (messageSelector == null || messageSelector.isBlank() ? jmsContext.createConsumer(queue)
: jmsContext.createConsumer(queue, messageSelector));
AtomicBoolean messageFound = new AtomicBoolean(false);
ReceivedMessage received = null;
long pollInterval = DEFAULT_POLL_INTERVAL_MS;
long remainingTime = timeoutMs;
try {
while (remainingTime > 0 && !messageFound.get()) {
Message message = consumer.receive(remainingTime);
if (message != null) {
received = decodeMessage(message, queueName, format);
messageFound.set(true);
} else {
// Exponential backoff
pollInterval = Math.min(pollInterval * 2, DEFAULT_MAX_POLL_INTERVAL_MS);
remainingTime -= pollInterval;
}
}
if (received == null) {
throw new MessagingTimeoutException("No message matching filter found on queue '" + queueName
+ "' within " + timeout.toMillis() + "ms");
}
return received;
} catch (MessagingTimeoutException e) {
throw e;
} catch (Exception e) {
throw new MessagingDestinationException("Failed to receive message from queue: " + queueName, e);
} finally {
try {
consumer.close();
} catch (JMSRuntimeException e) {
LOG.warn("Failed to close consumer", e);
}
}
}
/**
* Browse a queue (non-destructive read).
*
* @param queueName Queue name
* @param messageSelector JMS message selector (optional)
* @param format Expected message format
* @param maxMessages Maximum number of messages to browse
* @return List of received messages
*/
public List<ReceivedMessage> browse(String queueName, String messageSelector, MqMessageFormat format,
int maxMessages) {
List<ReceivedMessage> messages = new ArrayList<>();
javax.jms.Queue queue = getQueue(queueName);
try (javax.jms.QueueBrowser browser = (messageSelector == null || messageSelector.isBlank())
? jmsContext.createBrowser(queue)
: jmsContext.createBrowser(queue, messageSelector)) {
Enumeration<?> enumeration = browser.getEnumeration();
int count = 0;
while (enumeration.hasMoreElements() && count < maxMessages) {
Message message = (Message) enumeration.nextElement();
if (message != null) {
ReceivedMessage received = decodeMessage(message, queueName, format);
messages.add(received);
count++;
}
}
return messages;
} catch (JMSException e) {
throw new MessagingDestinationException("Failed to browse queue: " + queueName, e);
}
}
/**
* Decode a JMS message to ReceivedMessage.
*/
private ReceivedMessage decodeMessage(Message jmsMessage, String queueName, MqMessageFormat format) {
long timestamp;
try {
timestamp = jmsMessage.getJMSTimestamp();
} catch (JMSException e) {
timestamp = System.currentTimeMillis();
}
if (timestamp == 0) {
timestamp = System.currentTimeMillis();
}
String body;
MessageContentType contentType;
Map<String, String> headers = new HashMap<>();
// Extract JMS properties as headers
extractJmsProperties(jmsMessage, headers);
if (jmsMessage instanceof TextMessage textMessage) {
try {
body = textMessage.getText();
} catch (JMSException e) {
throw new RuntimeException("Failed to read text message body", e);
}
contentType = switch (format) {
case XML -> MessageContentType.XML;
default -> MessageContentType.JSON;
};
} else if (jmsMessage instanceof BytesMessage bytesMessage) {
int ccsid;
try {
ccsid = bytesMessage.getIntProperty("CCSID");
} catch (JMSException e) {
ccsid = 1208; // default UTF-8
}
body = decodeBytesMessage(bytesMessage, ccsid);
contentType = MessageContentType.RAW_TEXT;
} else {
try {
throw new IllegalArgumentException("Unsupported message type: " + jmsMessage.getJMSType());
} catch (JMSException e) {
throw new IllegalArgumentException("Unsupported message type", e);
}
}
return new ReceivedMessage(body, contentType, headers, timestamp, queueName, null);
}
/**
* Decode BytesMessage body based on CCSID.
*/
private String decodeBytesMessage(BytesMessage bytesMessage, int ccsid) {
try {
long bodyLength;
try {
bodyLength = bytesMessage.getBodyLength();
} catch (JMSException e) {
throw new RuntimeException("Failed to get message body length", e);
}
byte[] data = new byte[(int) bodyLength];
bytesMessage.readBytes(data);
Charset charset = switch (ccsid) {
case 870 -> EBCDIC_870;
case 1208 -> UTF_8;
default -> UTF_8;
};
return new String(data, charset);
} catch (JMSException e) {
throw new RuntimeException("Failed to read BytesMessage body", e);
}
}
/**
* Extract JMS properties as headers.
*/
@SuppressWarnings("unchecked")
private void extractJmsProperties(Message message, Map<String, String> headers) {
try {
// Common JMS headers
headers.put("JMSMessageID", message.getJMSMessageID());
try {
headers.put("JMSType", message.getJMSType() != null ? message.getJMSType() : "");
} catch (JMSException e) {
headers.put("JMSType", "");
}
try {
headers.put("JMSDestination",
message.getJMSDestination() != null ? message.getJMSDestination().toString() : "");
} catch (JMSException e) {
headers.put("JMSDestination", "");
}
try {
headers.put("JMSDeliveryMode", String.valueOf(message.getJMSDeliveryMode()));
} catch (JMSException e) {
headers.put("JMSDeliveryMode", "");
}
try {
headers.put("JMSPriority", String.valueOf(message.getJMSPriority()));
} catch (JMSException e) {
headers.put("JMSPriority", "");
}
try {
headers.put("JMSTimestamp", String.valueOf(message.getJMSTimestamp()));
} catch (JMSException e) {
headers.put("JMSTimestamp", "");
}
// Extract custom properties
Enumeration<String> propertyNames = (Enumeration<String>) message.getPropertyNames();
while (propertyNames.hasMoreElements()) {
String propName = propertyNames.nextElement();
Object propValue = message.getObjectProperty(propName);
if (propValue != null) {
headers.put(propName, propValue.toString());
}
}
} catch (JMSException e) {
LOG.warn("Failed to extract JMS properties", e);
}
}
/**
* Get Queue object from queue name.
*/
private javax.jms.Queue getQueue(String queueName) {
return jmsContext.createQueue(queueName);
}
@Override
public void close() {
if (jmsContext != null) {
try {
jmsContext.close();
LOG.info("Closed connection to IBM MQ: {}", queueManager);
} catch (Exception e) {
LOG.error("Failed to close IBM MQ connection", e);
}
}
}
private SSLSocketFactory getSslSocketFactory(String keystorePath, String keystorePassword) throws Exception {
// --- keystore ---
KeyStore keyStore = KeyStore.getInstance("JKS");
InputStream ksStream = IbmMqConnector.class.getClassLoader().getResourceAsStream(keystorePath);
if (ksStream == null) {
throw new IllegalStateException("Keystore not found: " + keystorePath);
}
keyStore.load(ksStream, keystorePassword.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keystorePassword.toCharArray());
// --- truststore ---
KeyStore trustStore = KeyStore.getInstance("JKS");
InputStream tsStream = IbmMqConnector.class.getClassLoader().getResourceAsStream(keystorePath);
if (tsStream == null) {
throw new IllegalStateException("Truststore not found: " + keystorePath);
}
trustStore.load(tsStream, keystorePassword.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// --- SSL context ---
SSLContext sslContext = SSLContext.getInstance(TLS_VERSION);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
}

View File

@ -0,0 +1,115 @@
package cz.moneta.test.harness.connectors.messaging;
import com.google.gson.*;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.JsonDecoder;
import java.util.ArrayList;
import java.util.List;
public class JsonToAvroConverter {
protected static GenericRecord processJson(String json, Schema schema) throws IllegalArgumentException, JsonSchemaException {
GenericRecord result = (GenericRecord) jsonElementToAvro(JsonParser.parseString(json), schema);
return result;
}
private static Object jsonElementToAvro(JsonElement element, Schema schema) throws JsonSchemaException {
boolean schemaIsNullable = isNullable(schema);
if (schemaIsNullable) {
schema = typeFromNullable(schema);
}
if (element == null || element.isJsonNull()) {
if (!schemaIsNullable) {
throw new JsonSchemaException("The element is not nullable in Avro schema.");
}
return null;
} else if (element.isJsonObject()) {
if (schema.getType() != Schema.Type.RECORD) {
throw new JsonSchemaException(
String.format("The element `%s` doesn't match Avro type RECORD", element));
}
return jsonObjectToAvro(element.getAsJsonObject(), schema);
} else if (element.isJsonArray()) {
if (schema.getType() != Schema.Type.ARRAY) {
throw new JsonSchemaException(
String.format("The element `%s` doesn't match Avro type ARRAY", element));
}
JsonArray jsonArray = element.getAsJsonArray();
List<Object> avroArray = new ArrayList<>(jsonArray.size());
for (JsonElement e : element.getAsJsonArray()) {
avroArray.add(jsonElementToAvro(e, schema.getElementType()));
}
return avroArray;
} else if (element.isJsonPrimitive()) {
return jsonPrimitiveToAvro(element.getAsJsonPrimitive(), schema);
} else {
throw new JsonSchemaException(
String.format(
"The Json element `%s` is of an unknown class %s", element, element.getClass()));
}
}
private static GenericRecord jsonObjectToAvro(JsonObject jsonObject, Schema schema) throws JsonSchemaException {
GenericRecord avroRecord = new GenericData.Record(schema);
for (Schema.Field field : schema.getFields()) {
avroRecord.put(field.name(), jsonElementToAvro(jsonObject.get(field.name()), field.schema()));
}
return avroRecord;
}
private static boolean isNullable(Schema type) {
return type.getType() == Schema.Type.NULL
|| type.getType() == Schema.Type.UNION
&& type.getTypes().stream().anyMatch(JsonToAvroConverter::isNullable);
}
private static Schema typeFromNullable(Schema type) {
if (type.getType() == Schema.Type.UNION) {
return typeFromNullable(
type.getTypes().stream()
.filter(t -> t.getType() != Schema.Type.NULL)
.findFirst()
.orElseThrow(
() ->
new IllegalStateException(
String.format("Type `%s` should have a non null subtype", type))));
}
return type;
}
private static Object jsonPrimitiveToAvro(JsonPrimitive primitive, Schema schema){
switch (schema.getType()) {
case NULL:
return null;
case STRING:
return primitive.getAsString();
case BOOLEAN:
return primitive.getAsBoolean();
case INT:
return primitive.getAsInt();
case LONG:
return primitive.getAsLong();
case FLOAT:
return primitive.getAsFloat();
case DOUBLE:
return primitive.getAsDouble();
default:
return primitive.getAsString();
}
}
private static class JsonSchemaException extends Exception {
JsonSchemaException(String message) {
super(message);
}
}
}

View File

@ -1,18 +1,16 @@
package cz.moneta.test.harness.connectors.messaging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.moneta.test.harness.connectors.Connector;
import cz.moneta.test.harness.exception.MessagingTimeoutException;
import cz.moneta.test.harness.messaging.model.MessageContentType;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;
import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig;
import io.confluent.kafka.serializers.KafkaAvroDeserializer;
import io.confluent.kafka.serializers.KafkaAvroDeserializerConfig;
import io.confluent.kafka.serializers.KafkaAvroSerializer;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import org.apache.avro.generic.GenericRecord;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
@ -22,332 +20,329 @@ import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import cz.moneta.test.harness.messaging.exception.MessagingConnectionException;
import cz.moneta.test.harness.messaging.exception.MessagingDestinationException;
import cz.moneta.test.harness.messaging.exception.MessagingSchemaException;
import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException;
import cz.moneta.test.harness.support.messaging.kafka.MessageContentType;
import cz.moneta.test.harness.support.messaging.kafka.ReceivedMessage;
import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;
import io.confluent.kafka.serializers.KafkaAvroDeserializer;
import io.confluent.kafka.serializers.KafkaAvroSerializer;
public class KafkaConnector implements Connector {
/**
* Kafka connector for sending and receiving messages.
* Supports Avro serialization with Confluent Schema Registry.
* <p>
* Uses manual partition assignment (no consumer group) for test isolation.
* Each receive operation creates a new consumer to avoid offset sharing.
*/
public class KafkaConnector implements cz.moneta.test.harness.connectors.Connector {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Logger LOG = LoggerFactory.getLogger(KafkaConnector.class);
private final Properties producerProps = new Properties();
private final Properties consumerProps = new Properties();
private final Properties producerConfig;
private final Properties consumerConfig;
private final String schemaRegistryUrl;
private final CachedSchemaRegistryClient schemaRegistryClient;
private volatile KafkaProducer<String, GenericRecord> producer;
private final Object producerLock = new Object();
private KafkaProducer<String, GenericRecord> producer;
/**
* Creates a new KafkaConnector.
*
* @param bootstrapServers Kafka bootstrap servers
* @param apiKey Kafka API key
* @param apiSecret Kafka API secret
* @param schemaRegistryUrl Schema Registry URL
* @param schemaRegistryApiKey Schema Registry API key
* @param schemaRegistryApiSecret Schema Registry API secret
*/
public KafkaConnector(String bootstrapServers,
String apiKey,
String apiSecret,
String schemaRegistryUrl,
String schemaRegistryApiKey,
String schemaRegistryApiSecret) {
String jaasConfig = String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
apiKey,
apiSecret);
String schemaRegistryAuth = schemaRegistryApiKey + ":" + schemaRegistryApiSecret;
this.schemaRegistryUrl = schemaRegistryUrl;
this.schemaRegistryClient = new CachedSchemaRegistryClient(
Collections.singletonList(schemaRegistryUrl), 100, new HashMap<>());
producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
producerProps.put("security.protocol", "SASL_SSL");
producerProps.put("sasl.mechanism", "PLAIN");
producerProps.put("sasl.jaas.config", jaasConfig);
producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class.getName());
producerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, schemaRegistryUrl);
producerProps.put(AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE, "USER_INFO");
producerProps.put(AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG, schemaRegistryAuth);
producerProps.put("auto.register.schemas", "false");
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
consumerProps.put("security.protocol", "SASL_SSL");
consumerProps.put("sasl.mechanism", "PLAIN");
consumerProps.put("sasl.jaas.config", jaasConfig);
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class.getName());
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
consumerProps.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, "false");
consumerProps.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, schemaRegistryUrl);
consumerProps.put(AbstractKafkaSchemaSerDeConfig.BASIC_AUTH_CREDENTIALS_SOURCE, "USER_INFO");
consumerProps.put(AbstractKafkaSchemaSerDeConfig.USER_INFO_CONFIG, schemaRegistryAuth);
this.schemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryUrl, 128);
this.producerConfig = createProducerConfig(bootstrapServers, apiKey, apiSecret);
this.consumerConfig = createConsumerConfig(bootstrapServers, schemaRegistryApiKey, schemaRegistryApiSecret);
}
public void send(String topic, String key, String jsonPayload, Map<String, String> headers) {
Objects.requireNonNull(topic, "topic");
Schema schema = getSchemaForTopic(topic);
GenericRecord record = jsonToAvro(jsonPayload, schema);
ProducerRecord<String, GenericRecord> producerRecord = new ProducerRecord<>(topic, key, record);
Optional.ofNullable(headers).orElseGet(HashMap::new)
.forEach((headerKey, headerValue) -> producerRecord.headers()
.add(headerKey, String.valueOf(headerValue).getBytes(StandardCharsets.UTF_8)));
/**
* Creates producer configuration.
*/
private Properties createProducerConfig(String bootstrapServers, String apiKey, String apiSecret) {
Properties config = new Properties();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class);
config.put("schema.registry.url", schemaRegistryUrl);
config.put(ProducerConfig.ACKS_CONFIG, "all");
config.put(ProducerConfig.LINGER_MS_CONFIG, 1);
config.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// SASL/PLAIN authentication
// config.put("security.protocol", "SASL_SSL");
// config.put("sasl.mechanism", "PLAIN");
// config.put("sasl.jaas.config",
// "org.apache.kafka.common.security.plain.PlainLoginModule required " +
// "username=\"" + apiKey + "\" password=\"" + apiSecret + "\";");
// SSL configuration
// config.put("ssl.endpoint.identification.algorithm", "https");
return config;
}
/**
* Creates consumer configuration.
*/
private Properties createConsumerConfig(String bootstrapServers, String apiKey, String apiSecret) {
Properties config = new Properties();
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class);
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "none");
config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// SASL/PLAIN authentication
config.put("security.protocol", "SASL_SSL");
config.put("sasl.mechanism", "PLAIN");
config.put("sasl.jaas.config",
"org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"" + apiKey + "\" password=\"" + apiSecret + "\";");
// Schema Registry for deserialization
config.put("schema.registry.url", schemaRegistryUrl);
config.put("specific.avro.reader", false);
// SSL configuration
config.put("ssl.endpoint.identification.algorithm", "https");
return config;
}
/**
* Sends a message to a Kafka topic.
*/
public void send(String topic, String key, String jsonPayload, Map<String, String> headers) {
try {
getProducer().send(producerRecord).get(30, TimeUnit.SECONDS);
org.apache.avro.Schema schema = getSchemaForTopic(topic);
GenericRecord record = jsonToAvro(jsonPayload, schema);
ProducerRecord<String, GenericRecord> producerRecord =
new ProducerRecord<>(topic, key, record);
// Add headers
if (headers != null) {
Headers kafkaHeaders = producerRecord.headers();
headers.forEach((k, v) ->
kafkaHeaders.add(k, v.getBytes(StandardCharsets.UTF_8)));
}
// Send and wait for confirmation
getProducer().send(producerRecord, (metadata, exception) -> {
if (exception != null) {
LOG.error("Failed to send message", exception);
} else {
LOG.debug("Message sent to topic {} partition {} offset {}",
metadata.topic(), metadata.partition(), metadata.offset());
}
}).get(10, java.util.concurrent.TimeUnit.SECONDS);
} catch (ExecutionException e) {
throw new MessagingConnectionException(
"Failed to send message to topic " + topic, e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingConnectionException(
"Interrupted while sending message to topic " + topic, e);
} catch (MessagingSchemaException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to send Kafka message to topic: " + topic, e);
throw new MessagingSchemaException(
"Failed to convert JSON to Avro for topic " + topic, e);
}
}
/**
* Receives a message from a Kafka topic matching the filter.
*/
public List<ReceivedMessage> receive(String topic,
Predicate<ReceivedMessage> filter,
Duration timeout) {
long timeoutMillis = Optional.ofNullable(timeout).orElse(Duration.ofSeconds(30)).toMillis();
long deadline = System.currentTimeMillis() + timeoutMillis;
long backoff = 100;
KafkaConsumer<String, GenericRecord> consumer = null;
try {
consumer = new KafkaConsumer<>(consumerConfig);
try (KafkaConsumer<String, Object> consumer = createConsumer()) {
List<TopicPartition> partitions = consumer.partitionsFor(topic).stream()
.map(info -> new TopicPartition(topic, info.partition()))
.toList();
consumer.assign(partitions);
consumer.seekToEnd(partitions);
while (System.currentTimeMillis() < deadline) {
ConsumerRecords<String, Object> records = consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
for (ConsumerRecord<String, Object> record : records) {
ReceivedMessage message = toReceivedMessage(record);
if (filter == null || filter.test(message)) {
return List.of(message);
}
}
backoff = 100;
continue;
}
TimeUnit.MILLISECONDS.sleep(backoff);
backoff = Math.min(backoff * 2, 1000);
// Get partitions for the topic
List<TopicPartition> partitions = getPartitionsForTopic(consumer, topic);
if (partitions.isEmpty()) {
throw new MessagingDestinationException(
"Topic '" + topic + "' does not exist or has no partitions");
}
} catch (MessagingTimeoutException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to receive Kafka message from topic: " + topic, e);
}
throw new MessagingTimeoutException("Timeout waiting for Kafka message from topic: " + topic);
}
public Map<TopicPartition, Long> saveOffsets(String topic) {
try (KafkaConsumer<String, Object> consumer = createConsumer()) {
List<TopicPartition> partitions = consumer.partitionsFor(topic).stream()
.map(info -> new TopicPartition(topic, info.partition()))
.toList();
// Assign partitions and seek to end
consumer.assign(partitions);
consumer.seekToEnd(partitions);
Map<TopicPartition, Long> offsets = new HashMap<>();
partitions.forEach(partition -> offsets.put(partition, consumer.position(partition)));
return offsets;
// consumer.seekToBeginning(partitions);
consumer.seekToBeginning(partitions);
// Poll loop with exponential backoff
long startTime = System.currentTimeMillis();
Duration pollInterval = Duration.ofMillis(100);
Duration maxPollInterval = Duration.ofSeconds(1);
while (Duration.ofMillis(System.currentTimeMillis() - startTime).compareTo(timeout) < 0) {
ConsumerRecords<String, GenericRecord> records = consumer.poll(pollInterval);
for (ConsumerRecord<String, GenericRecord> record : records) {
ReceivedMessage message = convertToReceivedMessage(record, topic);
if (filter.test(message)) {
LOG.debug("Found matching message on topic {} partition {} offset {}",
record.topic(), record.partition(), record.offset());
return Collections.singletonList(message);
}
}
// Exponential backoff
pollInterval = Duration.ofMillis(
Math.min(pollInterval.toMillis() * 2, maxPollInterval.toMillis()));
}
throw new MessagingTimeoutException(
"No message matching filter found on topic '" + topic + "' within " + timeout);
} finally {
if (consumer != null) {
consumer.close();
}
}
}
/**
* Gets partitions for a topic.
*/
private List<TopicPartition> getPartitionsForTopic(KafkaConsumer<?, ?> consumer, String topic) {
List<TopicPartition> partitions = new ArrayList<>();
List<org.apache.kafka.common.PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
if (partitionInfos != null) {
for (org.apache.kafka.common.PartitionInfo partitionInfo : partitionInfos) {
partitions.add(new TopicPartition(topic, partitionInfo.partition()));
}
}
return partitions;
}
/**
* Saves current offsets for a topic.
*/
public Map<TopicPartition, Long> saveOffsets(String topic) {
return new HashMap<>();
}
/**
* Closes the connector and releases resources.
*/
@Override
public void close() {
KafkaProducer<String, GenericRecord> current = producer;
if (current != null) {
current.close(Duration.ofSeconds(5));
if (producer != null) {
producer.close();
}
}
/**
* Gets or creates the producer (singleton, thread-safe).
*/
private KafkaProducer<String, GenericRecord> getProducer() {
KafkaProducer<String, GenericRecord> current = producer;
if (current == null) {
synchronized (producerLock) {
current = producer;
if (current == null) {
producer = current = new KafkaProducer<>(producerProps);
if (producer == null) {
synchronized (this) {
if (producer == null) {
producer = new KafkaProducer<>(producerConfig);
}
}
}
return current;
return producer;
}
private KafkaConsumer<String, Object> createConsumer() {
Properties properties = new Properties();
properties.putAll(consumerProps);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "harness-" + UUID.randomUUID());
return new KafkaConsumer<>(properties);
}
private ReceivedMessage toReceivedMessage(ConsumerRecord<String, Object> record) {
String body = convertValueToJson(record.value());
Map<String, String> headers = new LinkedHashMap<>();
for (Header header : record.headers()) {
headers.put(header.key(), new String(header.value(), StandardCharsets.UTF_8));
}
return new ReceivedMessage(body,
MessageContentType.JSON,
headers,
record.timestamp(),
record.topic());
}
private String convertValueToJson(Object value) {
try {
if (value instanceof GenericRecord genericRecord) {
return avroToJson(genericRecord);
}
if (value instanceof CharSequence) {
return value.toString();
}
return OBJECT_MAPPER.writeValueAsString(value);
} catch (Exception e) {
throw new IllegalStateException("Failed to convert Kafka payload to JSON", e);
}
}
private Schema getSchemaForTopic(String topic) {
/**
* Retrieves schema from Schema Registry based on topic name.
*/
private org.apache.avro.Schema getSchemaForTopic(String topic) {
String subject = topic + "-value";
try {
// Get all versions and use the latest one
java.util.List<Integer> versions = schemaRegistryClient.getAllVersions(subject);
int latestVersion = versions.get(versions.size() - 1);
io.confluent.kafka.schemaregistry.client.rest.entities.Schema confluentSchema =
schemaRegistryClient.getByVersion(subject, latestVersion, false);
String schemaString = confluentSchema.getSchema();
return new Schema.Parser().parse(schemaString);
io.confluent.kafka.schemaregistry.client.SchemaMetadata metadata =
schemaRegistryClient.getLatestSchemaMetadata(subject);
return new org.apache.avro.Schema.Parser().parse(metadata.getSchema());
} catch (Exception e) {
throw new IllegalStateException("Failed to get schema for subject: " + subject, e);
if (e.getMessage() != null && e.getMessage().contains("404")) {
throw new MessagingSchemaException(
"Schema not found for subject '" + subject + "' in Schema Registry. " +
"Make sure the topic exists and schema is registered.");
}
throw new MessagingSchemaException(
"Failed to retrieve schema for topic '" + topic + "'", e);
}
}
/**
* Converts JSON string to Avro GenericRecord.
*/
private GenericRecord jsonToAvro(String json, org.apache.avro.Schema schema) {
try {
GenericRecord genericRecord = JsonToAvroConverter.processJson(json, schema);
return genericRecord;
} catch (Exception e) {
throw new MessagingSchemaException("Failed to convert JSON to Avro: " + e.getMessage(), e);
}
}
/**
* Converts Kafka ConsumerRecord to ReceivedMessage.
*/
private ReceivedMessage convertToReceivedMessage(ConsumerRecord<String, GenericRecord> record, String topic) {
try {
String jsonBody = avroToJson(record.value());
Map<String, String> headers = new HashMap<>();
Headers kafkaHeaders = record.headers();
for (org.apache.kafka.common.header.Header header : kafkaHeaders) {
if (header.value() != null) {
headers.put(header.key(), new String(header.value(), StandardCharsets.UTF_8));
}
}
return ReceivedMessage.builder()
.body(jsonBody)
.contentType(MessageContentType.JSON)
.headers(headers)
.timestamp(record.timestamp())
.source(topic)
.key(record.key())
.build();
} catch (Exception e) {
LOG.error("Failed to convert Avro record to ReceivedMessage", e);
throw new RuntimeException("Failed to convert message", e);
}
}
/**
* Converts Avro GenericRecord to JSON string.
*/
private String avroToJson(GenericRecord record) {
try {
return OBJECT_MAPPER.writeValueAsString(convertAvroObject(record));
return record.toString();
} catch (Exception e) {
throw new IllegalStateException("Failed to convert Avro record to JSON", e);
throw new RuntimeException("Failed to convert Avro to JSON: " + e.getMessage(), e);
}
}
private GenericRecord jsonToAvro(String jsonPayload, Schema schema) {
try {
JsonNode root = OBJECT_MAPPER.readTree(jsonPayload);
Object converted = convertJsonNode(root, schema);
return (GenericRecord) converted;
} catch (Exception e) {
throw new IllegalStateException("Failed to convert JSON payload to Avro", e);
}
}
private Object convertJsonNode(JsonNode node, Schema schema) {
return switch (schema.getType()) {
case RECORD -> {
GenericData.Record record = new GenericData.Record(schema);
schema.getFields().forEach(field -> record.put(field.name(),
convertJsonNode(node.path(field.name()), field.schema())));
yield record;
}
case ARRAY -> {
List<Object> values = new ArrayList<>();
node.forEach(item -> values.add(convertJsonNode(item, schema.getElementType())));
yield values;
}
case MAP -> {
Map<String, Object> map = new HashMap<>();
node.fields().forEachRemaining(entry -> map.put(entry.getKey(),
convertJsonNode(entry.getValue(), schema.getValueType())));
yield map;
}
case UNION -> resolveUnion(node, schema);
case ENUM -> new GenericData.EnumSymbol(schema, node.asText());
case FIXED -> {
byte[] fixedBytes = toBytes(node);
yield new GenericData.Fixed(schema, fixedBytes);
}
case STRING -> node.isNull() ? null : node.asText();
case INT -> node.isNull() ? null : node.asInt();
case LONG -> node.isNull() ? null : node.asLong();
case FLOAT -> node.isNull() ? null : (float) node.asDouble();
case DOUBLE -> node.isNull() ? null : node.asDouble();
case BOOLEAN -> node.isNull() ? null : node.asBoolean();
case BYTES -> ByteBuffer.wrap(toBytes(node));
case NULL -> null;
};
}
private Object resolveUnion(JsonNode node, Schema unionSchema) {
if (node == null || node.isNull()) {
return null;
}
IllegalStateException lastException = null;
for (Schema candidate : unionSchema.getTypes()) {
if (candidate.getType() == Schema.Type.NULL) {
continue;
}
try {
Object value = convertJsonNode(node, candidate);
if (GenericData.get().validate(candidate, value)) {
return value;
}
} catch (Exception e) {
lastException = new IllegalStateException("Failed to resolve union type", e);
}
}
if (lastException != null) {
throw lastException;
}
throw new IllegalStateException("Cannot resolve union for node: " + node);
}
private byte[] toBytes(JsonNode node) {
if (node.isBinary()) {
try {
return node.binaryValue();
} catch (Exception ignored) {
// fallback to textual representation
}
}
String text = node.asText();
try {
return Base64.getDecoder().decode(text);
} catch (Exception ignored) {
return text.getBytes(StandardCharsets.UTF_8);
}
}
private Object convertAvroObject(Object value) {
if (value == null) {
return null;
}
if (value instanceof GenericRecord record) {
Map<String, Object> jsonObject = new LinkedHashMap<>();
record.getSchema().getFields().forEach(field -> jsonObject.put(field.name(), convertAvroObject(record.get(field.name()))));
return jsonObject;
}
if (value instanceof GenericData.Array<?> array) {
List<Object> values = new ArrayList<>(array.size());
array.forEach(item -> values.add(convertAvroObject(item)));
return values;
}
if (value instanceof Map<?, ?> map) {
Map<String, Object> values = new LinkedHashMap<>();
map.forEach((key, item) -> values.put(String.valueOf(key), convertAvroObject(item)));
return values;
}
if (value instanceof ByteBuffer byteBuffer) {
ByteBuffer duplicate = byteBuffer.duplicate();
byte[] bytes = new byte[duplicate.remaining()];
duplicate.get(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
return value;
}
}

View File

@ -55,4 +55,5 @@ public class CaGwEndpoint implements RestEndpoint {
public StoreAccessor getStore() {
return store;
}
}

View File

@ -0,0 +1,199 @@
package cz.moneta.test.harness.endpoints.imq;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import cz.moneta.test.harness.connectors.VaultConnector;
import cz.moneta.test.harness.connectors.messaging.IbmMqConnector;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.messaging.MqMessageFormat;
import cz.moneta.test.harness.messaging.ReceivedMessage;
import cz.moneta.test.harness.support.auth.Credentials;
/**
* IBM MQ First Vision endpoint. Provides high-level access to IBM MQ queues
* with configuration from StoreAccessor.
* <p>
* Credentials are loaded from HashiCorp Vault.
*/
public class ImqFirstVisionEndpoint implements Endpoint {
private static final Logger LOG = LogManager.getLogger(ImqFirstVisionEndpoint.class);
private final IbmMqConnector connector;
private final StoreAccessor store;
private String username, password, keystorePassword;
// Configuration keys
private static final String CONNECTION_NAME_LIST_KEY = "endpoints.imq-first-vision.connection-name-list";
private static final String CHANNEL_KEY = "endpoints.imq-first-vision.channel";
private static final String QUEUE_MANAGER_KEY = "endpoints.imq-first-vision.queue-manager";
private static final String SSL_CIPHER_SUITE_KEY = "endpoints.imq-first-vision.ssl-cipher-suite";
private static final String VAULT_PATH_KEY = "vault.imq-first-vision.secrets.path";
private static final String VAULT_KEYSTORE_PASSWORD_KEY = "keystorePassword";
private static final String KEYSTORE_PATH = "keystores/imq-keystore.jks";
/**
* Constructor that reads configuration from StoreAccessor.
*/
public ImqFirstVisionEndpoint(StoreAccessor store) {
this.store = store;
// Read configuration
String connectionNameList = getConfig(CONNECTION_NAME_LIST_KEY);
String channel = getConfig(CHANNEL_KEY);
String queueManager = getConfig(QUEUE_MANAGER_KEY);
String sslCipherSuite = getConfig(SSL_CIPHER_SUITE_KEY);
loadCredentialsFromVault();
try {
this.connector = new IbmMqConnector(connectionNameList, channel, queueManager, username, password,
KEYSTORE_PATH, keystorePassword, sslCipherSuite);
LOG.info("Initialized IBM MQ First Vision endpoint for queue manager: {}", queueManager);
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize IBM MQ endpoint", e);
}
}
/**
* Get a configuration value from StoreAccessor.
*/
private String getConfig(String key) {
return Optional.ofNullable(store.getConfig(key))
.orElseThrow(() -> new IllegalStateException("You need to configure " + key));
}
/**
* Load credentials from HashiCorp Vault.
*/
private void loadCredentialsFromVault() {
try {
// Get vault URL from configuration
String vaultPath = getConfig(VAULT_PATH_KEY);
String vaultUrl = getConfig(HarnessConfigConstants.VAULT_URL_CONFIG);
String vaultUser = getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG);
String vaultPassword = getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG);
VaultConnector vaultConnector = new VaultConnector(vaultUrl, vaultUser, vaultPassword);
Optional<Credentials> credentials = vaultConnector.getUsernameAndPassword(vaultPath);
if (credentials.isPresent()) {
this.username = credentials.get().getUsername();
this.password = credentials.get().getPassword();
this.keystorePassword = vaultConnector.getValue(vaultPath, VAULT_KEYSTORE_PASSWORD_KEY)
.map(Object::toString).orElse(null);
LOG.info("Successfully loaded credentials from Vault for path: {}", vaultPath);
} else {
throw new IllegalStateException("Credentials not found in Vault at path: " + vaultPath);
}
} catch (Exception e) {
throw new IllegalStateException("Failed to load credentials from Vault", e);
}
}
/**
* Send a message to a queue.
*
* @param queueName Physical queue name or logical name (from
* ImqFirstVisionQueue)
* @param payload Message payload
* @param format Message format
* @param properties JMS properties
*/
public void send(String queueName, String payload, MqMessageFormat format,
java.util.Map<String, String> properties) {
connector.send(queueName, payload, format, properties);
}
/**
* Send a message to a queue using logical queue name.
*/
public void send(ImqFirstVisionQueue queue, String payload, MqMessageFormat format,
java.util.Map<String, String> properties) {
String physicalQueueName = resolveQueue(queue);
connector.send(physicalQueueName, payload, format, properties);
}
/**
* Receive a message from a queue.
*
* @param queueName Physical queue name or logical name
* @param messageSelector JMS message selector (optional)
* @param format Expected message format
* @param timeout Timeout duration
* @return Received message
*/
public ReceivedMessage receive(String queueName, String messageSelector, MqMessageFormat format, Duration timeout) {
return connector.receive(queueName, messageSelector, format, timeout);
}
/**
* Receive a message from a queue using logical queue name.
*/
public ReceivedMessage receive(ImqFirstVisionQueue queue, String messageSelector, MqMessageFormat format,
Duration timeout) {
String physicalQueueName = resolveQueue(queue);
return connector.receive(physicalQueueName, messageSelector, format, timeout);
}
/**
* Browse a queue (non-destructive read).
*
* @param queueName Physical queue name or logical name
* @param messageSelector JMS message selector (optional)
* @param format Expected message format
* @param maxMessages Maximum number of messages
* @return List of received messages
*/
public List<ReceivedMessage> browse(String queueName, String messageSelector, MqMessageFormat format,
int maxMessages) {
return connector.browse(queueName, messageSelector, format, maxMessages);
}
/**
* Browse a queue using logical queue name.
*/
public List<ReceivedMessage> browse(ImqFirstVisionQueue queue, String messageSelector, MqMessageFormat format,
int maxMessages) {
String physicalQueueName = resolveQueue(queue);
return connector.browse(physicalQueueName, messageSelector, format, maxMessages);
}
/**
* Resolve logical queue name to physical queue name.
*
* @param logicalName Logical queue name or ImqFirstVisionQueue enum
* @return Physical queue name
*/
public String resolveQueue(String logicalName) {
String configKey = "endpoints.imq-first-vision." + logicalName + ".queue";
return Optional.ofNullable(store.getConfig(configKey)).orElseThrow(
() -> new IllegalStateException("Queue '" + logicalName + "' is not configured in " + configKey));
}
/**
* Resolve ImqFirstVisionQueue enum to physical queue name.
*/
public String resolveQueue(ImqFirstVisionQueue queue) {
return resolveQueue(queue.getConfigKey());
}
@Override
public void close() {
if (connector != null) {
connector.close();
}
}
}

View File

@ -0,0 +1,51 @@
package cz.moneta.test.harness.endpoints.imq;
/**
* Logical queue names for IBM MQ First Vision.
* Physical queue names are resolved from configuration.
*/
public enum ImqFirstVisionQueue {
/**
* Payment notifications queue.
*/
PAYMENT_NOTIFICATIONS("payment-notifications"),
/**
* Payment request queue.
*/
PAYMENT_REQUEST("payment-request"),
/**
* MF (Money Flow) requests queue.
*/
MF_REQUESTS("mf-requests"),
/**
* MF (Money Flow) responses queue.
*/
MF_RESPONSES("mf-responses"),
/**
* MF (Money Flow) EBCDIC queue.
*/
MF_EBCDIC("mf-ebcdic"),
/**
* MF (Money Flow) UTF-8 queue.
*/
MF_UTF8("mf-utf8");
private final String configKey;
ImqFirstVisionQueue(String configKey) {
this.configKey = configKey;
}
/**
* Get the configuration key for this queue.
* Used to resolve physical queue name from configuration.
*/
public String getConfigKey() {
return configKey;
}
}

View File

@ -0,0 +1,199 @@
package cz.moneta.test.harness.endpoints.kafka;
import cz.moneta.test.harness.connectors.messaging.KafkaConnector;
import cz.moneta.test.harness.context.BaseStoreAccessor;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.messaging.exception.MessagingConnectionException;
import cz.moneta.test.harness.support.messaging.kafka.ReceivedMessage;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.response.LogicalResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Kafka endpoint that provides high-level API for Kafka messaging.
* <p>
* Reads configuration from StoreAccessor and credentials from HashiCorp Vault.
* Implements singleton pattern per test class (managed by Harness).
* </p>
*/
public class KafkaEndpoint implements Endpoint {
private final KafkaConnector connector;
private final StoreAccessor store;
/**
* Creates a new KafkaEndpoint.
* <p>
* Configuration is read from StoreAccessor:
* - endpoints.kafka.bootstrap-servers
* - endpoints.kafka.schema-registry-url
* <p>
* Credentials are retrieved from Vault:
* - vault.kafka.secrets.path -> apiKey, apiSecret
* </p>
*/
public KafkaEndpoint(StoreAccessor store) {
this.store = store;
// Read configuration
String bootstrapServers = store.getConfig("endpoints.kafka.bootstrap-servers");
if (bootstrapServers == null) {
throw new IllegalStateException("You need to configure " + bootstrapServers + " to work with Kafka");
}
String schemaRegistryUrl = store.getConfig("endpoints.kafka.schema-registry-url");
if (schemaRegistryUrl == null) {
throw new IllegalStateException("You need to configure " + schemaRegistryUrl + " url to work with Kafka");
}
// Retrieve credentials from Vault
String vaultPath = store.getConfig("vault.kafka.secrets.path");
if (vaultPath == null) {
throw new IllegalStateException(
"You need to configure vault.kafka.secrets.path");
}
//
String apiKey = getVaultValue(vaultPath, "apiKey");
String apiSecret = getVaultValue(vaultPath, "apiSecret");
String schemaRegistryApiKey = getVaultValue(vaultPath, "schemaRegistryApiKey");
String schemaRegistryApiSecret = getVaultValue(vaultPath, "schemaRegistryApiSecret");
// Create connector
this.connector = new KafkaConnector(
bootstrapServers,
apiKey,
apiSecret,
schemaRegistryUrl,
schemaRegistryApiKey,
schemaRegistryApiSecret
);
}
/**
* Sends a message to a Kafka topic.
*
* @param topic Kafka topic name
* @param key Message key
* @param jsonPayload Message body as JSON string
* @param headers Kafka headers (traceparent, requestID, activityID, etc.)
*/
public void send(String topic, String key, String jsonPayload, Map<String, String> headers) {
connector.send(topic, key, jsonPayload, headers);
}
/**
* Receives a message from a Kafka topic matching the filter.
*
* @param topic Kafka topic name
* @param filter Predicate to filter messages
* @param timeout Maximum time to wait for a message
* @return First matching ReceivedMessage
*/
public List<ReceivedMessage> receive(String topic,
java.util.function.Predicate<ReceivedMessage> filter,
Duration timeout) {
return connector.receive(topic, filter, timeout);
}
/**
* Receives a message with default timeout (30 seconds).
*/
public List<ReceivedMessage> receive(String topic,
java.util.function.Predicate<ReceivedMessage> filter) {
return receive(topic, filter, Duration.ofSeconds(30));
}
/**
* Receives the first available message from a topic.
*
* @param topic Kafka topic name
* @param timeout Maximum time to wait
* @return First message
*/
public List<ReceivedMessage> receive(String topic, Duration timeout) {
return receive(topic, msg -> true, timeout);
}
/**
* Closes the endpoint and releases resources.
*/
@Override
public void close() {
if (connector != null) {
connector.close();
}
}
/**
* Checks if Kafka is accessible.
*/
@Override
public boolean canAccess() {
try {
// Try to get topic metadata - if this succeeds, Kafka is accessible
// Note: We don't actually make a network call here, just verify config
String bootstrapServers = store.getConfig("endpoints.kafka.bootstrap-servers");
return bootstrapServers != null && !bootstrapServers.isEmpty();
} catch (Exception e) {
return false;
}
}
/**
* Gets the underlying connector (for advanced use cases).
*/
public KafkaConnector getConnector() {
return connector;
}
/**
* Gets the store accessor.
*/
public StoreAccessor getStore() {
return store;
}
/**
* Retrieves a value from Vault.
*/
private String getVaultValue(String path, String key) {
try {
VaultConfig vaultConfig = new VaultConfig()
.address(store.getConfig("vault.address", "http://localhost:8200"))
.token(store.getConfig("vault.token"))
.build();
Vault vault = new Vault(vaultConfig, 2);
LogicalResponse response = vault.logical().read(path);
if (response == null) {
throw new MessagingConnectionException(
"Failed to read from Vault path: " + path);
}
Map<String, String> data = response.getData();
if (data == null) {
throw new MessagingConnectionException(
"No data found in Vault path: " + path);
}
String value = data.get(key);
if (value == null) {
throw new MessagingConnectionException(
"Credential '" + key + "' not found in Vault path: " + path);
}
return value;
} catch (VaultException e) {
throw new MessagingConnectionException(
"Failed to retrieve credential '" + key + "' from Vault at " + path, e);
}
}
}

View File

@ -1,125 +0,0 @@
package cz.moneta.test.harness.endpoints.messaging;
import cz.moneta.test.harness.connectors.VaultConnector;
import cz.moneta.test.harness.connectors.messaging.IbmMqConnector;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
public class IbmMqEndpoint implements Endpoint {
private static final String CONFIG_HOST = "endpoints.imq-first-vision.host";
private static final String CONFIG_PORT = "endpoints.imq-first-vision.port";
private static final String CONFIG_CHANNEL = "endpoints.imq-first-vision.channel";
private static final String CONFIG_QUEUE_MANAGER = "endpoints.imq-first-vision.queue-manager";
private static final String CONFIG_KEYSTORE_PATH = "endpoints.imq-first-vision.keystore.path";
private static final String CONFIG_KEYSTORE_PASSWORD = "endpoints.imq-first-vision.keystore.password";
private static final String CONFIG_VAULT_PATH = "vault.path.messaging.ibmmq";
private static final String CONFIG_USERNAME = "endpoints.imq-first-vision.username";
private static final String CONFIG_PASSWORD = "endpoints.imq-first-vision.password";
private static final String CONFIG_SSL_CYPHER_SUITES = "endpoints.imq-first-vision.ssl-cipher-suite";
private final StoreAccessor store;
private volatile IbmMqConnector connector;
private final Object connectorLock = new Object();
public IbmMqEndpoint(StoreAccessor store) {
this.store = store;
}
public void send(String queueName,
String payload,
MqMessageFormat format,
Map<String, String> properties) {
getConnector().send(queueName, payload, format, properties);
}
public ReceivedMessage receive(String queueName,
String messageSelector,
MqMessageFormat expectedFormat,
Duration timeout) {
return getConnector().receive(queueName, messageSelector, expectedFormat, timeout);
}
public List<ReceivedMessage> browse(String queueName,
Predicate<ReceivedMessage> filter,
MqMessageFormat expectedFormat,
Duration timeout) {
return getConnector().browse(queueName, filter, expectedFormat, timeout);
}
@Override
public void close() {
IbmMqConnector current = connector;
if (current != null) {
current.close();
}
}
private IbmMqConnector getConnector() {
IbmMqConnector current = connector;
if (current == null) {
synchronized (connectorLock) {
current = connector;
if (current == null) {
current = createConnector();
connector = current;
}
}
}
return current;
}
private IbmMqConnector createConnector() {
String host = requireConfig(CONFIG_HOST);
int port = Integer.parseInt(requireConfig(CONFIG_PORT));
String channel = requireConfig(CONFIG_CHANNEL);
String queueManager = requireConfig(CONFIG_QUEUE_MANAGER);
String username = resolveSecret(CONFIG_USERNAME, "username");
String password = resolveSecret(CONFIG_PASSWORD, "password");
String keystorePath = store.getConfig(CONFIG_KEYSTORE_PATH);
String keystorePassword = store.getConfig(CONFIG_KEYSTORE_PASSWORD);
String sslCipherSuites = store.getConfig(CONFIG_SSL_CYPHER_SUITES);
return new IbmMqConnector(host, port, channel, queueManager, username, password, keystorePath, keystorePassword, sslCipherSuites);
}
private String resolveSecret(String localConfigKey, String vaultKey) {
return Optional.ofNullable(store.getConfig(localConfigKey))
.or(() -> readFromVault(vaultKey))
.orElseThrow(() -> new IllegalStateException("Missing messaging secret: " + localConfigKey));
}
private Optional<String> readFromVault(String key) {
String path = store.getConfig(CONFIG_VAULT_PATH);
if (path == null || path.isBlank()) {
return Optional.empty();
}
String basePath = store.getConfig("vault.path.base");
String resolvedPath = path.startsWith("/") || basePath == null || basePath.isBlank()
? path
: basePath + "/" + path;
return createVaultConnector().flatMap(vault -> vault.getValue(resolvedPath, key));
}
private Optional<VaultConnector> createVaultConnector() {
return Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_URL_CONFIG))
.flatMap(url -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG))
.flatMap(username -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG))
.map(password -> new VaultConnector(url, username, password))));
}
private String requireConfig(String key) {
return Optional.ofNullable(store.getConfig(key))
.orElseThrow(() -> new IllegalStateException("Missing required config: " + key));
}
}

View File

@ -1,119 +0,0 @@
package cz.moneta.test.harness.endpoints.messaging;
import cz.moneta.test.harness.connectors.VaultConnector;
import cz.moneta.test.harness.connectors.messaging.KafkaConnector;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
public class KafkaEndpoint implements Endpoint {
private static final String CONFIG_BOOTSTRAP_SERVERS = "messaging.kafka.bootstrap-servers";
private static final String CONFIG_SCHEMA_REGISTRY_URL = "messaging.kafka.schema-registry-url";
private static final String CONFIG_VAULT_PATH = "vault.path.messaging.kafka";
private static final String CONFIG_API_KEY = "messaging.kafka.api-key";
private static final String CONFIG_API_SECRET = "messaging.kafka.api-secret";
private static final String CONFIG_SCHEMA_REGISTRY_API_KEY = "messaging.kafka.schema-registry-api-key";
private static final String CONFIG_SCHEMA_REGISTRY_API_SECRET = "messaging.kafka.schema-registry-api-secret";
private static final String VAULT_API_KEY = "apiKey";
private static final String VAULT_API_SECRET = "apiSecret";
private static final String VAULT_SCHEMA_REGISTRY_API_KEY = "schemaRegistryApiKey";
private static final String VAULT_SCHEMA_REGISTRY_API_SECRET = "schemaRegistryApiSecret";
private final StoreAccessor store;
private volatile KafkaConnector connector;
private final Object connectorLock = new Object();
public KafkaEndpoint(StoreAccessor store) {
this.store = store;
}
public void send(String topic, String key, String payload, Map<String, String> headers) {
getConnector().send(topic, key, payload, headers);
}
public ReceivedMessage receive(String topic, Predicate<ReceivedMessage> filter, Duration timeout) {
List<ReceivedMessage> messages = getConnector().receive(topic, filter, timeout);
return messages.isEmpty() ? null : messages.get(0);
}
@Override
public void close() {
KafkaConnector current = connector;
if (current != null) {
current.close();
}
}
private KafkaConnector getConnector() {
KafkaConnector current = connector;
if (current == null) {
synchronized (connectorLock) {
current = connector;
if (current == null) {
current = createConnector();
connector = current;
}
}
}
return current;
}
private KafkaConnector createConnector() {
String bootstrapServers = requireConfig(CONFIG_BOOTSTRAP_SERVERS);
String schemaRegistryUrl = requireConfig(CONFIG_SCHEMA_REGISTRY_URL);
String apiKey = resolveSecret(CONFIG_API_KEY, VAULT_API_KEY);
String apiSecret = resolveSecret(CONFIG_API_SECRET, VAULT_API_SECRET);
String schemaRegistryApiKey = resolveSecret(CONFIG_SCHEMA_REGISTRY_API_KEY, VAULT_SCHEMA_REGISTRY_API_KEY);
String schemaRegistryApiSecret = resolveSecret(CONFIG_SCHEMA_REGISTRY_API_SECRET, VAULT_SCHEMA_REGISTRY_API_SECRET);
return new KafkaConnector(
bootstrapServers,
apiKey,
apiSecret,
schemaRegistryUrl,
schemaRegistryApiKey,
schemaRegistryApiSecret
);
}
private String resolveSecret(String localConfigKey, String vaultKey) {
return Optional.ofNullable(store.getConfig(localConfigKey))
.or(() -> readFromVault(vaultKey))
.orElseThrow(() -> new IllegalStateException("Missing messaging secret: " + localConfigKey));
}
private Optional<String> readFromVault(String key) {
String path = store.getConfig(CONFIG_VAULT_PATH);
if (path == null || path.isBlank()) {
return Optional.empty();
}
String basePath = store.getConfig("vault.path.base");
String resolvedPath = path.startsWith("/") || basePath == null || basePath.isBlank()
? path
: basePath + "/" + path;
return createVaultConnector().flatMap(vault -> vault.getValue(resolvedPath, key));
}
private Optional<VaultConnector> createVaultConnector() {
return Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_URL_CONFIG))
.flatMap(url -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG))
.flatMap(username -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG))
.map(password -> new VaultConnector(url, username, password))));
}
private String requireConfig(String key) {
return Optional.ofNullable(store.getConfig(key))
.orElseThrow(() -> new IllegalStateException("Missing required config: " + key));
}
}

View File

@ -1,130 +0,0 @@
package cz.moneta.test.harness.endpoints.messaging;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.exception.MessagingTimeoutException;
import cz.moneta.test.harness.messaging.model.Destination;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.Queue;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import cz.moneta.test.harness.messaging.model.Topic;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
public class MessagingEndpoint implements Endpoint {
private final StoreAccessor store;
private volatile KafkaEndpoint kafkaEndpoint;
private volatile IbmMqEndpoint ibmMqEndpoint;
private final Object endpointLock = new Object();
public MessagingEndpoint(StoreAccessor store) {
this.store = store;
}
public void send(String destinationName,
String key,
String payload,
MqMessageFormat formatOverride,
Map<String, String> headers) {
Destination destination = resolveDestination(destinationName);
if (destination instanceof Topic topic) {
getKafkaEndpoint().send(topic.getTopicName(), key, payload, headers);
return;
}
Queue queue = (Queue) destination;
MqMessageFormat format = Optional.ofNullable(formatOverride).orElse(queue.getFormat());
getIbmMqEndpoint().send(queue.getQueueName(), payload, format, headers);
}
public ReceivedMessage receive(String destinationName,
Predicate<ReceivedMessage> filter,
Duration timeout,
MqMessageFormat formatOverride) {
Destination destination = resolveDestination(destinationName);
if (destination instanceof Topic topic) {
return getKafkaEndpoint().receive(topic.getTopicName(), filter, timeout);
}
Queue queue = (Queue) destination;
MqMessageFormat format = Optional.ofNullable(formatOverride).orElse(queue.getFormat());
List<ReceivedMessage> messages = getIbmMqEndpoint().browse(queue.getQueueName(), filter, format, timeout);
if (messages.isEmpty()) {
throw new MessagingTimeoutException("No IBM MQ message found for destination: " + destinationName);
}
return messages.get(0);
}
public Destination resolveDestination(String destinationName) {
String prefix = "messaging.destination." + destinationName + ".";
String type = Optional.ofNullable(store.getConfig(prefix + "type"))
.map(v -> v.toLowerCase(Locale.ROOT))
.orElseThrow(() -> new IllegalStateException("Missing destination config: " + prefix + "type"));
return switch (type) {
case "kafka" -> {
String topic = requireConfig(prefix + "topic");
yield new Topic(destinationName, topic);
}
case "ibmmq" -> {
String queue = requireConfig(prefix + "queue");
String format = store.getConfig(prefix + "format", MqMessageFormat.JSON.name().toLowerCase(Locale.ROOT));
yield new Queue(destinationName, queue, MqMessageFormat.fromConfig(format, MqMessageFormat.JSON));
}
default -> throw new IllegalStateException("Unsupported destination type '" + type + "' for destination: " + destinationName);
};
}
@Override
public void close() {
KafkaEndpoint kafka = kafkaEndpoint;
if (kafka != null) {
kafka.close();
}
IbmMqEndpoint mq = ibmMqEndpoint;
if (mq != null) {
mq.close();
}
}
private KafkaEndpoint getKafkaEndpoint() {
KafkaEndpoint current = kafkaEndpoint;
if (current == null) {
synchronized (endpointLock) {
current = kafkaEndpoint;
if (current == null) {
current = new KafkaEndpoint(store);
kafkaEndpoint = current;
}
}
}
return current;
}
private IbmMqEndpoint getIbmMqEndpoint() {
IbmMqEndpoint current = ibmMqEndpoint;
if (current == null) {
synchronized (endpointLock) {
current = ibmMqEndpoint;
if (current == null) {
current = new IbmMqEndpoint(store);
ibmMqEndpoint = current;
}
}
}
return current;
}
private String requireConfig(String key) {
return Optional.ofNullable(store.getConfig(key))
.filter(v -> !Objects.equals(v.trim(), ""))
.orElseThrow(() -> new IllegalStateException("Missing required config: " + key));
}
}

View File

@ -0,0 +1,51 @@
package cz.moneta.test.harness.endpoints.payment_engine;
import cz.moneta.test.harness.connectors.rest.RestConnector;
import cz.moneta.test.harness.connectors.rest.SimpleRestConnector;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.RestEndpoint;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Map;
import java.util.Optional;
public class PaeMqEndpoint implements RestEndpoint {
private RestConnector restConnector;
private StoreAccessor store;
public PaeMqEndpoint(StoreAccessor store) {
this.store = store;
String endpointName = "endpoints.pae-mq.url";
this.restConnector = Optional.ofNullable(store.getConfig(endpointName))
.map(url -> new SimpleRestConnector(url, "PaeMqServerLogger"))
.orElseThrow(() -> new IllegalStateException("You need to configure " + endpointName + " to work with PAE IBM MQ server"));
}
@Override
public <T> Pair<Integer, T> get(String path, Map<String, Object> properties, Class<T> responseType, Map<String, Object> headers) {
return restConnector.get(path, properties, new GenericType<>(responseType), headers);
}
@Override
public <T> Pair<Integer, T> post(String path, Entity<?> request, Class<T> responseType, Map<String, Object> headers, boolean rawResponse) {
return restConnector.post(path, request, new GenericType<>(responseType), headers);
}
@Override
public <T> Pair<Integer, T> delete(String path, Map<String, Object> properties, Class<T> responseType, Map<String, Object> headers) {
return restConnector.delete(path, properties, new GenericType<>(responseType), headers);
}
@Override
public <T> Pair<Integer, T> patch(String path, Entity<?> request, Class<T> responseType, Map<String, Object> headers, boolean rawResponse) {
return restConnector.patch(path, request, new GenericType<>(responseType), headers);
}
@Override
public StoreAccessor getStore() {
return store;
}
}

View File

@ -1,8 +0,0 @@
package cz.moneta.test.harness.exception;
public class MessagingTimeoutException extends HarnessException {
public MessagingTimeoutException(String message) {
super(message);
}
}

View File

@ -0,0 +1,83 @@
package cz.moneta.test.harness.messaging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
/**
* Wrapper for extracted JSON path values.
* Provides fluent methods for value extraction and conversion.
*/
public class JsonPathValue {
private final JsonNode node;
private final String rawValue;
public JsonPathValue(JsonNode node) {
this.node = node;
this.rawValue = node != null ? node.asText() : null;
}
public JsonPathValue(String rawValue) {
this.node = null;
this.rawValue = rawValue;
}
/**
* Get the value as a string.
*/
public String asText() {
if (node != null && !(node instanceof NullNode)) {
return node.asText();
}
return rawValue;
}
/**
* Get the value as an integer.
*/
public int asInt() {
if (node != null && !(node instanceof NullNode)) {
return node.asInt();
}
return Integer.parseInt(rawValue);
}
/**
* Get the value as a long.
*/
public long asLong() {
if (node != null && !(node instanceof NullNode)) {
return node.asLong();
}
return Long.parseLong(rawValue);
}
/**
* Get the value as a boolean.
*/
public boolean asBoolean() {
if (node != null && !(node instanceof NullNode)) {
return node.asBoolean();
}
return Boolean.parseBoolean(rawValue);
}
/**
* Check if the value is null or missing.
*/
public boolean isNull() {
return node == null || node instanceof NullNode || rawValue == null;
}
/**
* Get the underlying JsonNode.
*/
public JsonNode getNode() {
return node;
}
@Override
public String toString() {
return asText();
}
}

View File

@ -0,0 +1,21 @@
package cz.moneta.test.harness.messaging;
/**
* Content type of a received message.
*/
public enum MessageContentType {
/**
* JSON content - body is a JSON string.
*/
JSON,
/**
* XML content - body is an XML string.
*/
XML,
/**
* Raw text content - body is plain text (e.g., EBCDIC decoded, UTF-8).
*/
RAW_TEXT
}

View File

@ -0,0 +1,111 @@
package cz.moneta.test.harness.messaging;
import org.assertj.core.api.AbstractObjectAssert;
/**
* Response interface for received messages.
* Provides assertion methods for verifying message content.
* Shared interface for both Kafka and IBM MQ message responses.
*/
public interface MessageResponse {
/**
* Assert that a field in the message body has the expected value.
* For JSON: uses JSON path (dot/bracket notation).
* For XML: uses XPath expression.
*
* @param path JSON path or XPath expression
* @param value expected value as string
* @return this instance for fluent assertions
* @throws AssertionError if assertion fails
*/
MessageResponse andAssertFieldValue(String path, String value);
/**
* Assert that a field exists in the message body.
*
* @param path JSON path or XPath expression
* @return this instance for fluent assertions
* @throws AssertionError if assertion fails
*/
MessageResponse andAssertPresent(String path);
/**
* Assert that a field does not exist in the message body.
*
* @param path JSON path or XPath expression
* @return this instance for fluent assertions
* @throws AssertionError if assertion fails
*/
MessageResponse andAssertNotPresent(String path);
/**
* Assert that a header (Kafka header or JMS property) has the expected value.
*
* @param headerName name of the header/property
* @param value expected value
* @return this instance for fluent assertions
* @throws AssertionError if assertion fails
*/
MessageResponse andAssertHeaderValue(String headerName, String value);
/**
* Assert that the message body contains a substring.
* Primarily used for EBCDIC/UTF-8 raw text assertions.
*
* @param substring expected substring
* @return this instance for fluent assertions
* @throws AssertionError if assertion fails
*/
MessageResponse andAssertBodyContains(String substring);
/**
* Get AssertJ fluent assertion for complex object assertions.
*
* @return AssertJ AbstractObjectAssert for fluent assertions
*/
AbstractObjectAssert<?, ?> andAssertWithAssertJ();
/**
* Extract a value from the message body.
* For JSON: uses JSON path (dot/bracket notation).
* For XML: uses XPath expression.
*
* @param path JSON path or XPath expression
* @return JsonPathValue wrapper for the extracted value
*/
JsonPathValue extract(String path);
/**
* Deserialize the message body to a Java object.
* For JSON: uses Jackson ObjectMapper.
* For XML: uses JAXB or Jackson XmlMapper.
*
* @param type target type
* @param <T> target type
* @return deserialized object
*/
<T> T mapTo(Class<T> type);
/**
* Get the raw message body.
*
* @return message body as string
*/
String getBody();
/**
* Get a header value (Kafka header or JMS property).
*
* @param name header/property name
* @return header value or null if not present
*/
String getHeader(String name);
/**
* Get the underlying received message.
*
* @return ReceivedMessage instance
*/
ReceivedMessage getMessage();
}

View File

@ -0,0 +1,31 @@
package cz.moneta.test.harness.messaging;
/**
* Message format for IBM MQ.
* Defines how messages are encoded and transmitted.
*/
public enum MqMessageFormat {
/**
* JSON format - JMS TextMessage with plain JSON string.
* Default format for IBM MQ.
*/
JSON,
/**
* XML format - JMS TextMessage with XML string.
* XML is decoded and can be queried using XPath.
*/
XML,
/**
* EBCDIC format - JMS BytesMessage with EBCDIC IBM-870 encoding.
* Used for mainframe systems (Czech/Slovak characters).
*/
EBCDIC_870,
/**
* UTF-8 format - JMS BytesMessage with UTF-8 (CCSID 1208) encoding.
* Used for binary data with explicit UTF-8 encoding.
*/
UTF8_1208
}

View File

@ -0,0 +1,386 @@
package cz.moneta.test.harness.messaging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a received message from a messaging system.
* Body is always normalized to a String regardless of source and wire format.
* <p>
* For Kafka: Avro GenericRecord is automatically converted to JSON.
* For IBM MQ (JSON): JSON string from JMS TextMessage.
* For IBM MQ (XML): XML string from JMS TextMessage.
* For IBM MQ (EBCDIC): byte[] from JMS BytesMessage decoded from IBM-870.
*/
public class ReceivedMessage {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final Pattern JSON_PATH_PATTERN = Pattern.compile("^([\\w.]+)\\[([\\d]+)\\](.*)$");
private final String body;
private final MessageContentType contentType;
private final Map<String, String> headers;
private final long timestamp;
private final String source;
private final String key;
public ReceivedMessage(String body, MessageContentType contentType, Map<String, String> headers,
long timestamp, String source, String key) {
this.body = body;
this.contentType = contentType;
this.headers = headers != null ? Collections.unmodifiableMap(new HashMap<>(headers)) : new HashMap<>();
this.timestamp = timestamp;
this.source = source;
this.key = key;
}
/**
* Extract a JSON value using JSON path (dot/bracket notation).
* Supports paths like "items[0].sku" or "nested.field".
*
* @param path JSON path
* @return JsonNode for the extracted value
*/
public JsonNode extractJson(String path) {
if (body == null || StringUtils.isEmpty(path)) {
return null;
}
try {
JsonNode root = JSON_MAPPER.readTree(body);
return evaluateJsonPath(root, path);
} catch (Exception e) {
throw new RuntimeException("Failed to extract JSON path: " + path, e);
}
}
/**
* Extract a value using XPath (for XML messages).
*
* @param xpathExpression XPath expression
* @return extracted value as string
*/
public String extractXml(String xpathExpression) {
if (body == null || StringUtils.isEmpty(xpathExpression)) {
return null;
}
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
javax.xml.parsers.DocumentBuilder finalBuilder = builder;
var document = finalBuilder.parse(new java.io.ByteArrayInputStream(body.getBytes()));
document.getDocumentElement().normalize();
XPath xpath = XPathFactory.newInstance().newXPath();
Object result = xpath.evaluate(xpathExpression, document, XPathConstants.NODE);
if (result instanceof org.w3c.dom.Node domNode) {
return domNode.getTextContent();
}
return null;
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate XPath: " + xpathExpression, e);
}
}
/**
* Universal extract method - auto-detects content type and uses appropriate extraction.
*
* @param expression JSON path or XPath expression
* @return extracted value as string
*/
public String extract(String expression) {
return switch (contentType) {
case JSON -> extractJson(expression).asText();
case XML -> extractXml(expression);
case RAW_TEXT -> body;
};
}
/**
* Evaluate JSON path on a JSON node.
* Supports dot notation and bracket notation for arrays.
*/
private JsonNode evaluateJsonPath(JsonNode node, String path) {
if (StringUtils.isEmpty(path)) {
return node;
}
String[] parts = tokenizePath(path);
JsonNode current = node;
for (String part : parts) {
if (current == null) {
return null;
}
if (part.isEmpty()) {
continue;
}
if (part.contains("[")) {
// Array access
Matcher matcher = JSON_PATH_PATTERN.matcher(part);
if (matcher.matches()) {
String arrayName = matcher.group(1);
int index = Integer.parseInt(matcher.group(2));
String remaining = matcher.group(3);
if (current.isArray()) {
current = index < current.size() ? current.get(index) : null;
} else if (current.isObject()) {
JsonNode arrayNode = current.get(arrayName);
if (arrayNode != null && arrayNode.isArray()) {
current = index < arrayNode.size() ? arrayNode.get(index) : null;
} else {
current = null;
}
}
// Continue with remaining path
if (StringUtils.isNotBlank(remaining)) {
current = evaluateJsonPath(current, remaining);
}
} else {
current = current.get(part);
}
} else if (part.contains(".")) {
// Navigate through object properties
String[] segments = part.split("\\.");
for (String segment : segments) {
if (StringUtils.isEmpty(segment)) {
continue;
}
current = current.get(segment);
}
} else {
current = current.get(part);
}
}
return current;
}
/**
* Tokenize JSON path into segments.
*/
private String[] tokenizePath(String path) {
if (StringUtils.isEmpty(path)) {
return new String[0];
}
java.util.List<String> tokens = new java.util.ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inBracket = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == '[') {
inBracket = true;
current.append(c);
} else if (c == ']') {
inBracket = false;
current.append(c);
} else if (c == '.' && !inBracket) {
if (current.length() > 0) {
tokens.add(current.toString());
current.setLength(0);
}
} else {
current.append(c);
}
}
if (current.length() > 0) {
tokens.add(current.toString());
}
return tokens.toArray(new String[0]);
}
/**
* Get the message key (Kafka message key, null for IBM MQ).
*/
public String getKey() {
return key;
}
/**
* Get a header value (Kafka header or JMS property).
*/
public String getHeader(String name) {
return headers.get(name);
}
/**
* Get all headers.
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* Get the message body.
*/
public String getBody() {
return body;
}
/**
* Get the message timestamp.
*/
public long getTimestamp() {
return timestamp;
}
/**
* Get the content type.
*/
public MessageContentType getContentType() {
return contentType;
}
/**
* Get the source (topic or queue name).
*/
public String getSource() {
return source;
}
/**
* Deserialize the message body to a Java object.
*
* @param type target type
* @param <T> target type
* @return deserialized object
*/
public <T> T mapTo(Class<T> type) {
if (body == null) {
return null;
}
try {
if (contentType == MessageContentType.XML) {
// XML deserialization using JAXB
return mapXmlTo(type);
} else {
// JSON deserialization using Jackson
return JSON_MAPPER.readValue(body, type);
}
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize message to " + type.getName(), e);
}
}
/**
* Deserialize the message body to a Java object for XML.
* Uses JAXB-like parsing - simplified for basic XML structures.
*/
private <T> T mapXmlTo(Class<T> type) {
try {
// For XML, parse to a simple map structure
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
javax.xml.parsers.DocumentBuilder finalBuilder = builder;
var document = finalBuilder.parse(new java.io.ByteArrayInputStream(body.getBytes()));
document.getDocumentElement().normalize();
Map<String, Object> xmlMap = xmlToMap(document.getDocumentElement());
return JSON_MAPPER.convertValue(xmlMap, type);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize XML message", e);
}
}
/**
* Convert XML element to Map.
*/
private Map<String, Object> xmlToMap(org.w3c.dom.Element element) {
Map<String, Object> result = new HashMap<>();
for (int i = 0; i < element.getAttributes().getLength(); i++) {
org.w3c.dom.NamedNodeMap attributes = element.getAttributes();
org.w3c.dom.Node attr = attributes.item(i);
result.put("@" + attr.getNodeName(), attr.getNodeValue());
}
// Add children
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
org.w3c.dom.Node node = element.getChildNodes().item(i);
if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
org.w3c.dom.Element childElement = (org.w3c.dom.Element) node;
String tagName = childElement.getTagName();
if (childElement.getChildNodes().getLength() == 0) {
// Leaf element
result.put(tagName, childElement.getTextContent());
} else {
// Check if all children are elements (complex) or text (simple)
boolean hasElement = false;
for (int j = 0; j < childElement.getChildNodes().getLength(); j++) {
org.w3c.dom.Node childNode = childElement.getChildNodes().item(j);
if (childNode.getNodeType() == org.w3c.dom.Node.TEXT_NODE &&
StringUtils.isNotBlank(childNode.getTextContent())) {
} else if (childNode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
hasElement = true;
}
}
if (hasElement) {
Map<String, Object> childMap = xmlToMap(childElement);
if (result.containsKey(tagName)) {
// Convert to list if multiple elements with same name
java.util.List<Object> list = new java.util.ArrayList<>();
if (result.get(tagName) instanceof Map) {
list.add(result.get(tagName));
}
list.add(childMap);
result.put(tagName, list);
} else {
result.put(tagName, childMap);
}
} else {
result.put(tagName, childElement.getTextContent());
}
}
}
}
// If element has only text content and no attributes or children, return text
if (element.getChildNodes().getLength() == 0) {
Map<String, Object> textMap = new HashMap<>();
textMap.put("#text", element.getTextContent());
return textMap;
}
return result;
}
@Override
public String toString() {
return "ReceivedMessage{" +
"contentType=" + contentType +
", source='" + source + '\'' +
", key='" + key + '\'' +
", body='" + body + '\'' +
'}';
}
}

View File

@ -0,0 +1,20 @@
package cz.moneta.test.harness.messaging.exception;
/**
* Exception thrown when connection to messaging system fails.
* Includes authentication failures, network issues, etc.
*/
public class MessagingConnectionException extends MessagingException {
public MessagingConnectionException(String message) {
super(message);
}
public MessagingConnectionException(String message, Throwable cause) {
super(message, cause);
}
public MessagingConnectionException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package cz.moneta.test.harness.messaging.exception;
/**
* Exception thrown when destination (topic/queue) does not exist or is inaccessible.
* Includes permission issues and unknown object errors.
*/
public class MessagingDestinationException extends MessagingException {
public MessagingDestinationException(String message) {
super(message);
}
public MessagingDestinationException(String message, Throwable cause) {
super(message, cause);
}
public MessagingDestinationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package cz.moneta.test.harness.messaging.exception;
/**
* Base exception for all messaging-related errors.
* Extends RuntimeException for unchecked behavior.
*/
public abstract class MessagingException extends RuntimeException {
protected MessagingException(String message) {
super(message);
}
protected MessagingException(String message, Throwable cause) {
super(message, cause);
}
protected MessagingException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package cz.moneta.test.harness.messaging.exception;
/**
* Exception thrown when schema validation fails or schema is not found.
* Used for Avro schema mismatches or missing schema in Schema Registry.
*/
public class MessagingSchemaException extends MessagingException {
public MessagingSchemaException(String message) {
super(message);
}
public MessagingSchemaException(String message, Throwable cause) {
super(message, cause);
}
public MessagingSchemaException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package cz.moneta.test.harness.messaging.exception;
/**
* Exception thrown when message receiving times out.
* No matching message found within the specified timeout period.
*/
public class MessagingTimeoutException extends MessagingException {
public MessagingTimeoutException(String message) {
super(message);
}
public MessagingTimeoutException(String message, Throwable cause) {
super(message, cause);
}
public MessagingTimeoutException(Throwable cause) {
super(cause);
}
}

View File

@ -1,22 +0,0 @@
package cz.moneta.test.harness.messaging.model;
import java.util.Objects;
public abstract class Destination {
private final String name;
private final String type;
protected Destination(String name, String type) {
this.name = Objects.requireNonNull(name, "name");
this.type = Objects.requireNonNull(type, "type");
}
public String getName() {
return name;
}
public String getType() {
return type;
}
}

View File

@ -1,7 +0,0 @@
package cz.moneta.test.harness.messaging.model;
public enum MessageContentType {
JSON,
XML,
RAW_TEXT
}

View File

@ -1,18 +0,0 @@
package cz.moneta.test.harness.messaging.model;
import java.util.Locale;
import java.util.Optional;
public enum MqMessageFormat {
JSON,
XML,
EBCDIC_870,
UTF8_1208;
public static MqMessageFormat fromConfig(String value, MqMessageFormat defaultValue) {
return Optional.ofNullable(value)
.map(v -> v.toUpperCase(Locale.ROOT).replace('-', '_'))
.map(MqMessageFormat::valueOf)
.orElse(defaultValue);
}
}

View File

@ -1,23 +0,0 @@
package cz.moneta.test.harness.messaging.model;
import java.util.Objects;
public class Queue extends Destination {
private final String queueName;
private final MqMessageFormat format;
public Queue(String name, String queueName, MqMessageFormat format) {
super(name, "ibmmq");
this.queueName = Objects.requireNonNull(queueName, "queueName");
this.format = Objects.requireNonNull(format, "format");
}
public String getQueueName() {
return queueName;
}
public MqMessageFormat getFormat() {
return format;
}
}

View File

@ -1,167 +0,0 @@
package cz.moneta.test.harness.messaging.model;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.apache.commons.lang3.StringUtils;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
public class ReceivedMessage {
private static final Pattern ARRAY_NODE_PATTERN = Pattern.compile("(.*?)\\[([0-9]*?)\\]");
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final XmlMapper XML_MAPPER = new XmlMapper();
private final String body;
private final MessageContentType contentType;
private final Map<String, String> headers;
private final long timestamp;
private final String source;
public ReceivedMessage(String body,
MessageContentType contentType,
Map<String, String> headers,
long timestamp,
String source) {
this.body = Optional.ofNullable(body).orElse("");
this.contentType = Objects.requireNonNull(contentType, "contentType");
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(Optional.ofNullable(headers).orElseGet(Collections::emptyMap)));
this.timestamp = timestamp;
this.source = Optional.ofNullable(source).orElse("");
}
public JsonNode extractJson(String path) {
JsonNode root = readJsonLikeNode();
return extractNode(path, root);
}
public String extractXml(String xpathExpression) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(body)));
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new EmptyNamespaceContext());
return String.valueOf(xpath.evaluate(xpathExpression, document, XPathConstants.STRING));
} catch (Exception e) {
throw new IllegalStateException("Failed to extract xml value for expression: " + xpathExpression, e);
}
}
public String extract(String expression) {
return switch (contentType) {
case JSON -> extractJson(expression).asText();
case XML -> extractXml(expression);
case RAW_TEXT -> body;
};
}
public <T> T mapTo(Class<T> type) {
try {
return switch (contentType) {
case JSON -> JSON_MAPPER.readValue(body, type);
case XML -> XML_MAPPER.readValue(body, type);
case RAW_TEXT -> {
if (String.class.equals(type)) {
yield type.cast(body);
}
throw new IllegalStateException("RAW_TEXT can only be mapped to String");
}
};
} catch (Exception e) {
throw new IllegalStateException("Failed to map message body", e);
}
}
public String getBody() {
return body;
}
public MessageContentType getContentType() {
return contentType;
}
public Map<String, String> getHeaders() {
return headers;
}
public long getTimestamp() {
return timestamp;
}
public String getSource() {
return source;
}
private JsonNode readJsonLikeNode() {
try {
return switch (contentType) {
case JSON -> JSON_MAPPER.readTree(body);
case XML -> XML_MAPPER.readTree(body.getBytes());
case RAW_TEXT -> {
if (StringUtils.isBlank(body)) {
yield MissingNode.getInstance();
}
yield JSON_MAPPER.readTree(body);
}
};
} catch (Exception e) {
throw new IllegalStateException("Unable to parse message as JSON-like content", e);
}
}
private static JsonNode extractNode(String path, JsonNode rootNode) {
return Arrays.stream(path.split("\\."))
.filter(StringUtils::isNotEmpty)
.reduce(rootNode,
(r, p) -> {
Matcher matcher = ARRAY_NODE_PATTERN.matcher(p);
if (matcher.find()) {
return r.path(matcher.group(1)).path(Integer.parseInt(matcher.group(2)));
}
return r.path(p);
},
(j1, j2) -> j1);
}
private static final class EmptyNamespaceContext implements NamespaceContext {
@Override
public String getNamespaceURI(String prefix) {
return XMLConstants.NULL_NS_URI;
}
@Override
public String getPrefix(String namespaceURI) {
return "";
}
@Override
public Iterator<String> getPrefixes(String namespaceURI) {
return Collections.emptyIterator();
}
}
}

View File

@ -1,17 +0,0 @@
package cz.moneta.test.harness.messaging.model;
import java.util.Objects;
public class Topic extends Destination {
private final String topicName;
public Topic(String name, String topicName) {
super(name, "kafka");
this.topicName = Objects.requireNonNull(topicName, "topicName");
}
public String getTopicName() {
return topicName;
}
}

View File

@ -0,0 +1,595 @@
package cz.moneta.test.harness.support.messaging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionEndpoint;
import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionQueue;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.messaging.MessageContentType;
import cz.moneta.test.harness.messaging.MqMessageFormat;
import cz.moneta.test.harness.messaging.ReceivedMessage;
import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException;
import cz.moneta.test.harness.support.util.FileReader;
import cz.moneta.test.harness.support.util.Template;
import org.apache.commons.lang3.StringUtils;
import java.time.Duration;
import java.util.*;
import java.util.function.Predicate;
/**
* Fluent builder for IBM MQ requests.
* <p>
* Usage:
* <pre>{@code
* ImqRequest.toQueue(endpoint, queue)
* .asJson()
* .withPayload("{\"field\": \"value\"}")
* .send();
*
* ImqRequest.fromQueue(endpoint, queue)
* .receiveWhere(msg -> msg.extract("field").equals("value"))
* .withTimeout(10, TimeUnit.SECONDS)
* .andAssertFieldValue("result", "OK");
* }</pre>
* </p>
*/
@SuppressWarnings("unchecked")
public final class ImqRequest {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final String PROP_LOGICAL_QUEUE = "LogicalQueue";
private static final String PROP_SELECTOR = "Selector";
public static final String PROP_JMS_CORRELATION_ID = "JMSCorrelationID";
public static final String PROP_JMS_MESSAGE_ID = "JMSMessageID";
public static final String PROP_JMS_TYPE = "JMSType";
private ImqRequest() {
}
/**
* Start building a message to send to a queue.
*/
public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
return new QueueBuilder(endpoint, queue, null);
}
/**
* Start building a message to receive from a queue.
*/
public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue queue) {
return new QueueBuilder(endpoint, queue, null);
}
/**
* Start building a message to send to a queue by physical name.
*/
public static QueuePhase toQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
return new QueueBuilder(endpoint, null, queueName);
}
/**
* Start building a message to receive from a queue by physical name.
*/
public static ReceivePhase fromQueue(ImqFirstVisionEndpoint endpoint, String queueName) {
return new QueueBuilder(endpoint, null, queueName);
}
/**
* Phase after specifying queue - can send or receive.
*/
public interface QueuePhase extends PayloadPhase, ReceivePhase {
}
/**
* Phase for building message payload.
*/
public interface PayloadPhase {
/**
* Set message format to JSON (default).
*/
QueuePhase asJson();
/**
* Set message format to XML.
*/
QueuePhase asXml();
/**
* Set message format to EBCDIC (IBM-870).
*/
QueuePhase asEbcdic();
/**
* Set message format to UTF-8 (CCSID 1208).
*/
QueuePhase asUtf8();
/**
* Set message payload as JSON string.
*/
PayloadPhase withPayload(String payload);
/**
* Load payload from resource file.
*/
PayloadPhase withPayloadFromFile(String path);
/**
* Render payload from template.
*/
PayloadPhase withPayloadFromTemplate(Template template);
/**
* Add field to JSON payload at root level.
*/
PayloadPhase addField(String fieldName, Object value);
/**
* Add field to JSON payload at specified path.
*/
PayloadPhase addField(String path, String fieldName, Object value);
/**
* Append value to JSON array at specified path.
*/
PayloadPhase appendToArray(String path, Object value);
/**
* Set JMS correlation ID.
*/
PayloadPhase withCorrelationId(String correlationId);
/**
* Set JMS message ID.
*/
PayloadPhase withMessageId(String messageId);
/**
* Set JMS type.
*/
PayloadPhase withJmsType(String jmsType);
/**
* Send the message.
*/
void send();
}
/**
* Phase for receiving messages.
*/
public interface ReceivePhase {
/**
* Set JMS message selector.
*/
ReceivePhase withSelector(String selector);
/**
* Start receiving message matching predicate.
*/
AwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter);
/**
* Browse messages from queue (non-destructive).
*/
List<ReceivedMessage> browse(int maxMessages);
/**
* Browse messages from queue with selector (non-destructive).
*/
List<ReceivedMessage> browse(String selector, int maxMessages);
/**
* Set message format to JSON (default) when receiving.
*/
QueuePhase asJson();
/**
* Set message format to XML when receiving.
*/
QueuePhase asXml();
/**
* Set message format to EBCDIC (IBM-870) when receiving.
*/
QueuePhase asEbcdic();
/**
* Set message format to UTF-8 (CCSID 1208) when receiving.
*/
QueuePhase asUtf8();
}
/**
* Phase after specifying filter - await message with timeout.
*/
public interface AwaitingPhase {
/**
* Set timeout and get message response for assertions.
*/
MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit);
/**
* Set timeout duration and get message response for assertions.
*/
MessageResponse withTimeout(Duration timeout);
}
/**
* Response with fluent assertions.
*/
public interface MessageResponse extends cz.moneta.test.harness.support.messaging.MessageResponse {
}
/**
* Builder for queue operations.
*/
private static class QueueBuilder implements QueuePhase, AwaitingPhase {
private final ImqFirstVisionEndpoint endpoint;
private final ImqFirstVisionQueue logicalQueue;
private final String physicalQueue;
private String selector;
private MqMessageFormat format = MqMessageFormat.JSON;
private String payload;
private Map<String, Object> fields = new HashMap<>();
private List<Map.Entry<String, Object>> arrayAppends = new ArrayList<>();
private String correlationId;
private String messageId;
private String jmsType;
private Predicate<ReceivedMessage> filter;
private Duration timeout;
public QueueBuilder(ImqFirstVisionEndpoint endpoint, ImqFirstVisionQueue logicalQueue, String physicalQueue) {
this.endpoint = endpoint;
this.logicalQueue = logicalQueue;
this.physicalQueue = physicalQueue;
}
private String getQueueName() {
if (logicalQueue != null) {
return endpoint.resolveQueue(logicalQueue);
}
return physicalQueue;
}
@Override
public QueuePhase asJson() {
this.format = MqMessageFormat.JSON;
return this;
}
@Override
public QueuePhase asXml() {
this.format = MqMessageFormat.XML;
return this;
}
@Override
public QueuePhase asEbcdic() {
this.format = MqMessageFormat.EBCDIC_870;
return this;
}
@Override
public QueuePhase asUtf8() {
this.format = MqMessageFormat.UTF8_1208;
return this;
}
@Override
public PayloadPhase withPayload(String payload) {
this.payload = payload;
return this;
}
@Override
public PayloadPhase withPayloadFromFile(String path) {
this.payload = FileReader.readFileFromResources(path);
return this;
}
@Override
public PayloadPhase withPayloadFromTemplate(Template template) {
this.payload = template.render();
return this;
}
@Override
public PayloadPhase addField(String fieldName, Object value) {
return addField("", fieldName, value);
}
@Override
public PayloadPhase addField(String path, String fieldName, Object value) {
String key = StringUtils.isNotBlank(path) && StringUtils.isNotBlank(fieldName) ? path + "." + fieldName : fieldName;
this.fields.put(key, value);
return this;
}
@Override
public PayloadPhase appendToArray(String path, Object value) {
this.arrayAppends.add(Map.entry(path, value));
return this;
}
@Override
public PayloadPhase withCorrelationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
@Override
public PayloadPhase withMessageId(String messageId) {
this.messageId = messageId;
return this;
}
@Override
public PayloadPhase withJmsType(String jmsType) {
this.jmsType = jmsType;
return this;
}
@Override
public void send() {
String finalPayload = buildPayload();
Map<String, String> properties = new HashMap<>();
if (logicalQueue != null) {
properties.put(PROP_LOGICAL_QUEUE, logicalQueue.name());
}
if (selector != null && !selector.isBlank()) {
properties.put(PROP_SELECTOR, selector);
}
if (correlationId != null) {
properties.put(PROP_JMS_CORRELATION_ID, correlationId);
}
if (messageId != null) {
properties.put(PROP_JMS_MESSAGE_ID, messageId);
}
if (jmsType != null) {
properties.put(PROP_JMS_TYPE, jmsType);
}
endpoint.send(getQueueName(), finalPayload, format, properties);
}
@Override
public ReceivePhase withSelector(String selector) {
this.selector = selector;
return this;
}
@Override
public AwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter) {
this.filter = filter;
return this;
}
@Override
public List<ReceivedMessage> browse(int maxMessages) {
return browse(selector, maxMessages);
}
@Override
public List<ReceivedMessage> browse(String sel, int maxMessages) {
return endpoint.browse(getQueueName(), sel, format, maxMessages);
}
@Override
public MessageResponse withTimeout(long duration, java.util.concurrent.TimeUnit unit) {
return withTimeout(Duration.of(duration, java.time.temporal.ChronoUnit.MILLIS));
}
@Override
public MessageResponse withTimeout(Duration timeout) {
this.timeout = timeout;
if (filter == null) {
throw new IllegalStateException("Must specify receiveWhere filter before withTimeout");
}
ReceivedMessage message = receiveMessage();
return new ResponseImpl(message);
}
private ReceivedMessage receiveMessage() {
long startTime = System.currentTimeMillis();
long timeoutMs = timeout.toMillis();
while (System.currentTimeMillis() - startTime < timeoutMs) {
try {
ReceivedMessage message = endpoint.receive(getQueueName(), selector, format, Duration.ofSeconds(1));
if (filter.test(message)) {
return message;
}
} catch (MessagingTimeoutException e) {
// Continue polling
}
}
throw new MessagingTimeoutException(
"No message matching filter found on queue '" + getQueueName() +
"' within " + timeout.toMillis() + "ms");
}
private String buildPayload() {
if (payload == null) {
return "{}";
}
if (fields.isEmpty() && arrayAppends.isEmpty()) {
return payload;
}
try {
Map<String, Object> json = JSON_MAPPER.readValue(payload, Map.class);
for (Map.Entry<String, Object> entry : fields.entrySet()) {
setField(json, entry.getKey(), entry.getValue());
}
for (Map.Entry<String, Object> entry : arrayAppends) {
appendToArray(json, entry.getKey(), entry.getValue());
}
return JSON_MAPPER.writeValueAsString(json);
} catch (Exception e) {
throw new HarnessException("Failed to build payload", e);
}
}
private void setField(Map<String, Object> json, String path, Object value) {
if (StringUtils.isBlank(path)) {
json.put(path, value);
return;
}
String[] parts = path.split("\\.");
Map<String, Object> current = json;
for (int i = 0; i < parts.length - 1; i++) {
String part = parts[i];
Object next = current.get(part);
if (!(next instanceof Map)) {
next = new HashMap<String, Object>();
current.put(part, next);
}
current = (Map<String, Object>) next;
}
current.put(parts[parts.length - 1], value);
}
private void appendToArray(Map<String, Object> json, String path, Object value) {
String[] parts = path.split("\\.");
Map<String, Object> current = json;
for (int i = 0; i < parts.length - 1; i++) {
String part = parts[i];
Object next = current.get(part);
if (!(next instanceof Map)) {
next = new HashMap<String, Object>();
current.put(part, next);
}
current = (Map<String, Object>) next;
}
String arrayKey = parts[parts.length - 1];
Object arrayValue = current.get(arrayKey);
List<Map<String, Object>> array;
if (arrayValue instanceof List) {
array = (List<Map<String, Object>>) arrayValue;
} else {
array = new ArrayList<>();
current.put(arrayKey, array);
}
array.add((Map<String, Object>) value);
}
}
/**
* Response implementation with assertions.
*/
private static class ResponseImpl implements MessageResponse {
private final ReceivedMessage message;
public ResponseImpl(ReceivedMessage message) {
this.message = message;
}
@Override
public MessageResponse andAssertFieldValue(String path, String value) {
String actual = message.extract(path);
if (!Objects.equals(value, actual)) {
throw new AssertionError(String.format("Expected field '%s' to be '%s' but was '%s'", path, value, actual));
}
return this;
}
@Override
public MessageResponse andAssertPresent(String path) {
JsonPathValue extracted = new JsonPathValue(message.extract(path));
if (extracted.asText() == null && !isPresentInJson(path)) {
throw new AssertionError(String.format("Expected field '%s' to be present", path));
}
return this;
}
@Override
public MessageResponse andAssertNotPresent(String path) {
if (isPresentInJson(path)) {
throw new AssertionError(String.format("Expected field '%s' to be absent", path));
}
return this;
}
@Override
public MessageResponse andAssertHeaderValue(String headerName, String value) {
String actual = message.getHeader(headerName);
if (!Objects.equals(value, actual)) {
throw new AssertionError(String.format("Expected header '%s' to be '%s' but was '%s'", headerName, value, actual));
}
return this;
}
@Override
public MessageResponse andAssertBodyContains(String substring) {
if (!message.getBody().contains(substring)) {
throw new AssertionError(String.format("Body does not contain '%s'", substring));
}
return this;
}
@Override
public JsonPathValue extract(String path) {
return new JsonPathValue(message.extract(path));
}
@Override
public <T> T mapTo(Class<T> type) {
return message.mapTo(type);
}
@Override
public cz.moneta.test.harness.support.messaging.ReceivedMessage getMessage() {
return cz.moneta.test.harness.support.messaging.ReceivedMessage.fromMessagingReceivedMessage(message);
}
@Override
public String getBody() {
return message.getBody();
}
@Override
public String getHeader(String name) {
return message.getHeader(name);
}
private boolean isPresentInJson(String path) {
try {
String body = message.getBody();
if (message.getContentType() != MessageContentType.JSON) {
return false;
}
JsonNode node = JSON_MAPPER.readTree(body);
JsonNode target = extractNode(path, node);
return target != null && !target.isMissingNode();
} catch (Exception e) {
return false;
}
}
private JsonNode extractNode(String path, JsonNode node) {
return Arrays.stream(path.split("\\."))
.filter(StringUtils::isNotEmpty)
.reduce(node,
(n, p) -> n.isContainerNode() ? n.get(p) : n,
(n1, n2) -> n1);
}
}
}

View File

@ -0,0 +1,65 @@
package cz.moneta.test.harness.support.messaging;
/**
* Wrapper for extracted JSON path value.
*/
public class JsonPathValue {
private final String value;
public JsonPathValue(String value) {
this.value = value;
}
/**
* Returns the value as String.
*/
public String asText() {
return value;
}
/**
* Returns the value as Boolean.
*/
public Boolean asBoolean() {
if (value == null) {
return null;
}
return Boolean.parseBoolean(value);
}
/**
* Returns the value as Integer.
*/
public Integer asInteger() {
if (value == null) {
return null;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse '" + value + "' as Integer", e);
}
}
/**
* Returns the value as Long.
*/
public Long asLong() {
if (value == null) {
return null;
}
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse '" + value + "' as Long", e);
}
}
/**
* Returns the underlying value.
*/
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,19 @@
package cz.moneta.test.harness.support.messaging;
/**
* Content type of a received message.
*/
public enum MessageContentType {
/**
* JSON content - body is a JSON string, can use dot-path or bracket notation for extraction
*/
JSON,
/**
* XML content - body is an XML string, can use XPath for extraction
*/
XML,
/**
* Raw text content - body is plain text without structured format
*/
RAW_TEXT
}

View File

@ -0,0 +1,90 @@
package cz.moneta.test.harness.support.messaging;
/**
* Response from a messaging operation (send/receive).
* Provides fluent assertion API for received messages.
*/
public interface MessageResponse {
/**
* Asserts that a field has the expected value.
*
* @param path the field path (JSON dot-path for JSON, XPath for XML)
* @param value the expected value
* @return this for method chaining
*/
MessageResponse andAssertFieldValue(String path, String value);
/**
* Asserts that a field is present in the message.
*
* @param path the field path
* @return this for method chaining
*/
MessageResponse andAssertPresent(String path);
/**
* Asserts that a field is NOT present in the message.
*
* @param path the field path
* @return this for method chaining
*/
MessageResponse andAssertNotPresent(String path);
/**
* Asserts that a header has the expected value.
*
* @param headerName the header name
* @param value the expected value
* @return this for method chaining
*/
MessageResponse andAssertHeaderValue(String headerName, String value);
/**
* Asserts that the body contains the given substring.
* Useful for raw text or EBCDIC/UTF-8 encoded messages.
*
* @param substring the expected substring
* @return this for method chaining
*/
MessageResponse andAssertBodyContains(String substring);
/**
* Extracts a value from the message.
*
* @param path the path expression
* @return JsonPathValue for further assertion
*/
JsonPathValue extract(String path);
/**
* Deserializes the message body into a Java object.
*
* @param type the target class
* @param <T> the target type
* @return the deserialized object
*/
<T> T mapTo(Class<T> type);
/**
* Returns the received message.
*
* @return the message
*/
ReceivedMessage getMessage();
/**
* Returns the message body.
*
* @return the body
*/
String getBody();
/**
* Returns a header value.
*
* @param name the header name
* @return the header value or null
*/
String getHeader(String name);
}

View File

@ -1,193 +0,0 @@
package cz.moneta.test.harness.support.messaging;
import cz.moneta.test.harness.endpoints.messaging.MessagingEndpoint;
import cz.moneta.test.harness.messaging.model.MqMessageFormat;
import cz.moneta.test.harness.messaging.model.ReceivedMessage;
import cz.moneta.test.harness.support.util.FileReader;
import cz.moneta.test.harness.support.util.Template;
import org.junit.jupiter.api.Assertions;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public final class MessagingRequest {
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
private final MessagingEndpoint endpoint;
private String destinationName;
private String key;
private String payload;
private Duration timeout = DEFAULT_TIMEOUT;
private final Map<String, String> headers = new LinkedHashMap<>();
private MqMessageFormat formatOverride;
private Predicate<ReceivedMessage> receiveFilter;
private ReceivedMessage receivedMessage;
private boolean receivePending;
private Mode mode = Mode.UNSET;
private MessagingRequest(MessagingEndpoint endpoint) {
this.endpoint = endpoint;
}
public static MessagingRequest builder(MessagingEndpoint endpoint) {
return new MessagingRequest(endpoint);
}
public MessagingRequest to(String destinationName) {
this.destinationName = destinationName;
this.mode = Mode.SEND;
resetReceiveState();
return this;
}
public MessagingRequest from(String destinationName) {
this.destinationName = destinationName;
this.mode = Mode.RECEIVE;
resetReceiveState();
return this;
}
public MessagingRequest withKey(String key) {
this.key = key;
return this;
}
public MessagingRequest withPayload(String payload) {
this.payload = payload;
return this;
}
public MessagingRequest withPayloadFromTemplate(Template template) {
this.payload = template.render();
return this;
}
public MessagingRequest withPayloadFromFile(String path) {
this.payload = FileReader.readFileFromResources(path);
return this;
}
public MessagingRequest withHeader(String key, String value) {
this.headers.put(key, value);
return this;
}
public MessagingRequest withTraceparent(String value) {
return withHeader("traceparent", value);
}
public MessagingRequest withRequestID(String value) {
return withHeader("requestID", value);
}
public MessagingRequest withActivityID(String value) {
return withHeader("activityID", value);
}
public MessagingRequest withSourceCodebookId(String value) {
return withHeader("sourceCodebookId", value);
}
public MessagingRequest asJson() {
this.formatOverride = MqMessageFormat.JSON;
return this;
}
public MessagingRequest asXml() {
this.formatOverride = MqMessageFormat.XML;
return this;
}
public MessagingRequest asEbcdic() {
this.formatOverride = MqMessageFormat.EBCDIC_870;
return this;
}
public MessagingRequest asUtf8() {
this.formatOverride = MqMessageFormat.UTF8_1208;
return this;
}
public MessagingRequest withTimeout(long value, TimeUnit unit) {
this.timeout = Duration.ofMillis(unit.toMillis(value));
if (receivePending && receivedMessage == null) {
doReceive();
}
return this;
}
public MessagingRequest send() {
ensureMode(Mode.SEND);
if (payload == null) {
throw new IllegalStateException("Message payload must be provided before send()");
}
endpoint.send(destinationName, key, payload, formatOverride, headers);
return this;
}
public MessagingRequest receiveWhere(Predicate<ReceivedMessage> filter) {
ensureMode(Mode.RECEIVE);
this.receiveFilter = filter;
this.receivePending = true;
this.receivedMessage = null;
return this;
}
public MessagingRequest andAssertFieldValue(String expression, String expectedValue) {
ReceivedMessage message = getReceivedMessage();
Assertions.assertEquals(expectedValue,
message.extract(expression),
String.format("Unexpected message field value for '%s'. Message body: %s", expression, message.getBody()));
return this;
}
public String extract(String expression) {
return getReceivedMessage().extract(expression);
}
public ReceivedMessage getReceivedMessage() {
ensureMode(Mode.RECEIVE);
if (receivedMessage == null) {
doReceive();
}
return receivedMessage;
}
private void doReceive() {
if (!receivePending) {
receiveFilter = msg -> true;
receivePending = true;
}
receivedMessage = endpoint.receive(
destinationName,
Optional.ofNullable(receiveFilter).orElse(msg -> true),
timeout,
formatOverride
);
receivePending = false;
}
private void ensureMode(Mode requiredMode) {
if (this.mode != requiredMode) {
throw new IllegalStateException("Messaging request is not in " + requiredMode + " mode");
}
}
private void resetReceiveState() {
this.receiveFilter = null;
this.receivedMessage = null;
this.receivePending = false;
}
private enum Mode {
UNSET,
SEND,
RECEIVE
}
}

View File

@ -0,0 +1,23 @@
package cz.moneta.test.harness.support.messaging;
/**
* Message format for IBM MQ messages.
*/
public enum MqMessageFormat {
/**
* JSON format - JMS TextMessage with plain JSON string (UTF-8 default)
*/
JSON,
/**
* XML format - JMS TextMessage with XML string (UTF-8 default)
*/
XML,
/**
* EBCDIC format - JMS BytesMessage with EBCDIC IBM-870 encoding (CZ/SK mainframe)
*/
EBCDIC_870,
/**
* UTF-8 format - JMS BytesMessage with UTF-8 IBM CCSID 1208 encoding
*/
UTF8_1208
}

View File

@ -0,0 +1,304 @@
package cz.moneta.test.harness.support.messaging;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.apache.commons.lang3.StringUtils;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.w3c.dom.Document;
/**
* Represents a received message from a messaging system (IBM MQ or Kafka).
* <p>
* Provides unified API for accessing message content regardless of source system.
* Body is always normalized to a String, with content type detection for proper extraction.
* </p>
*/
public class ReceivedMessage {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final XmlMapper XML_MAPPER = new XmlMapper();
private final String body;
private final MessageContentType contentType;
private final Map<String, String> headers;
private final long timestamp;
private final String source;
private final String key;
private ReceivedMessage(Builder builder) {
this.body = builder.body;
this.contentType = builder.contentType;
this.headers = builder.headers != null ? Collections.unmodifiableMap(new HashMap<>(builder.headers)) : Collections.emptyMap();
this.timestamp = builder.timestamp;
this.source = builder.source;
this.key = builder.key;
}
/**
* Creates a new builder for ReceivedMessage.
*/
public static Builder builder() {
return new Builder();
}
/**
* Extracts a value from the message body using JSON dot-path or bracket notation.
* <p>
* Supports paths like:
* <ul>
* <li>{@code "field"} - top-level field</li>
* <li>{@code "parent.child"} - nested field</li>
* <li>{@code "items[0]"} - array element</li>
* <li>{@code "items[0].name"} - nested field in array element</li>
* </ul>
* </p>
*
* @param path the JSON path expression
* @return the extracted value as JsonNode
*/
public JsonNode extractJson(String path) {
if (contentType != MessageContentType.JSON) {
throw new IllegalStateException("JSON extraction is only supported for JSON content type, got: " + contentType);
}
try {
JsonNode rootNode = JSON_MAPPER.readTree(body);
return extractNode(path, rootNode);
} catch (IOException e) {
throw new RuntimeException("Failed to parse JSON body: " + body, e);
}
}
/**
* Extracts a value from XML message body using XPath expression.
*
* @param xpath the XPath expression
* @return the extracted value as String
*/
public String extractXml(String xpath) {
if (contentType != MessageContentType.XML) {
throw new IllegalStateException("XML extraction is only supported for XML content type, got: " + contentType);
}
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
XPath xPath = XPathFactory.newInstance().newXPath();
return xPath.evaluate(xpath, doc);
} catch (XPathExpressionException e) {
throw new RuntimeException("Failed to evaluate XPath: " + xpath, e);
} catch (Exception e) {
throw new RuntimeException("Failed to parse XML body: " + body, e);
}
}
/**
* Extracts a value from the message body. Auto-detects content type and uses appropriate extraction method.
*
* @param expression the path expression (JSON path for JSON, XPath for XML)
* @return the extracted value as String
*/
public String extract(String expression) {
return switch (contentType) {
case JSON -> extractJson(expression).asText();
case XML -> extractXml(expression);
case RAW_TEXT -> body;
};
}
/**
* Returns the message body as a String.
*
* @return the body content
*/
public String getBody() {
return body;
}
/**
* Returns the message content type.
*
* @return the content type
*/
public MessageContentType getContentType() {
return contentType;
}
/**
* Returns a header value by name.
*
* @param name the header name
* @return the header value, or null if not present
*/
public String getHeader(String name) {
return headers.get(name);
}
/**
* Returns all headers.
*
* @return unmodifiable map of headers
*/
public Map<String, String> getHeaders() {
return headers;
}
/**
* Returns the message timestamp.
*
* @return timestamp in milliseconds
*/
public long getTimestamp() {
return timestamp;
}
/**
* Returns the source (topic name for Kafka, queue name for IBM MQ).
*
* @return the source name
*/
public String getSource() {
return source;
}
/**
* Returns the message key (Kafka only, null for IBM MQ).
*
* @return the message key or null
*/
public String getKey() {
return key;
}
/**
* Deserializes the message body into a Java object.
* <p>
* For JSON content: uses Jackson ObjectMapper.readValue
* For XML content: uses Jackson XmlMapper
* </p>
*
* @param type the target class
* @param <T> the target type
* @return the deserialized object
*/
public <T> T mapTo(Class<T> type) {
try {
if (contentType == MessageContentType.XML) {
return XML_MAPPER.readValue(body, type);
} else {
return JSON_MAPPER.readValue(body, type);
}
} catch (IOException e) {
throw new RuntimeException("Failed to deserialize message body to " + type.getName(), e);
}
}
/**
* Builder for ReceivedMessage.
*/
public static class Builder {
private String body;
private MessageContentType contentType = MessageContentType.JSON;
private Map<String, String> headers;
private long timestamp = System.currentTimeMillis();
private String source;
private String key;
public Builder body(String body) {
this.body = body;
return this;
}
public Builder contentType(MessageContentType contentType) {
this.contentType = contentType;
return this;
}
public Builder headers(Map<String, String> headers) {
this.headers = headers;
return this;
}
public Builder timestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder source(String source) {
this.source = source;
return this;
}
public Builder key(String key) {
this.key = key;
return this;
}
public ReceivedMessage build() {
return new ReceivedMessage(this);
}
}
/**
* Extracts a node from JSON using dot/bracket notation.
*/
private static JsonNode extractNode(String path, JsonNode rootNode) {
Pattern arrayPattern = Pattern.compile("(.*?)\\[([0-9]+)\\]");
return Arrays.stream(path.split("\\."))
.filter(StringUtils::isNotEmpty)
.reduce(rootNode,
(node, part) -> {
Matcher matcher = arrayPattern.matcher(part);
if (matcher.find()) {
return node.path(matcher.group(1)).path(Integer.parseInt(matcher.group(2)));
} else {
return node.path(part);
}
},
(n1, n2) -> n1);
}
/**
* Converts a cz.moneta.test.harness.messaging.ReceivedMessage to this class.
*
* @param other the message to convert
* @return converted message
*/
public static ReceivedMessage fromMessagingReceivedMessage(
cz.moneta.test.harness.messaging.ReceivedMessage other) {
if (other == null) {
return null;
}
cz.moneta.test.harness.support.messaging.MessageContentType contentType =
switch (other.getContentType()) {
case JSON -> cz.moneta.test.harness.support.messaging.MessageContentType.JSON;
case XML -> cz.moneta.test.harness.support.messaging.MessageContentType.XML;
case RAW_TEXT -> cz.moneta.test.harness.support.messaging.MessageContentType.RAW_TEXT;
};
return builder()
.body(other.getBody())
.contentType(contentType)
.headers(other.getHeaders())
.timestamp(other.getTimestamp())
.source(other.getSource())
.key(other.getKey())
.build();
}
}

View File

@ -0,0 +1,568 @@
package cz.moneta.test.harness.support.messaging.kafka;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import cz.moneta.test.harness.endpoints.kafka.KafkaEndpoint;
import cz.moneta.test.harness.support.util.FileReader;
/**
* Fluent builder for creating and sending Kafka messages.
* <p>
* Usage:
* <pre>{@code
* harness.withKafka()
* .toTopic("order-events")
* .withKey("order-123")
* .withPayload("{\"orderId\": \"123\", \"status\": \"CREATED\"}")
* .withTraceparent("00-traceId-spanId-01")
* .send();
* }</pre>
* </p>
*/
public class KafkaRequest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Pattern ARRAY_NODE_PATTERN = Pattern.compile("(.*?)\\[([0-9]*?)\\]");
private KafkaRequest() {
}
/**
* Creates a new Kafka request builder.
*/
public static KafkaBuilder builder(KafkaEndpoint endpoint) {
return new KafkaRequestBuilder(endpoint);
}
/**
* Phase 1: Select direction (send or receive).
*/
public interface KafkaBuilder {
/**
* Specifies the target topic for sending.
*
* @param topic Kafka topic name
* @return KafkaPayloadPhase for payload configuration
*/
KafkaPayloadPhase toTopic(String topic);
/**
* Specifies the source topic for receiving.
*
* @param topic Kafka topic name
* @return KafkaReceiveFilterPhase for filter configuration
*/
KafkaReceiveFilterPhase fromTopic(String topic);
}
/**
* Phase 2a: Configure payload and headers for sending.
*/
public interface KafkaPayloadPhase {
/**
* Sets the message key.
*
* @param key Message key
* @return this for chaining
*/
KafkaPayloadPhase withKey(String key);
/**
* Sets the payload as JSON string.
*
* @param json JSON payload
* @return this for chaining
*/
KafkaPayloadPhase withPayload(String json);
/**
* Loads payload from a file.
*
* @param path Path to JSON file in resources
* @return this for chaining
*/
KafkaPayloadPhase withPayloadFromFile(String path);
/**
* Loads payload from a template file and renders it.
*
* @param path Path to template file
* @param variables Variables for template rendering
* @return this for chaining
*/
KafkaPayloadPhase withPayloadFromTemplate(String path, Map<String, Object> variables);
/**
* Adds a field to the JSON payload.
*
* @param fieldName Field name
* @param value Field value
* @return this for chaining
*/
KafkaPayloadPhase addField(String fieldName, Object value);
/**
* Adds a field to a nested path in the JSON payload.
*
* @param path Path to parent node (e.g., "items[1].details")
* @param fieldName Field name to add
* @param value Field value
* @return this for chaining
*/
KafkaPayloadPhase addField(String path, String fieldName, Object value);
/**
* Appends a value to an array in the JSON payload.
*
* @param path Path to array (e.g., "items")
* @param value Value to append
* @return this for chaining
*/
KafkaPayloadPhase appendToArray(String path, Object value);
/**
* Adds W3C Trace Context traceparent header.
*
* @param value Traceparent value (e.g., "00-traceId-spanId-01")
* @return this for chaining
*/
KafkaPayloadPhase withTraceparent(String value);
/**
* Adds requestID header.
*
* @param value Request ID
* @return this for chaining
*/
KafkaPayloadPhase withRequestID(String value);
/**
* Adds activityID header.
*
* @param value Activity ID
* @return this for chaining
*/
KafkaPayloadPhase withActivityID(String value);
/**
* Adds sourceCodebookId header.
*
* @param value Source codebook ID
* @return this for chaining
*/
KafkaPayloadPhase withSourceCodebookId(String value);
/**
* Adds a custom header.
*
* @param key Header name
* @param value Header value
* @return this for chaining
*/
KafkaPayloadPhase withHeader(String key, String value);
/**
* Sends the message.
*/
void send();
}
/**
* Phase 2b: Configure filter for receiving.
*/
public interface KafkaReceiveFilterPhase {
/**
* Specifies the filter predicate for receiving messages.
*
* @param filter Predicate to filter messages
* @return KafkaAwaitingPhase for timeout configuration
*/
KafkaAwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter);
}
/**
* Phase 3: Configure timeout.
*/
public interface KafkaAwaitingPhase {
/**
* Sets the timeout for waiting for a message.
*
* @param duration Timeout duration
* @param unit Time unit
* @return MessageResponse for assertions
*/
MessageResponse withTimeout(long duration, TimeUnit unit);
}
/**
* Internal implementation class.
*/
private static class KafkaRequestBuilder implements
KafkaBuilder,
KafkaPayloadPhase, KafkaReceiveFilterPhase, KafkaAwaitingPhase,
MessageResponse {
private final KafkaEndpoint endpoint;
// Send configuration
private String topic;
private String key;
private String payload;
private final Map<String, String> headers = new HashMap<>();
// Receive configuration
private Predicate<ReceivedMessage> filter;
private Duration timeout;
// Response (after receive)
private List<ReceivedMessage> messages;
public KafkaRequestBuilder(KafkaEndpoint endpoint) {
this.endpoint = endpoint;
}
// === Send phase ===
@Override
public KafkaPayloadPhase toTopic(String topic) {
this.topic = topic;
return this;
}
@Override
public KafkaPayloadPhase withKey(String key) {
this.key = key;
return this;
}
@Override
public KafkaPayloadPhase withPayload(String json) {
this.payload = json;
return this;
}
@Override
public KafkaPayloadPhase withPayloadFromFile(String path) {
this.payload = FileReader.readFileFromResources(path);
return this;
}
@Override
public KafkaPayloadPhase withPayloadFromTemplate(String path, Map<String, Object> variables) {
String templateContent = FileReader.readFileFromResources(path);
cz.moneta.test.harness.support.util.Template template =
new cz.moneta.test.harness.support.util.Template(templateContent);
if (variables != null) {
variables.forEach((k, v) -> template.set(k, v != null ? v.toString() : ""));
}
this.payload = template.render();
return this;
}
@Override
public KafkaPayloadPhase addField(String fieldName, Object value) {
return addField("", fieldName, value);
}
@Override
public KafkaPayloadPhase addField(String path, String fieldName, Object value) {
try {
if (payload == null) {
payload = "{}";
}
JsonNode rootNode = MAPPER.readTree(payload);
JsonNode targetNode = extractNode(path, rootNode);
JsonNode newNode = MAPPER.valueToTree(value);
((ObjectNode) targetNode).set(fieldName, newNode);
payload = MAPPER.writeValueAsString(rootNode);
} catch (Exception e) {
throw new IllegalStateException("Failed to add field '" + fieldName + "' at path '" + path + "': " + e.getMessage(), e);
}
return this;
}
@Override
public KafkaPayloadPhase appendToArray(String path, Object value) {
try {
if (payload == null) {
payload = "{}";
}
JsonNode rootNode = MAPPER.readTree(payload);
JsonNode targetNode = extractNode(path, rootNode);
JsonNode newNode = MAPPER.valueToTree(value);
if (targetNode.isArray()) {
((ArrayNode) targetNode).add(newNode);
} else {
throw new IllegalStateException("Path '" + path + "' does not point to an array");
}
payload = MAPPER.writeValueAsString(rootNode);
} catch (Exception e) {
throw new IllegalStateException("Failed to append to array at path '" + path + "': " + e.getMessage(), e);
}
return this;
}
@Override
public KafkaPayloadPhase withTraceparent(String value) {
return withHeader("traceparent", value);
}
@Override
public KafkaPayloadPhase withRequestID(String value) {
return withHeader("requestID", value);
}
@Override
public KafkaPayloadPhase withActivityID(String value) {
return withHeader("activityID", value);
}
@Override
public KafkaPayloadPhase withSourceCodebookId(String value) {
return withHeader("sourceCodebookId", value);
}
@Override
public KafkaPayloadPhase withHeader(String key, String value) {
this.headers.put(key, value);
return this;
}
@Override
public void send() {
if (topic == null) {
throw new IllegalStateException("Topic not specified. Call toTopic() first.");
}
if (payload == null) {
throw new IllegalStateException("Payload not specified. Call withPayload() or withPayloadFromFile() first.");
}
endpoint.send(topic, key, payload, headers);
}
// === Receive phase ===
@Override
public KafkaReceiveFilterPhase fromTopic(String topic) {
this.topic = topic;
return this;
}
@Override
public KafkaAwaitingPhase receiveWhere(Predicate<ReceivedMessage> filter) {
this.filter = filter;
return this;
}
@Override
public MessageResponse withTimeout(long duration, TimeUnit unit) {
this.timeout = Duration.of(duration, unit.toChronoUnit());
messages = endpoint.receive(topic, filter, timeout);
return this;
}
// === MessageResponse implementation ===
@Override
public MessageResponse andAssertFieldValue(String path, String value) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
ReceivedMessage msg = messages.get(0);
String actual = msg.extract(path);
if (!Objects.equals(value, actual)) {
throw new AssertionError(
String.format("Field '%s' has value '%s', expected '%s'", path, actual, value));
}
return this;
}
@Override
public MessageResponse andAssertPresent(String path) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
ReceivedMessage msg = messages.get(0);
JsonNode node = msg.extractJson(path);
if (node.isMissingNode()) {
throw new AssertionError("Field '" + path + "' is missing");
}
return this;
}
@Override
public MessageResponse andAssertNotPresent(String path) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
ReceivedMessage msg = messages.get(0);
JsonNode node = msg.extractJson(path);
if (!node.isMissingNode()) {
throw new AssertionError("Field '" + path + "' is present with value: " + node.asText());
}
return this;
}
@Override
public MessageResponse andAssertHeaderValue(String headerName, String value) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
ReceivedMessage msg = messages.get(0);
String actual = msg.getHeader(headerName);
if (!Objects.equals(value, actual)) {
throw new AssertionError(
String.format("Header '%s' has value '%s', expected '%s'", headerName, actual, value));
}
return this;
}
@Override
public MessageResponse andAssertBodyContains(String substring) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
ReceivedMessage msg = messages.get(0);
String body = msg.getBody();
if (body == null || !body.contains(substring)) {
throw new AssertionError(
String.format("Body does not contain '%s'. Actual body: %s", substring, body));
}
return this;
}
@Override
public net.javacrumbs.jsonunit.assertj.JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ() {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to assert on");
}
return net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson(messages.get(0).getBody());
}
@Override
public JsonPathValue extract(String path) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to extract from");
}
JsonNode node = messages.get(0).extractJson(path);
return new JsonPathValueImpl(node);
}
@Override
public <T> T mapTo(Class<T> type) {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received to map");
}
return messages.get(0).mapTo(type);
}
@Override
public ReceivedMessage getMessage() {
if (messages == null || messages.isEmpty()) {
throw new IllegalStateException("No message received");
}
return messages.get(0);
}
@Override
public String getBody() {
if (messages == null || messages.isEmpty()) {
return null;
}
return messages.get(0).getBody();
}
@Override
public String getHeader(String name) {
if (messages == null || messages.isEmpty()) {
return null;
}
return messages.get(0).getHeader(name);
}
// === Helper methods ===
private JsonNode extractNode(String path, JsonNode rootNode) {
if (StringUtils.isBlank(path)) {
return rootNode;
}
return Arrays.stream(path.split("\\."))
.filter(StringUtils::isNotEmpty)
.reduce(rootNode,
(r, p) -> {
Matcher matcher = ARRAY_NODE_PATTERN.matcher(p);
if (matcher.find()) {
return r.path(matcher.group(1)).path(Integer.valueOf(matcher.group(2)));
} else {
return r.path(p);
}
},
(j1, j2) -> j1);
}
}
/**
* Implementation of JsonPathValue.
*/
private static class JsonPathValueImpl implements MessageResponse.JsonPathValue {
private final JsonNode node;
public JsonPathValueImpl(JsonNode node) {
this.node = node;
}
@Override
public String asText() {
if (node == null || node.isMissingNode()) {
return null;
}
return node.asText();
}
@Override
public Integer asInt() {
if (node == null || node.isMissingNode()) {
return null;
}
return node.asInt();
}
@Override
public Long asLong() {
if (node == null || node.isMissingNode()) {
return null;
}
return node.asLong();
}
@Override
public Boolean asBoolean() {
if (node == null || node.isMissingNode()) {
return null;
}
return node.asBoolean();
}
@Override
public boolean isMissing() {
return node == null || node.isMissingNode();
}
}
}

View File

@ -0,0 +1,19 @@
package cz.moneta.test.harness.support.messaging.kafka;
/**
* Enum representing the content type of a received message.
*/
public enum MessageContentType {
/**
* JSON content - parsed with Jackson ObjectMapper
*/
JSON,
/**
* XML content - can be parsed with XPath or Jackson XmlMapper
*/
XML,
/**
* Raw text content - not parsed, returned as-is
*/
RAW_TEXT
}

View File

@ -0,0 +1,139 @@
package cz.moneta.test.harness.support.messaging.kafka;
import net.javacrumbs.jsonunit.assertj.JsonAssert;
/**
* Interface representing the response from a message receive operation.
* Provides fluent assertion methods for testing received messages.
*/
public interface MessageResponse {
/**
* Asserts that a field has the expected value.
*
* @param path JSON path or XPath
* @param value expected value as string
* @return this for chaining
*/
MessageResponse andAssertFieldValue(String path, String value);
/**
* Asserts that a field is present.
*
* @param path JSON path or XPath
* @return this for chaining
*/
MessageResponse andAssertPresent(String path);
/**
* Asserts that a field is not present.
*
* @param path JSON path or XPath
* @return this for chaining
*/
MessageResponse andAssertNotPresent(String path);
/**
* Asserts that a header has the expected value.
*
* @param headerName name of the header
* @param value expected value
* @return this for chaining
*/
MessageResponse andAssertHeaderValue(String headerName, String value);
/**
* Asserts that the body contains a substring.
* Useful for EBCDIC/UTF-8 raw text messages.
*
* @param substring expected substring
* @return this for chaining
*/
MessageResponse andAssertBodyContains(String substring);
/**
* Returns AssertJ JsonAssert for complex assertions.
*
* @return JsonAssert instance
*/
JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ();
/**
* Extracts a value from the message body.
*
* @param path JSON path or XPath
* @return JsonPathValue wrapper for further conversion
*/
JsonPathValue extract(String path);
/**
* Deserializes the message body to a Java object.
*
* @param type the target type
* @param <T> the target type
* @return deserialized object
*/
<T> T mapTo(Class<T> type);
/**
* Gets the underlying received message.
*
* @return the received message
*/
ReceivedMessage getMessage();
/**
* Gets the message body as string.
*
* @return body string
*/
String getBody();
/**
* Gets a header value.
*
* @param name header name
* @return header value or null
*/
String getHeader(String name);
/**
* Interface for extracted path values.
*/
interface JsonPathValue {
/**
* Converts to string.
*
* @return string value
*/
String asText();
/**
* Converts to integer.
*
* @return integer value
*/
Integer asInt();
/**
* Converts to long.
*
* @return long value
*/
Long asLong();
/**
* Converts to boolean.
*
* @return boolean value
*/
Boolean asBoolean();
/**
* Checks if value is missing/null.
*
* @return true if value is missing
*/
boolean isMissing();
}
}

View File

@ -0,0 +1,365 @@
package cz.moneta.test.harness.support.messaging.kafka;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import net.javacrumbs.jsonunit.assertj.JsonAssert;
/**
* Represents a received message from a messaging system (Kafka or IBM MQ).
* Provides unified API for extracting data regardless of the source system.
*/
public class ReceivedMessage {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final XmlMapper XML_MAPPER = new XmlMapper();
private static final Pattern ARRAY_NODE_PATTERN = Pattern.compile("(.*?)\\[([0-9]*?)\\]");
private final String body;
private final MessageContentType contentType;
private final Map<String, String> headers;
private final long timestamp;
private final String source;
private final String key;
private ReceivedMessage(Builder builder) {
this.body = builder.body;
this.contentType = builder.contentType;
this.headers = builder.headers != null ? new HashMap<>(builder.headers) : new HashMap<>();
this.timestamp = builder.timestamp;
this.source = builder.source;
this.key = builder.key;
}
/**
* Creates a new builder for ReceivedMessage.
*/
public static Builder builder() {
return new Builder();
}
/**
* Extracts value using JSON path (dot/bracket notation).
* For XML content, automatically converts to XPath evaluation.
*
* @param path JSON path (e.g., "items[0].sku") or XPath for XML
* @return JsonNode representing the extracted value
*/
public JsonNode extractJson(String path) {
if (body == null) {
return null;
}
if (contentType == MessageContentType.XML) {
// For XML, try XPath first, then fall back to JSON path
try {
return extractAsXml(path);
} catch (Exception e) {
// Fall through to JSON parsing
}
}
try {
JsonNode rootNode = JSON_MAPPER.readTree(body);
return extractNode(path, rootNode);
} catch (Exception e) {
throw new RuntimeException("Failed to extract JSON path '" + path + "' from body: " + e.getMessage(), e);
}
}
/**
* Extracts value using XPath expression (for XML messages).
*
* @param xpath XPath expression (e.g., "/response/balance")
* @return String value of the extracted node
*/
public String extractXml(String xpath) {
if (body == null) {
return null;
}
try {
Document doc = XML_MAPPER.readTree(body).isMissingNode() ? null : XML_MAPPER.readTree(body).isObject()
? parseXmlToDocument(body)
: parseXmlToDocument(body);
if (doc == null) {
doc = parseXmlToDocument(body);
}
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(xpath, doc, XPathConstants.NODESET);
if (nodes.getLength() == 0) {
return null;
}
return nodes.item(0).getTextContent();
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate XPath '" + xpath + "' on body: " + e.getMessage(), e);
}
}
/**
* Universal extract method - auto-detects content type and evaluates expression accordingly.
*
* @param expression JSON path for JSON content, XPath for XML content
* @return String value of the extracted node
*/
public String extract(String expression) {
if (body == null) {
return null;
}
return switch (contentType) {
case JSON -> extractJson(expression).asText();
case XML -> extractXml(expression);
case RAW_TEXT -> body;
};
}
/**
* Deserializes the message body to a Java object.
*
* @param type the target type
* @param <T> the target type
* @return deserialized object
*/
public <T> T mapTo(Class<T> type) {
if (body == null) {
return null;
}
try {
if (contentType == MessageContentType.JSON) {
return JSON_MAPPER.readValue(body, type);
} else if (contentType == MessageContentType.XML) {
return XML_MAPPER.readValue(body, type);
} else {
throw new IllegalStateException("Cannot deserialize RAW_TEXT to " + type.getName());
}
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize body to " + type.getName() + ": " + e.getMessage(), e);
}
}
/**
* Deserializes the message body to a list of objects.
*
* @param type the element type
* @param <T> the element type
* @return list of deserialized objects
*/
public <T> List<T> mapToList(Class<T> type) {
if (body == null) {
return Collections.emptyList();
}
try {
if (contentType == MessageContentType.JSON) {
return JSON_MAPPER.readValue(body, new TypeReference<List<T>>() {});
} else {
throw new IllegalStateException("Cannot deserialize to list for content type " + contentType);
}
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize body to List<" + type.getName() + ">: " + e.getMessage(), e);
}
}
/**
* Gets the message body as string.
*/
public String getBody() {
return body;
}
/**
* Gets the message key (Kafka) or null (IBM MQ).
*/
public String getKey() {
return key;
}
/**
* Gets header value by name (Kafka header or JMS property).
*/
public String getHeader(String name) {
return headers.get(name);
}
/**
* Gets all headers.
*/
public Map<String, String> getHeaders() {
return Collections.unmodifiableMap(headers);
}
/**
* Gets the message timestamp.
*/
public long getTimestamp() {
return timestamp;
}
/**
* Gets the source (topic name for Kafka, queue name for IBM MQ).
*/
public String getSource() {
return source;
}
/**
* Gets the content type.
*/
public MessageContentType getContentType() {
return contentType;
}
/**
* Creates JsonAssert for AssertJ-style assertions on the message body.
*/
public JsonAssert.ConfigurableJsonAssert andAssertWithAssertJ() {
if (body == null) {
throw new IllegalStateException("Cannot assert on null body");
}
return net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson(body);
}
private JsonNode extractNode(String path, JsonNode rootNode) {
if (StringUtils.isBlank(path)) {
return rootNode;
}
return Arrays.stream(path.split("\\."))
.filter(StringUtils::isNotEmpty)
.reduce(rootNode,
(r, p) -> {
Matcher matcher = ARRAY_NODE_PATTERN.matcher(p);
if (matcher.find()) {
return r.path(matcher.group(1)).path(Integer.valueOf(matcher.group(2)));
} else {
return r.path(p);
}
},
(j1, j2) -> j1);
}
private JsonNode extractAsXml(String path) throws Exception {
// Try XPath first
try {
String value = extractXml(path);
if (value != null) {
return JSON_MAPPER.valueToTree(value);
}
} catch (Exception e) {
// Fall back to JSON path on XML body
}
return extractNode(path, XML_MAPPER.readTree(body));
}
private Document parseXmlToDocument(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(xml)));
}
/**
* Builder for ReceivedMessage.
*/
public static class Builder {
private String body;
private MessageContentType contentType = MessageContentType.JSON;
private Map<String, String> headers = new HashMap<>();
private long timestamp = System.currentTimeMillis();
private String source;
private String key;
public Builder body(String body) {
this.body = body;
return this;
}
public Builder contentType(MessageContentType contentType) {
this.contentType = contentType;
return this;
}
public Builder headers(Map<String, String> headers) {
this.headers = headers;
return this;
}
public Builder timestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder source(String source) {
this.source = source;
return this;
}
public Builder key(String key) {
this.key = key;
return this;
}
public ReceivedMessage build() {
return new ReceivedMessage(this);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ReceivedMessage that = (ReceivedMessage) o;
return timestamp == that.timestamp &&
Objects.equals(body, that.body) &&
contentType == that.contentType &&
Objects.equals(headers, that.headers) &&
Objects.equals(source, that.source) &&
Objects.equals(key, that.key);
}
@Override
public int hashCode() {
return Objects.hash(body, contentType, headers, timestamp, source, key);
}
@Override
public String toString() {
return "ReceivedMessage{" +
"body='" + body + '\'' +
", contentType=" + contentType +
", headers=" + headers +
", timestamp=" + timestamp +
", source='" + source + '\'' +
", key='" + key + '\'' +
'}';
}
}

View File

@ -14,7 +14,7 @@ public class Template {
}
public Template(String templateString) {
this.template = new ST(templateString, '$', '$');
this.template = new ST(templateString);
}
public Template set(String variable, String value) {

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
<fileset name="all" enabled="true" check-config-name="Google Checks" local="false">
<file-match-pattern match-pattern="." include-pattern="true"/>
</fileset>
</fileset-config>

View File

@ -1,95 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.moneta.test</groupId>
<artifactId>tests</artifactId>
<version>2.28-SNAPSHOT</version>
<properties>
<harness.version>7.55-SNAPSHOT</harness.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.platform.version>1.5.1</junit.platform.version>
<ibm.mq.version>9.4.5.0</ibm.mq.version>
</properties>
<dependencies>
<dependency>
<groupId>cz.moneta.test</groupId>
<artifactId>harness</artifactId>
<version>${harness.version}</version>
</dependency>
<dependency>
<groupId>com.ibm.mq</groupId>
<artifactId>com.ibm.mq.allclient</artifactId>
<version>${ibm.mq.version}</version>
</dependency>
<!-- StringTemplate -->
<dependency>
<groupId>org.antlr</groupId>
<artifactId>ST4</artifactId>
<version>4.3.4</version>
</dependency>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc_auth</artifactId>
<version>8.2.0.x86</version>
<type>dll</type>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>15.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>${junit.platform.version}</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console</artifactId>
<version>${junit.platform.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>Moneta Artifactory</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>cz.moneta.test</groupId>
<artifactId>tests</artifactId>
<version>2.29-SNAPSHOT</version>
<properties>
<harness.version>7.55-SNAPSHOT</harness.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.platform.version>1.5.1</junit.platform.version>
</properties>
<dependencies>
<dependency>
<groupId>cz.moneta.test</groupId>
<artifactId>harness</artifactId>
<version>${harness.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc_auth</artifactId>
<version>8.2.0.x86</version>
<type>dll</type>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>15.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>${junit.platform.version}</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console</artifactId>
<version>${junit.platform.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>Moneta Artifactory</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -109,37 +91,30 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
--add-opens
java.base/java.lang.invoke=ALL-UNNAMED
-Dhttp.proxyHost=wsa-aws.mbid.cz
-Dhttp.proxyPort=8008
-Dhttps.proxyHost=wsa-aws.mbid.cz
-Dhttps.proxyPort=8008
-Dhttp.nonProxyHosts="elasticclusterawscoord*|elasticclusterawsingest*|jenkinslivex*|cbltstx|vault|vault.svc.k8s.moneta-containers.net|selenium-hub.svc.k8s.moneta-containers.net|jira*|d000*|x000*|l000*|digdev*|r000|spii-live-significant|mbczvl1dl0ihat3.ux.mbid.cz|mbczvl1dl0ihet3.ux.mbid.cz|wso2-fve-gw.ux.mbid.cz|wso2eifve.lb.mbid.cz|wso2eippe.lb.mbid.cz|wso2-ppe-gw.ux.mbid.cz|mbczvl0bl0enin3.ux.mbid.cz|wso2-tst1-gw.ux.mbid.cz|wso2eitst1.lb.mbid.cz|wso2-edu-gw.ux.mbid.cz|mbczvl0bl0enin5.ux.mbid.cz|mbczvl1dl0enin6.ux.mbid.cz|wso2api01-wso2-02.ux.mbid.cz|api-szr.tst.moneta-containers.net|api-szr.ppe.moneta-containers.net|docker1|mbczvl1dl0mockt.ux.mbid.cz|api.tst.moneta-containers.net|api.ppe.moneta-containers.net"</argLine>
<includes>
<include>
cz.moneta.test.sandbox.demo.HarnessDemoTest.java</include>
</includes>
<trimStackTrace>false</trimStackTrace>
<useFile>false</useFile>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>--add-opens java.base/java.lang.invoke=ALL-UNNAMED -Dhttp.proxyHost=wsa-aws.mbid.cz -Dhttp.proxyPort=8008 -Dhttps.proxyHost=wsa-aws.mbid.cz -Dhttps.proxyPort=8008
-Dhttp.nonProxyHosts="elasticclusterawscoord*|elasticclusterawsingest*|jenkinslivex*|cbltstx|vault|vault.svc.k8s.moneta-containers.net|selenium-hub.svc.k8s.moneta-containers.net|jira*|d000*|x000*|l000*|digdev*|r000|spii-live-significant|mbczvl1dl0ihat3.ux.mbid.cz|mbczvl1dl0ihet3.ux.mbid.cz|wso2-fve-gw.ux.mbid.cz|wso2eifve.lb.mbid.cz|wso2eippe.lb.mbid.cz|wso2-ppe-gw.ux.mbid.cz|mbczvl0bl0enin3.ux.mbid.cz|wso2-tst1-gw.ux.mbid.cz|wso2eitst1.lb.mbid.cz|wso2-edu-gw.ux.mbid.cz|mbczvl0bl0enin5.ux.mbid.cz|mbczvl1dl0enin6.ux.mbid.cz|wso2api01-wso2-02.ux.mbid.cz|api-szr.tst.moneta-containers.net|api-szr.ppe.moneta-containers.net|docker1|mbczvl1dl0mockt.ux.mbid.cz|api.tst.moneta-containers.net|api.ppe.moneta-containers.net"</argLine>
<includes>
<include>cz.moneta.test.sandbox.demo.HarnessDemoTest.java</include>
</includes>
<trimStackTrace>false</trimStackTrace>
<useFile>false</useFile>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
@ -173,129 +148,122 @@
<name>MVN Repository</name>
<url>https://mvnrepository.com/artifact/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>plugins-release</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/plugins-release</url>
</pluginRepository>
<pluginRepository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>plugins-snapshot</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
</profile>
<profile>
<id>build.package</id>
<activation>
<property>
<name>build.package</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>
cz.moneta.test.testrunner.TestRunner</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>libs-release</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/libs-release</url>
</repository>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>libs-snapshot</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/libs-snapshot</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>plugins-release</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/plugins-release</url>
</pluginRepository>
<pluginRepository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>plugins-snapshot</name>
<url>
https://artifactory-aws.ux.mbid.cz/artifactory/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>plugins-release</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/plugins-release</url>
</pluginRepository>
<pluginRepository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>plugins-snapshot</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
</profile>
<profile>
<id>build.package</id>
<activation>
<property>
<name>build.package</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>cz.moneta.test.testrunner.TestRunner</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>libs-release</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/libs-release</url>
</repository>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>libs-snapshot</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/libs-snapshot</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>plugins-release</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/plugins-release</url>
</pluginRepository>
<pluginRepository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>snapshots</id>
<name>plugins-snapshot</name>
<url>https://artifactory-aws.ux.mbid.cz/artifactory/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</project>

View File

@ -7,7 +7,7 @@ import cz.moneta.test.dsl.auto.smartauto.setman.SmartAutoSetman;
import cz.moneta.test.dsl.broadcom.Broadcom;
import cz.moneta.test.dsl.brokerportal.BrokerPortal;
import cz.moneta.test.dsl.caapi.CaApiBuilder;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.CaGw;
import cz.moneta.test.dsl.cashman.Cashman;
import cz.moneta.test.dsl.cebia.Cebia;
import cz.moneta.test.dsl.demo.Spirits;
@ -23,9 +23,10 @@ import cz.moneta.test.dsl.greenscreen.GreenScreen;
import cz.moneta.test.dsl.hypos.Hypos;
import cz.moneta.test.dsl.ib.Ib;
import cz.moneta.test.dsl.ilods.Ilods;
import cz.moneta.test.dsl.imq.ImqFirstVision;
import cz.moneta.test.dsl.kafka.Kafka;
import cz.moneta.test.dsl.kasanova.Kasanova;
import cz.moneta.test.dsl.mobile.smartbanking.home.Sb;
import cz.moneta.test.dsl.messaging.Messaging;
import cz.moneta.test.dsl.monetaapiportal.MonetaApiPortal;
import cz.moneta.test.dsl.monetaportal.MonetaPortal;
import cz.moneta.test.dsl.mwf.IHub;
@ -111,8 +112,8 @@ public class Harness extends BaseStoreAccessor {
return new CaApiBuilder(this);
}
public CaGwBuilder withCaGw() {
return new CaGwBuilder(this);
public CaGw withCaGw() {
return new CaGw(this);
}
@Deprecated
@ -283,8 +284,12 @@ public class Harness extends BaseStoreAccessor {
return new Cashman(this);
}
public Messaging withMessaging() {
return new Messaging(this);
public ImqFirstVision withImqFirstVision() {
return new ImqFirstVision(this);
}
public Kafka withKafka() {
return new Kafka(this);
}
private void initGenerators() {

View File

@ -47,7 +47,7 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
String VEHICLE_REGISTRATION_DATE_CHOOSE_BUTTON = "//button//span[contains(text(), 'Vybrat')]";
String VEHICLE_FINANCE_PRODUCT = "(//DIV//p//span[contains(text(), 'Vyberte záznam ...')])[1]";
String VEHICLE_FINANCE_PRODUCT_CHOOSE_BUTTON_DEFAULT = "(//div/h3[contains(text(), '%s')])/..//following-sibling::div//button//span[contains(text(), 'Vybrat')][1]/..";
String VEHICLE_CAR_INSURANCE = "//span[contains(text(),'Povinné ručení')]/following-sibling::div/div[contains(text(),'Vyberte záznam ...')][1]";
String VEHICLE_CAR_INSURANCE = "//h4/following-sibling::div/div[contains(text(),'Vyberte záznam ...')][1]";
String VEHICLE_CAR_ACCIDENT_INSURANCE = "(//DIV//p//span[contains(text(), 'Vyberte záznam ...')])[1]";
String VEHICLE_CAR_BENEFITS = "(//DIV//p//span[contains(text(), 'Vyberte záznam ...')])[1]";
String VEHICLE_CAR_BENEFITS_OPTION_DEFAULT = "(//label//span[contains(text(), '%s')])[1]/..";
@ -75,6 +75,7 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
String VEHICLE_CAR_INSURANCE_VEHICLE_SECOND_FUEL_TYPE_CHOOSE = "//label[contains(.,'Palivo')]/../div/ul/li[contains(.,'%s')]";
String VEHICLE_CAR_INSURANCE_VEHICLE_SEARCH_NO_VIN = "//button[contains(.,'Vyhledat')]";
String VEHICLE_CAR_INSURANCE_VEHICLE_VOLUME_AGRO = "//label[contains(.,'Objem')]/following-sibling::div/div/input";
String VEHICLE_CAR_INSURANCE_VEHICLE_NUMBER_OF_SEATS = "//label[contains(.,'Počet sedadel')]/following-sibling::div/div/input";
String VEHICLE_CAR_INSURANCE_VEHICLE_POWER_AGRO = "//label[contains(.,'Výkon')]/following-sibling::div/div/input";
String VEHICLE_CAR_INSURANCE_VEHICLE_WEIGHT_AGRO = "//label[contains(.,'Hmotnost')]/following-sibling::div/div/input";
String VEHICLE_CAR_INSURANCE_VEHICLE_FUEL_AGRO = "//label[contains(.,'Palivo')]/following-sibling::div/button";
@ -84,7 +85,8 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
String VEHICLE_CAR_INSURANCE_INSURANCE_HAV_FIELD_CHOOSE_DEFAULT = "//h4[contains(.,'Havarijní pojištění')]/../../../../div[2]/div[2]/div/div/section/div/div/div/button";
String VEHICLE_CAR_INSURANCE_INSURANCE_POV_FIELD_DEFAULT = "//h4[contains(.,'Povinné ručení')]/../../../../div[2]/div[1]/div/div[1]/section/h4/button/span[2]";
String VEHICLE_CAR_INSURANCE_INSURANCE_POV_FIELD_CHOOSE_DEFAULT = "//h4[contains(.,'Povinné ručení')]/../../../../div[2]/div[1]/div/div/section/div/div/div/button";
String VEHICLE_CAR_INSURANCE_INSURANCE_CHOOSE = "//section[@data-testid='Tabs']/div[2]/div[3]/div/div[3]/div/div/button[contains(.,'Vybrat')]";
String VEHICLE_CAR_INSURANCE_INSURANCE_CHOOSE = "//section[@data-testid='Collapse']//button[contains(text(),'Vybrat')]";
String VEHICLE_CAR_INSURANCE_INSURANCE_CLOSE = "/html/body/aside[1]/div/div/div[2]/div/div/button[contains(text(),'Zavřít')]";
String CREATE_APPLICATION = "//span[contains(text(), 'Vytvořit žádost')]/parent::button";
String DIV_CLICK_BLOCKER = "//div[@class='_21KP5GbzHYNm19YwGmWy0r']";
String DIV_CLICK_BLOCKER_2 = "//div[@class='_3Lp0XIzdhx1ktbMqO92O8T']";
@ -251,8 +253,8 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
@TypeInto(value = VEHICLE_CAR_INSURANCE_CUSTOMER_NATIONALITY, clear = true)
NewCalculationPage typeVehicleInsuranceCustomerNationality(String nationality);
@Wait(VEHICLE_CAR_INSURANCE_CUSTOMER_NATIONALITY_FIELD)
@Click(value = VEHICLE_CAR_INSURANCE_CUSTOMER_NATIONALITY_FIELD)
@Wait(value = VEHICLE_CAR_INSURANCE_CUSTOMER_NATIONALITY_FIELD)
@Click(value = VEHICLE_CAR_INSURANCE_CUSTOMER_NATIONALITY_FIELD, jsClick = true)
NewCalculationPage clickVehicleInsuranceCustomerNationality();
@Wait(VEHICLE_CAR_INSURANCE_CUSTOMER_ADDRESS)
@ -271,9 +273,12 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
@TypeInto(value = VEHICLE_CAR_INSURANCE_VEHICLE_VOLUME_AGRO)
NewCalculationPage typeVehicleInsuranceVehicleVolume(String volume);
@Wait(VEHICLE_CAR_INSURANCE_VEHICLE_TAB)
@TypeInto(value = VEHICLE_CAR_INSURANCE_VEHICLE_NUMBER_OF_SEATS)
NewCalculationPage typeVehicleInsuranceVehicleNumberOfSeats(String numberOfSeats);
@Wait(VEHICLE_CAR_INSURANCE_VEHICLE_TAB)
@TypeInto(value = VEHICLE_CAR_INSURANCE_VEHICLE_POWER_AGRO)
@TypeInto(value = VEHICLE_CAR_INSURANCE_VEHICLE_POWER_AGRO, andWait = @Wait(explicitWaitSeconds = 2))
NewCalculationPage typeVehicleInsuranceVehiclePower(String power);
@Wait(VEHICLE_CAR_INSURANCE_VEHICLE_TAB)
@ -345,6 +350,10 @@ public interface NewCalculationPage extends SmartAutoFlow<NewCalculationPage>, S
@Click(value = VEHICLE_CAR_INSURANCE_INSURANCE_CHOOSE,jsClick = true)
NewCalculationPage clickVehicleInsuranceInsuranceChoose();
@Wait(value = VEHICLE_CAR_INSURANCE_INSURANCE_CLOSE, waitSecondsForElement = 30)
@Click(value = VEHICLE_CAR_INSURANCE_INSURANCE_CLOSE,jsClick = true)
NewCalculationPage clickVehicleInsuranceInsuranceClose();
@Wait(value = {DIV_CLICK_BLOCKER, DIV_CLICK_BLOCKER_2}, until = Until.GONE, waitSecondsForElement = 30)
@Wait(value = VEHICLE_CAR_ACCIDENT_INSURANCE, waitSecondsForElement = 30)
@Click(value = VEHICLE_CAR_ACCIDENT_INSURANCE, andWait = @Wait(value = CommonElements.MODAL, waitSecondsForElement = 30))

View File

@ -14,7 +14,7 @@ public interface OthersPage extends SmartAutoFlow<OthersPage> {
String ACCOUNT_NUMBER = "//input[@name='sign.bankContactC2C.accountNumber']";
String BANK_CODE = "//span[contains(text(), 'Kód banky')]/../../..//input";
String BUTTON = "//button[contains(text(), '%s')]";
String APPROVAL_MESSAGE = "//span[contains(text(), 'Zpráva pro schvalovatele')]/../../../textarea";
String APPROVAL_MESSAGE = "//h6[contains(text(), 'Zpráva pro schvalovatele')]/../../../div/div/span/textarea";
String VEHICLE_PAGE = "//span[contains(text(), 'Předmět')]";
String DUPLICATE_APPLICATION = "//span[contains(text(), 'Duplikovat')]";
String CALCULATION_NAME = "//input[@name='calculationName']";

View File

@ -13,7 +13,7 @@ import static cz.moneta.test.dsl.auto.smartauto.QueuePage.*;
waitSecondsForElement = 40)
public interface QueuePage extends SmartAutoFlow<QueuePage>, StoreAccessor, CalendarAccessor {
String HEADING_SEND_APPLICATIONS = "//span[contains(text(), 'Odeslané žádosti')]";
String HEADING_SEND_APPLICATIONS = "//h1[contains(text(), 'Odeslané žádosti')]";
String FIRST_APPLICATION = "//div/div/div/button/i";
String DATE_FROM = "//label[contains(., 'Datum od')]/following-sibling::div/button";
String DATE_TO = "//label[contains(., 'Datum do')]/following-sibling::div/button";

View File

@ -6,11 +6,11 @@ import cz.moneta.test.harness.support.web.*;
public interface SavedCalculationsPage extends SmartAutoFlow<SavedCalculationsPage>, StoreAccessor, CalendarAccessor {
String HEADING_SAVED_CALCULATIONS = "//h1/span[contains(text(), 'Kalkulace')]";
String FIRST_APPLICATION_DIV = "//h2/../div[1]/div/";
String CALCULATION_NAME = FIRST_APPLICATION_DIV + "div/div[1]";
String CALCULATION_STATUS = FIRST_APPLICATION_DIV + "/span/span[contains(text(), 'Rozpracovaná žádost')]";
String CALCULATION_DATE = FIRST_APPLICATION_DIV + "div/div[5]";
String HEADING_SAVED_CALCULATIONS = "//h1[contains(text(), 'Kalkulace')]";
String FIRST_APPLICATION_DIV = "/html/body/div[1]/div[1]/div/div/div[1]/div[2]/div[1]/div/div[1]";
String CALCULATION_NAME = FIRST_APPLICATION_DIV + "/div[2]/div[contains(text(), 'TestDuplikace')]";
String CALCULATION_STATUS = FIRST_APPLICATION_DIV + "//span[contains(text(), 'Rozpracovaná žádost')]";
String CALCULATION_DATE = FIRST_APPLICATION_DIV + "/div[3]/div[2]";
String DATE_FROM = "//label[contains(., 'Datum od')]/following-sibling::div/button";
String DATE_TO = "//label[contains(., 'Datum do')]/following-sibling::div/button";
String SEARCH_BUTTON = "//button[@testid='bbutton' and span[text()='Zobrazit']]";

View File

@ -31,7 +31,7 @@ public interface VehiclePage extends SmartAutoFlow<VehiclePage>, StoreAccessor {
String SEATS = "//input[@name='vehicle.specificCar.sittingPlaceNr']";
String FUEL = "(//span[contains(text(), 'Palivo')])[2]/../..//following-sibling::div//input[@value='%s']";
String MILEAGE = "//input[@name='vehicle.specificCar.travel']";
String IMPORT_VEHICLE = "//span[contains(text(), 'Vozidlo z dovozu')]/../../../div/div/span/button/span[contains(text(), '%s')]";
String IMPORT_VEHICLE = "//span[contains(text(), 'Vozidlo z dovozu')]/../../../div/div/div/span/button//span[contains(text(), '%s')]/..";
String DEALER_IS_OWNER = "//span[contains(text(), 'Dealer je majitelem vozidla')]/../../..//span[contains(text(), '%s')]/..";
String DESCRIPTION = "//textarea[@name='vehicle.specificCar.description']";
String OTHERS_PAGE = "//span[contains(text(), 'Ostatní')]";
@ -130,10 +130,10 @@ public interface VehiclePage extends SmartAutoFlow<VehiclePage>, StoreAccessor {
@TypeInto(value = DESCRIPTION, andWait = @Wait(value = LOADER_DIV, until = Until.GONE))
VehiclePage typeDescription(String description);
@Click(IMPORT_VEHICLE)
@Click(value = IMPORT_VEHICLE, jsClick = true)
VehiclePage clickImportVehicle(ImportVehicle importVehicle);
@Click(DEALER_IS_OWNER)
@Click(value = DEALER_IS_OWNER,jsClick = true)
VehiclePage clickDealerIsOwner(DealerIsOwner dealerIsOwner);
@Click(value = OTHERS_PAGE, jsClick = true)

View File

@ -23,8 +23,8 @@ public interface ForBillingPage extends SmartAutoFlow<ForBillingPage> {
String FROM_DATE_TEXT = "//span[contains(text(), 'Od data')]";
String TO_DATE_TEXT = "//span[contains(text(), 'Do data')]";
String COMMISSIONS_CREDIT_NOTES_TEXT = "//span[contains(text(), 'Provize/Dobropis')]";
String FILTER_TITLE = "//span[contains(text(), 'Filtr')]";
String BILLING_COMMISSIONS_TITLE = "//span[contains(text(), 'Provize k fakturaci')]";
String FILTER_TITLE = "//h6[contains(text(), 'Filtr')]";
String BILLING_COMMISSIONS_TITLE = "//h6[contains(text(), 'Fakturace')]";
String RESET_BUTTON = "//span[contains(text(), 'Resetovat')]";
String SEARCH_BUTTON = "//span[contains(text(), 'Vyhledat')]";
String INVOICE_BUTTON = "//span[contains(text(), 'Vyfakturovat')]";

View File

@ -25,7 +25,7 @@ public interface DashboardPage extends SmartAutoFlow<DashboardPage> {
String TOTAL_AVAILABLE_LIMIT_TEXT = "//span[contains(text(), 'Volný limit')]";
String FOR_PAYMENT_TEXT = "//span[contains(text(), 'K úhradě')]";
String OVERDUE_PAYMENT_TEXT = "//span[contains(text(), 'Z toho po splatnosti')]";
String PRODUCT_LIST_TITLE = "//span[contains(text(), 'Seznam produktů')]";
String PRODUCT_LIST_TITLE = "//h6[contains(text(), 'Produkty')]";
String ALL_CONTRACT_BUTTON = "//span[contains(text(), 'Všechny zakázky')]";
@CheckElementsPresent(value = {

View File

@ -25,6 +25,7 @@ public class CalculationData {
private Subsidy subsidy;
private ItemType itemType;
private int firstPayment;
private String numberOfSeats;
public CalculationData setCustomer(Customer customer) {
this.customer = customer;
@ -131,4 +132,8 @@ public class CalculationData {
return this;
}
public CalculationData setNumberOfSeats(String numberOfSeats) {
this.numberOfSeats = numberOfSeats;
return this;
}
}

View File

@ -0,0 +1,33 @@
package cz.moneta.test.dsl.cagw;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.api.ApiBuilder;
import cz.moneta.test.dsl.cagw.auth.AuthBuilder;
import cz.moneta.test.dsl.cagw.oauth2.Oauth2Builder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.RawRestRequest;
public class CaGw extends CaGwBuilder {
public CaGw(Harness harness) {
super(harness);
}
public ApiBuilder api() {
return new ApiBuilder(harness);
}
public Oauth2Builder oauth2() {
return new Oauth2Builder(harness);
}
public AuthBuilder auth() {
return new AuthBuilder(harness);
}
public RawRestRequest.Path prepareRequest() {
CaGwEndpoint endpoint = harness.getEndpoint(CaGwEndpoint.class);
return RawRestRequest.jsonBuilder(endpoint);
}
}

View File

@ -1,34 +1,20 @@
package cz.moneta.test.dsl.cagw;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.api.ApiBuilder;
import cz.moneta.test.dsl.cagw.auth.AuthBuilder;
import cz.moneta.test.dsl.cagw.oauth2.Oauth2Builder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class CaGwBuilder {
private final Harness harness;
protected final Harness harness;
public CaGwBuilder(Harness harness) {
protected CaGwBuilder(Harness harness) {
this.harness = harness;
}
public ApiBuilder api() {
return new ApiBuilder(harness);
}
public Oauth2Builder oauth2() {
return new Oauth2Builder(harness);
}
public AuthBuilder auth() {
return new AuthBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
protected <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}
}

View File

@ -1,19 +1,16 @@
package cz.moneta.test.dsl.cagw.api;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.CblBuilder;
import cz.moneta.test.dsl.cagw.api.v2.V2Builder;
import cz.moneta.test.dsl.cagw.api.v4.V4Builder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.dsl.cagw.api.v1.V1Builder;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class ApiBuilder {
private final Harness harness;
public class ApiBuilder extends CaGwBuilder {
public ApiBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public CblBuilder cbl() {
@ -32,7 +29,4 @@ public class ApiBuilder {
return new V1Builder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.PsdBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class CblBuilder {
private final Harness harness;
public class CblBuilder extends CaGwBuilder {
public CblBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public PsdBuilder psd() {
return new PsdBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,17 +1,14 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.AispBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.PispBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class PsdBuilder {
private final Harness harness;
public class PsdBuilder extends CaGwBuilder {
public PsdBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public PispBuilder pisp() {
@ -22,7 +19,4 @@ public class PsdBuilder {
return new AispBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.aisp;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.MyBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class AispBuilder {
private final Harness harness;
public class AispBuilder extends CaGwBuilder {
public AispBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public MyBuilder my() {
return new MyBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,19 +1,16 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.Account;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.AccountsBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.AccountsWithParams;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class MyBuilder {
private final Harness harness;
public class MyBuilder extends CaGwBuilder {
public MyBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public AccountsBuilder accounts() {
@ -28,7 +25,4 @@ public class MyBuilder {
return getBuilder(AccountsWithParams.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.id.IdBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class AccountsBuilder {
private final Harness harness;
public class AccountsBuilder extends CaGwBuilder {
public AccountsBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public IdBuilder id() {
return new IdBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,18 +1,15 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.id;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.id.balance.Balance;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.id.transactions.Transactions;
import cz.moneta.test.dsl.cagw.api.cbl.psd.aisp.my.accounts.id.transactions.TransactionsWithParams;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class IdBuilder {
private final Harness harness;
public class IdBuilder extends CaGwBuilder {
public IdBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public Balance prepareBalanceRequest() {
@ -26,8 +23,5 @@ public class IdBuilder {
public TransactionsWithParams prepareTransactionsWithParamsRequest() {
return getBuilder(TransactionsWithParams.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.pisp;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.MyBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class PispBuilder {
private final Harness harness;
public class PispBuilder extends CaGwBuilder {
public PispBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public MyBuilder my() {
return new MyBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,17 +1,14 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments.Payment;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments.PaymentsBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class MyBuilder {
private final Harness harness;
public class MyBuilder extends CaGwBuilder {
public MyBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public PaymentsBuilder payments() {
@ -22,7 +19,4 @@ public class MyBuilder {
return getBuilder(Payment.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments.paymentid.PaymentIdBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class PaymentsBuilder {
private final Harness harness;
public class PaymentsBuilder extends CaGwBuilder {
public PaymentsBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public PaymentIdBuilder paymentId() {
return new PaymentIdBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments.paymentid;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.cbl.psd.pisp.my.payments.paymentid.status.Status;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class PaymentIdBuilder {
private final Harness harness;
public class PaymentIdBuilder extends CaGwBuilder {
public PaymentIdBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public Status prepareStatusRequest() {
return getBuilder(Status.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v1;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.ExternalBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class V1Builder {
private final Harness harness;
public class V1Builder extends CaGwBuilder {
public V1Builder(Harness harness) {
this.harness = harness;
super(harness);
}
public ExternalBuilder external() {
return new ExternalBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,17 +1,14 @@
package cz.moneta.test.dsl.cagw.api.v1.external;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.product.ProductBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.service.ServiceBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class ExternalBuilder {
private final Harness harness;
public class ExternalBuilder extends CaGwBuilder {
public ExternalBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public ProductBuilder product() {
@ -22,7 +19,4 @@ public class ExternalBuilder {
return new ServiceBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v1.external.product;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.product.cloan.CloanBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class ProductBuilder {
private final Harness harness;
public class ProductBuilder extends CaGwBuilder {
public ProductBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public CloanBuilder cbl() {
return new CloanBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v1.external.product.cloan;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.product.cloan.application.ApplicationBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class CloanBuilder {
private final Harness harness;
public class CloanBuilder extends CaGwBuilder {
public CloanBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public ApplicationBuilder application() {
return new ApplicationBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,22 +1,16 @@
package cz.moneta.test.dsl.cagw.api.v1.external.product.cloan.application;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
public class ApplicationBuilder {
private final Harness harness;
public class ApplicationBuilder extends CaGwBuilder {
public ApplicationBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public Application prepareApplicationRequest() {
return getBuilder(Application.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v1.external.service;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.service.voicebot.VoicebotBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class ServiceBuilder {
private final Harness harness;
public class ServiceBuilder extends CaGwBuilder {
public ServiceBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public VoicebotBuilder voicebot() {
return new VoicebotBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v1.external.service.voicebot;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v1.external.service.voicebot.voicepassworddetail.VoicePasswordDetail;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class VoicebotBuilder {
private final Harness harness;
public class VoicebotBuilder extends CaGwBuilder {
public VoicebotBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public VoicePasswordDetail prepareVoicePasswordDetailApi() {
public VoicePasswordDetail prepareVoicePasswordDetail() {
return getBuilder(VoicePasswordDetail.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v2;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v2.external.ExternalBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class V2Builder {
private final Harness harness;
public class V2Builder extends CaGwBuilder {
public V2Builder(Harness harness) {
this.harness = harness;
super(harness);
}
public ExternalBuilder external() {
return new ExternalBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v2.external;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v2.external.client.ClientBuilder;
public class ExternalBuilder {
private final Harness harness;
public class ExternalBuilder extends CaGwBuilder {
public ExternalBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public ClientBuilder client() {
return new ClientBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v2.external.client;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v2.external.client.kyc.KycBuilder;
public class ClientBuilder {
private final Harness harness;
public class ClientBuilder extends CaGwBuilder {
public ClientBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public KycBuilder kyc() {
return new KycBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v2.external.client.kyc;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v2.external.client.kyc.businesscard.BusinessCardBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class KycBuilder {
private final Harness harness;
public class KycBuilder extends CaGwBuilder {
public KycBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public BusinessCardBuilder bussinesCard() {
return new BusinessCardBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,22 +1,16 @@
package cz.moneta.test.dsl.cagw.api.v2.external.client.kyc.businesscard;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
public class BusinessCardBuilder {
private final Harness harness;
public class BusinessCardBuilder extends CaGwBuilder {
public BusinessCardBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public BusinessCard prepareBusinessCardRequest() {
return getBuilder(BusinessCard.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.api.v4;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.api.v4.token.TokenBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class V4Builder {
private final Harness harness;
public class V4Builder extends CaGwBuilder {
public V4Builder(Harness harness) {
this.harness = harness;
super(harness);
}
public TokenBuilder token() {
return new TokenBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,16 @@
package cz.moneta.test.dsl.cagw.api.v4.token;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
public class TokenBuilder {
private final Harness harness;
public class TokenBuilder extends CaGwBuilder {
public TokenBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public ApiV4Token prepareTokenRequest() {
return getBuilder(ApiV4Token.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.auth;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.auth.oauth.OauthBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class AuthBuilder {
private final Harness harness;
public class AuthBuilder extends CaGwBuilder {
public AuthBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public OauthBuilder oauth() {
return new OauthBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.auth.oauth;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.auth.oauth.v2.V2Builder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class OauthBuilder {
private final Harness harness;
public class OauthBuilder extends CaGwBuilder {
public OauthBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public V2Builder v2() {
return new V2Builder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.auth.oauth.v2;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.auth.oauth.v2.token.TokenBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class V2Builder {
private final Harness harness;
public class V2Builder extends CaGwBuilder {
public V2Builder(Harness harness) {
this.harness = harness;
super(harness);
}
public TokenBuilder token() {
return new TokenBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,24 +1,17 @@
package cz.moneta.test.dsl.cagw.auth.oauth.v2.token;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
public class TokenBuilder {
private final Harness harness;
public class TokenBuilder extends CaGwBuilder {
public TokenBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
public OauthToken prepareOauth2V2TokenRequest() {
return getBuilder(OauthToken.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,23 +1,17 @@
package cz.moneta.test.dsl.cagw.oauth2;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
import cz.moneta.test.dsl.cagw.oauth2.token.TokenBuilder;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
public class Oauth2Builder {
private final Harness harness;
public class Oauth2Builder extends CaGwBuilder {
public Oauth2Builder(Harness harness) {
this.harness = harness;
super(harness);
}
public TokenBuilder token() {
return new TokenBuilder(harness);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,24 +1,18 @@
package cz.moneta.test.dsl.cagw.oauth2.token;
import cz.moneta.test.dsl.Harness;
import cz.moneta.test.harness.endpoints.cagw.CaGwEndpoint;
import cz.moneta.test.harness.support.rest.Builders;
import cz.moneta.test.harness.support.rest.RestRequest;
import cz.moneta.test.dsl.cagw.CaGwBuilder;
public class TokenBuilder {
private final Harness harness;
public class TokenBuilder extends CaGwBuilder {
public TokenBuilder(Harness harness) {
this.harness = harness;
super(harness);
}
@Deprecated
public Oauth2Token prepareOauth2TokenRequest() {
return getBuilder(Oauth2Token.class);
}
private <RESP, B extends RestRequest<B, ?, RESP>> B getBuilder(Class<B> builderClass) {
return Builders.newRestBuilder(builderClass, CaGwEndpoint.class, harness);
}
}

View File

@ -1,5 +1,6 @@
package cz.moneta.test.dsl.exevido.components;
import cz.moneta.test.dsl.exevido.pages.inheritance.FoldersPage;
import cz.moneta.test.dsl.exevido.pages.IncomingMessagesPage;
import cz.moneta.test.dsl.exevido.pages.OutgoingMessagesPage;
import cz.moneta.test.harness.support.web.Click;
@ -9,6 +10,7 @@ import cz.moneta.test.harness.support.web.Wait;
import static cz.moneta.test.dsl.exevido.components.ExevidoPanels.LOADER_DIV;
import static cz.moneta.test.dsl.exevido.components.LeftMenu.PAGE_SA_SMART_MENU;
import static cz.moneta.test.dsl.exevido.pages.inheritance.FoldersPage.PAGE_LABEL_XPATH;
@Wait(PAGE_SA_SMART_MENU)
@Wait(value = LOADER_DIV, until = Until.GONE)
@ -22,6 +24,7 @@ public interface LeftMenu<T> {
String CATEGORY_INHERITANCE_MMB_XPATH = "//li[@id='Dědictví - MMB']";
String SUBCATEGORY_WITHOUT_FOLDER_XPATH = CATEGORY_INHERITANCE_MMB_XPATH + "//a[contains(., 'Bez spisu')]";
String SUBCATEGORY_FOR_PROCESSING_XPATH = CATEGORY_INHERITANCE_MMB_XPATH + "//a[contains(., 'Ke zpracování')]";
String SUBCATEGORY_FOLDERS_XPATH = CATEGORY_INHERITANCE_MMB_XPATH + "//a[contains(., 'Spisy')]";
String FOR_PROCESSING_D_REQUEST_MMB_XPATH = CATEGORY_INHERITANCE_MMB_XPATH + "//a[contains(., 'D - Žádost [M] - MMB')]";
String REFRESH_COUNTERS_BUTTON = "refresh_counters";
@ -37,7 +40,7 @@ public interface LeftMenu<T> {
@Click(SUBCATEGORY_ALL_OUTGOING_MESSAGES_A)
OutgoingMessagesPage clickAllOutgoingMessages();
@Click(value = CATEGORY_INHERITANCE_MMB_XPATH, by = Lookup.XPATH, andWait = @Wait(value = SUBCATEGORY_WITHOUT_FOLDER_XPATH, by = Lookup.XPATH ,until = Until.VISIBLE))
@Click(value = CATEGORY_INHERITANCE_MMB_XPATH, by = Lookup.XPATH, andWait = @Wait(value = SUBCATEGORY_WITHOUT_FOLDER_XPATH, by = Lookup.XPATH, until = Until.VISIBLE))
@Click(value = SUBCATEGORY_WITHOUT_FOLDER_XPATH, by = Lookup.XPATH, andWait = @Wait(value = LOADER_DIV, until = Until.GONE))
IncomingMessagesPage clickInheritanceMmbWithoutFolder();
@ -45,6 +48,10 @@ public interface LeftMenu<T> {
@Click(value = SUBCATEGORY_FOR_PROCESSING_XPATH, by = Lookup.XPATH, andWait = @Wait(value = FOR_PROCESSING_D_REQUEST_MMB_XPATH, by = Lookup.XPATH, until = Until.VISIBLE))
LeftMenu clickInheritanceMmbForProcessing();
@Click(value = CATEGORY_INHERITANCE_MMB_XPATH, by = Lookup.XPATH, andWait = @Wait(value = SUBCATEGORY_FOR_PROCESSING_XPATH, by = Lookup.XPATH, until = Until.VISIBLE))
@Click(value = SUBCATEGORY_FOLDERS_XPATH, by = Lookup.XPATH, andWait = @Wait(value = PAGE_LABEL_XPATH, by = Lookup.XPATH, until = Until.VISIBLE))
FoldersPage clickInheritanceMmbFolders();
@Click(value = FOR_PROCESSING_D_REQUEST_MMB_XPATH, by = Lookup.XPATH, andWait = @Wait(value = LOADER_DIV, until = Until.GONE))
IncomingMessagesPage clickRequestMmb();

View File

@ -36,6 +36,11 @@ public interface DetailIncomingMessagePage extends ExevidoWebFlow<DetailIncoming
String DOWNLOAD_ZFO = "//button[normalize-space()='Stáhnout ZFO']";
String MESSAGE_RETURN_P = "//p[text()='Zpráva byla vrácena na podatelnu k dalšímu zpracování']";
String INTERNAL_REFERENCE_NUMBER = "//accordion-group[.//b[text()='Spisy']]//table//tr/td[2][normalize-space()='%s']";
String ADD_MESSAGE_FOLDER_BUTTON = "add_message_folder_button";
String ADD_FOLDER_ISSUE_NUMBER_INPUT = "add_folder_issue_number";
String SAVE_FOLDER_ISSUE_NUMBER = "//td//i[@title='Přiřadit ke spisu']";
String REMOVE_FOLDER_ISSUE_NUMBER = "//tr[td[2][normalize-space(text())='%s']]//td[4]//i[@title='Odebrat spis']";
String CONFIRM_BUTTON = "confirm_button";
String MESSAGE_ID_SPAN = "message_id";
@Click(ASSIGN_BUTTON)
@ -119,6 +124,25 @@ public interface DetailIncomingMessagePage extends ExevidoWebFlow<DetailIncoming
@CheckElementPresent(value = INTERNAL_REFERENCE_NUMBER, by = Lookup.XPATH, isStringDynamicXpath = true)
DetailIncomingMessagePage checkInternalReferenceNumber(String referenceNumber);
@CustomAction
default DetailIncomingMessagePage clickAddMessageFolder() {
ExevidoEndpoint endpoint = getEndpoint(ExevidoEndpoint.class);
endpoint.moveToElement(ADD_MESSAGE_FOLDER_BUTTON);
endpoint.click(() -> ADD_MESSAGE_FOLDER_BUTTON);
return null;
}
@TypeInto(value = ADD_FOLDER_ISSUE_NUMBER_INPUT, clear = true, andWait = @Wait(value = ExevidoPanels.LOADER_DIV, until = Until.GONE))
@KeyPress(Key.ENTER)
DetailIncomingMessagePage fillFolderIssueNumber(String folderIssueNumber);
@Click(value = SAVE_FOLDER_ISSUE_NUMBER, by = Lookup.XPATH, andWait = @Wait(value = ExevidoPanels.LOADER_DIV, until = Until.GONE))
DetailIncomingMessagePage saveFolderIssueNumber();
@Click(value = REMOVE_FOLDER_ISSUE_NUMBER, by = Lookup.XPATH, isStringDynamicXpath = true)
@Click(CONFIRM_BUTTON)
DetailIncomingMessagePage removeFolderIssueNumber(String folderIssueNumber);
@CheckElementContent(value = MESSAGE_ID_SPAN)
NewIncomingMessagePage checkMessageId(String id);
}

View File

@ -49,6 +49,15 @@ public interface IncomingMessagesPage extends ExevidoWebFlow<IncomingMessagesPag
return null;
}
@CustomAction
default IncomingMessagesPage verifyIncomingMessageRowNotPresent() {
ExevidoEndpoint endpoint = getEndpoint(ExevidoEndpoint.class);
if (endpoint.isElementPresent(INCOMING_MESSAGE_ROW, Lookup.ID)) {
throw new AssertionError("Incoming message row should NOT be present.");
}
return null;
}
@TypeInto(value = DATE_RANGE_ACCEPTANCE_TIME, clear = true)
@KeyPress(value = Key.ENTER, andWait = @Wait(value = ExevidoPanels.LOADER_DIV, until = Until.GONE))
IncomingMessagesPage searchByDateRange(String date);

View File

@ -42,8 +42,8 @@ public interface NewOutgoingMessagePage extends ExevidoWebFlow<NewOutgoingMessag
String MESSAGE_IS_SENDING_DIV = "message_identification";
String MESSAGE_SUCCESSFULLY_SEND_LOG_XPATH = "//span[contains(text(), 'byla odeslána')]";
String MESSAGE_SEND_FAILURE_LOG_XPATH = "//span[contains(text(), 'selhání odeslání zprávy')]";
String ATTACHMENT_PATH = "regression/exevido/testExevido.txt";
String SECOND_ATTACHMENT_PATH = "regression/exevido/testExevido2.txt";
String ATTACHMENT_PATH = "regression/exevido/web/testExevido.txt";
String SECOND_ATTACHMENT_PATH = "regression/exevido/web/testExevido2.txt";
String MESSAGE_ID_SPAN = "message_id";
String MULTIPLE_RECIPIENTS_SPAN = "//label[@for='multiple_recipients']";
String ADD_RECIPIENT_TO_LIST_BUTTON = "add_to_list_button";

View File

@ -0,0 +1,25 @@
package cz.moneta.test.dsl.exevido.pages.inheritance;
import cz.moneta.test.dsl.exevido.ExevidoWebFlow;
import cz.moneta.test.dsl.exevido.pages.DetailIncomingMessagePage;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.exevido.ExevidoEndpoint;
import cz.moneta.test.harness.support.web.*;
import static cz.moneta.test.dsl.exevido.pages.inheritance.DetailFolderPage.PAGE_LABEL_XPATH;
@Wait(value = PAGE_LABEL_XPATH, by = Lookup.XPATH)
public interface DetailFolderPage extends ExevidoWebFlow<DetailFolderPage>, StoreAccessor {
String PAGE_LABEL_XPATH = "//b[contains(text(),'Spis:')]";
String OUTGOING_MESSAGES_LABEL_XPATH = "//a[contains(text(),'Odchozí zprávy')]";
String ROW_BY_MESSAGE_ID = "//tr[contains(@class,'clickable-row')]//td[2]/div[normalize-space()='%s']/ancestor::tr";
@CustomAction
default DetailIncomingMessagePage openMessageById(String messageId) {
ExevidoEndpoint endpoint = getEndpoint(ExevidoEndpoint.class);
endpoint.moveToElement(OUTGOING_MESSAGES_LABEL_XPATH, Lookup.XPATH);
endpoint.click(() -> String.format(ROW_BY_MESSAGE_ID, messageId), Lookup.XPATH);
return null;
}
}

View File

@ -0,0 +1,26 @@
package cz.moneta.test.dsl.exevido.pages.inheritance;
import cz.moneta.test.dsl.exevido.ExevidoWebFlow;
import cz.moneta.test.dsl.exevido.components.ExevidoPanels;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.exevido.ExevidoEndpoint;
import cz.moneta.test.harness.support.web.*;
import static cz.moneta.test.dsl.exevido.pages.inheritance.FoldersPage.PAGE_LABEL_XPATH;
@Wait(value = PAGE_LABEL_XPATH, by = Lookup.XPATH)
public interface FoldersPage extends ExevidoWebFlow<FoldersPage>, StoreAccessor {
String PAGE_LABEL_XPATH = "//b[contains(text(),'Spisy')]";
String PRODUCT_OWNER_NAME_INPUT = "text_product_owner_name";
String FOLDER_MESSAGE_ROW = "folder_0_row";
@TypeInto(value = PRODUCT_OWNER_NAME_INPUT)
@KeyPress(value = Key.ENTER, andWait = @Wait(value = ExevidoPanels.LOADER_DIV, until = Until.GONE))
FoldersPage fillProductOwnerName(String name);
@Click(value = FOLDER_MESSAGE_ROW, andWait = @Wait(value = ExevidoPanels.LOADER_DIV, until = Until.GONE))
DetailFolderPage clickFolderRow();
}

View File

@ -97,7 +97,7 @@ public class HyposContractPrepare {
.realtyPriceCurrent(3500000)
.realtyType(RealtyType.FLAT.getValue())
.pledgeType(PledgeType.PROPERTY_HOUSING.getValue())
.contractRelation(ContranctRelation.PLEDGE_WITH_CURRENT_REALTY.getValue())
.contractRelation(ContractRelation.PLEDGE_WITH_CURRENT_REALTY.getValue())
.collateralType(CollateralType.FLAT.getValue())
.appraiserCompany(AppraiserCompany.MONETA_SUPERVISION.getValue())
.build();

View File

@ -13,4 +13,4 @@ public enum AppraiserCompany {
public String getValue() {
return appraiserCompany;
}
}
}

View File

@ -1,6 +1,8 @@
package cz.moneta.test.dsl.hypos.enums;
public enum CadasterRequestType {
import cz.moneta.test.harness.support.web.SelectByValue;
public enum CadasterRequestType implements SelectByValue {
PART_OWNER_LIST("Část. výpis LV"),
CADASTER_MAP("Kat. mapa"),
OWNER_LIST("List vlastnictví"),
@ -12,6 +14,7 @@ public enum CadasterRequestType {
this.cadasterRequestType = cadasterRequestType;
}
@Override
public String getValue() {
return cadasterRequestType;
}

View File

@ -12,4 +12,4 @@ public enum CollateralCode {
public String getValue() {
return collateralCode;
}
}
}

View File

@ -1,6 +1,8 @@
package cz.moneta.test.dsl.hypos.enums;
public enum CollectableFee {
import cz.moneta.test.harness.support.web.SelectByValue;
public enum CollectableFee implements SelectByValue {
APPENDIX_CLIENT_INITIATIVE("DODATEK - Z PODNĚTU KLIENTA"),
FEE_DOWNLOAD_LV_KM("Poplatek za stažení LV/KM"),
OTHER_BANK_INFORMATION("OSTATNÍ - BANKOVNÍ INFORMACE");
@ -11,6 +13,7 @@ public enum CollectableFee {
this.collectableFee = collectableFee;
}
@Override
public String getValue() {
return collectableFee;
}

Some files were not shown because too many files have changed in this diff Show More