diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java new file mode 100644 index 0000000..8dcef81 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ImqRequest.java @@ -0,0 +1,516 @@ +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. + *

+ * Usage: + *

{@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");
+ * }
+ *

+ */ +@SuppressWarnings("unchecked") +public final class ImqRequest { + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + 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). + */ + PayloadPhase asJson(); + + /** + * Set message format to XML. + */ + PayloadPhase asXml(); + + /** + * Set message format to EBCDIC (IBM-870). + */ + PayloadPhase asEbcdic(); + + /** + * Set message format to UTF-8 (CCSID 1208). + */ + PayloadPhase 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); + + /** + * 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 filter); + + /** + * Browse messages from queue (non-destructive). + */ + List browse(int maxMessages); + + /** + * Browse messages from queue with selector (non-destructive). + */ + List browse(String selector, int maxMessages); + } + + /** + * 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 fields = new HashMap<>(); + private List> arrayAppends = new ArrayList<>(); + private Predicate 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 PayloadPhase asJson() { + this.format = MqMessageFormat.JSON; + return this; + } + + @Override + public PayloadPhase asXml() { + this.format = MqMessageFormat.XML; + return this; + } + + @Override + public PayloadPhase asEbcdic() { + this.format = MqMessageFormat.EBCDIC_870; + return this; + } + + @Override + public PayloadPhase 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 void send() { + String finalPayload = buildPayload(); + Map properties = new HashMap<>(); + + if (logicalQueue != null) { + properties.put("LogicalQueue", logicalQueue.name()); + } + if (selector != null && !selector.isBlank()) { + properties.put("Selector", selector); + } + + endpoint.send(getQueueName(), finalPayload, format, properties); + } + + @Override + public ReceivePhase withSelector(String selector) { + this.selector = selector; + return this; + } + + @Override + public AwaitingPhase receiveWhere(Predicate filter) { + this.filter = filter; + return this; + } + + @Override + public List browse(int maxMessages) { + return browse(selector, maxMessages); + } + + @Override + public List 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 json = JSON_MAPPER.readValue(payload, Map.class); + + for (Map.Entry entry : fields.entrySet()) { + setField(json, entry.getKey(), entry.getValue()); + } + + for (Map.Entry 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 json, String path, Object value) { + if (StringUtils.isBlank(path)) { + json.put(path, value); + return; + } + + String[] parts = path.split("\\."); + Map current = json; + + for (int i = 0; i < parts.length - 1; i++) { + String part = parts[i]; + if (!current.containsKey(part)) { + current.put(part, new HashMap()); + } + current = (Map) current.get(part); + } + + current.put(parts[parts.length - 1], value); + } + + private void appendToArray(Map json, String path, Object value) { + String[] parts = path.split("\\."); + Map current = json; + + for (int i = 0; i < parts.length - 1; i++) { + String part = parts[i]; + if (!current.containsKey(part)) { + current.put(part, new ArrayList>()); + } + current = (Map) current.get(part); + } + + List> array = (List>) current.get(parts[parts.length - 1]); + if (array == null) { + array = new ArrayList<>(); + current.put(parts[parts.length - 1], array); + } + array.add((Map) 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 mapTo(Class 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); + } + } +} diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/JsonPathValue.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/JsonPathValue.java new file mode 100644 index 0000000..d2b4543 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/JsonPathValue.java @@ -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; + } +} diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageContentType.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageContentType.java new file mode 100644 index 0000000..0f1d249 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageContentType.java @@ -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 +} diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageResponse.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageResponse.java new file mode 100644 index 0000000..8a060f8 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MessageResponse.java @@ -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 the target type + * @return the deserialized object + */ + T mapTo(Class 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); +} diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MqMessageFormat.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MqMessageFormat.java new file mode 100644 index 0000000..c2da694 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/MqMessageFormat.java @@ -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 +} diff --git a/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ReceivedMessage.java b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ReceivedMessage.java new file mode 100644 index 0000000..d6e1cc8 --- /dev/null +++ b/test-harness/src/main/java/cz/moneta/test/harness/support/messaging/ReceivedMessage.java @@ -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). + *

+ * 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. + *

+ */ +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 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. + *

+ * Supports paths like: + *

    + *
  • {@code "field"} - top-level field
  • + *
  • {@code "parent.child"} - nested field
  • + *
  • {@code "items[0]"} - array element
  • + *
  • {@code "items[0].name"} - nested field in array element
  • + *
+ *

+ * + * @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 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. + *

+ * For JSON content: uses Jackson ObjectMapper.readValue + * For XML content: uses Jackson XmlMapper + *

+ * + * @param type the target class + * @param the target type + * @return the deserialized object + */ + public T mapTo(Class 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 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 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(); + } +} diff --git a/tests/pom.xml b/tests/pom.xml index ef36a59..f401309 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ tests 2.29-SNAPSHOT - 7.55.820 + 7.55-SNAPSHOT UTF-8 1.5.1 @@ -15,6 +15,7 @@ cz.moneta.test harness ${harness.version} + compile org.slf4j diff --git a/tests/src/main/java/cz/moneta/test/dsl/Harness.java b/tests/src/main/java/cz/moneta/test/dsl/Harness.java index 33e570a..8e331c2 100644 --- a/tests/src/main/java/cz/moneta/test/dsl/Harness.java +++ b/tests/src/main/java/cz/moneta/test/dsl/Harness.java @@ -23,6 +23,7 @@ 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.kasanova.Kasanova; import cz.moneta.test.dsl.mobile.smartbanking.home.Sb; import cz.moneta.test.dsl.monetaapiportal.MonetaApiPortal; @@ -282,6 +283,10 @@ public class Harness extends BaseStoreAccessor { return new Cashman(this); } + public ImqFirstVision withImqFirstVision() { + return new ImqFirstVision(this); + } + private void initGenerators() { addGenerator(RC, new RcGenerator()); addGenerator(FIRST_NAME, new FirstNameGenerator()); diff --git a/tests/src/main/java/cz/moneta/test/dsl/imq/ImqFirstVision.java b/tests/src/main/java/cz/moneta/test/dsl/imq/ImqFirstVision.java new file mode 100644 index 0000000..47c157d --- /dev/null +++ b/tests/src/main/java/cz/moneta/test/dsl/imq/ImqFirstVision.java @@ -0,0 +1,187 @@ +package cz.moneta.test.dsl.imq; + +import cz.moneta.test.dsl.Harness; +import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionEndpoint; +import cz.moneta.test.harness.endpoints.imq.ImqFirstVisionQueue; +import cz.moneta.test.harness.messaging.ReceivedMessage; +import cz.moneta.test.harness.messaging.exception.MessagingTimeoutException; +import cz.moneta.test.harness.support.messaging.ImqRequest; +import cz.moneta.test.harness.messaging.MqMessageFormat; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +/** + * IBM MQ First Vision DSL. + *

+ * Usage example: + *

{@code
+ * harness.withImqFirstVision()
+ *     .toQueue(ImqFirstVisionQueue.PAYMENT_NOTIFICATIONS)
+ *     .withPayload("{\"paymentId\": \"PAY-123\"}")
+ *     .send();
+ *
+ * harness.withImqFirstVision()
+ *     .fromQueue(ImqFirstVisionQueue.PAYMENT_NOTIFICATIONS)
+ *     .receiveWhere(msg -> msg.extract("paymentId").equals("PAY-123"))
+ *     .withTimeout(10, TimeUnit.SECONDS)
+ *     .andAssertFieldValue("result", "OK");
+ * }
+ *

+ */ +public class ImqFirstVision { + + private final Harness harness; + + public ImqFirstVision(Harness harness) { + this.harness = harness; + } + + /** + * Start building a message to send to a queue. + */ + public ImqRequest.QueuePhase toQueue(ImqFirstVisionQueue queue) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + return ImqRequest.toQueue(endpoint, queue); + } + + /** + * Start building a message to receive from a queue. + */ + public ImqRequest.ReceivePhase fromQueue(ImqFirstVisionQueue queue) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + return ImqRequest.fromQueue(endpoint, queue); + } + + /** + * Send a JSON message to a queue. + */ + public void sendToQueue(ImqFirstVisionQueue queue, String payload) { + sendToQueue(queue, payload, MqMessageFormat.JSON, null); + } + + /** + * Send a message to a queue with specified format. + */ + public void sendToQueue(ImqFirstVisionQueue queue, String payload, MqMessageFormat format) { + sendToQueue(queue, payload, format, null); + } + + /** + * Send a message to a queue with specified format and properties. + */ + public void sendToQueue(ImqFirstVisionQueue queue, String payload, MqMessageFormat format, + Map properties) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + endpoint.send(queue, payload, format, properties); + } + + /** + * Send a JSON message to a queue by physical name. + */ + public void sendToQueue(String queueName, String payload) { + sendToQueue(queueName, payload, MqMessageFormat.JSON, null); + } + + /** + * Send a message to a queue by physical name with specified format. + */ + public void sendToQueue(String queueName, String payload, MqMessageFormat format) { + sendToQueue(queueName, payload, format, null); + } + + /** + * Send a message to a queue by physical name with specified format and properties. + */ + public void sendToQueue(String queueName, String payload, MqMessageFormat format, + Map properties) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + endpoint.send(queueName, payload, format, properties); + } + + /** + * Receive a message from a queue with timeout. + */ + public ReceivedMessage receiveFromQueue(ImqFirstVisionQueue queue, Duration timeout) { + return receiveFromQueue(queue, null, MqMessageFormat.JSON, timeout); + } + + /** + * Receive a message from a queue with selector and timeout. + */ + public ReceivedMessage receiveFromQueue(ImqFirstVisionQueue queue, String selector, Duration timeout) { + return receiveFromQueue(queue, selector, MqMessageFormat.JSON, timeout); + } + + /** + * Receive a message from a queue with format and timeout. + */ + public ReceivedMessage receiveFromQueue(ImqFirstVisionQueue queue, MqMessageFormat format, Duration timeout) { + return receiveFromQueue(queue, null, format, timeout); + } + + /** + * Receive a message from a queue with selector, format and timeout. + */ + public ReceivedMessage receiveFromQueue(ImqFirstVisionQueue queue, String selector, + MqMessageFormat format, Duration timeout) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + return endpoint.receive(queue, selector, format, timeout); + } + + /** + * Receive a message matching predicate from a queue. + */ + public ReceivedMessage receiveFromQueue(ImqFirstVisionQueue queue, Predicate filter, + Duration timeout) { + long startTime = System.currentTimeMillis(); + long timeoutMs = timeout.toMillis(); + + while (System.currentTimeMillis() - startTime < timeoutMs) { + try { + ReceivedMessage message = receiveFromQueue(queue, MqMessageFormat.JSON, Duration.ofSeconds(1)); + if (filter.test(message)) { + return message; + } + } catch (MessagingTimeoutException e) { + // Continue polling + } + } + + throw new MessagingTimeoutException( + "No message matching filter found on queue '" + queue.getConfigKey() + + "' within " + timeout.toMillis() + "ms"); + } + + /** + * Browse messages from a queue (non-destructive). + */ + public List browseQueue(ImqFirstVisionQueue queue, int maxMessages) { + return browseQueue(queue, null, MqMessageFormat.JSON, maxMessages); + } + + /** + * Browse messages from a queue with selector (non-destructive). + */ + public List browseQueue(ImqFirstVisionQueue queue, String selector, int maxMessages) { + return browseQueue(queue, selector, MqMessageFormat.JSON, maxMessages); + } + + /** + * Browse messages from a queue with format and max count (non-destructive). + */ + public List browseQueue(ImqFirstVisionQueue queue, MqMessageFormat format, int maxMessages) { + return browseQueue(queue, null, format, maxMessages); + } + + /** + * Browse messages from a queue with selector, format and max count (non-destructive). + */ + public List browseQueue(ImqFirstVisionQueue queue, String selector, + MqMessageFormat format, int maxMessages) { + ImqFirstVisionEndpoint endpoint = harness.getEndpoint(ImqFirstVisionEndpoint.class); + return endpoint.browse(queue, selector, format, maxMessages); + } +} diff --git a/tests/src/test/resources/envs/ppe b/tests/src/test/resources/envs/ppe index 71320c9..9de0894 100644 --- a/tests/src/test/resources/envs/ppe +++ b/tests/src/test/resources/envs/ppe @@ -76,4 +76,21 @@ endpoints.szr-mock-api.url=https://api-szr.ppe.moneta-containers.net endpoints.forte.url=https://forteppe.ux.mbid.cz/portal/home/ #Exevido -endpoints.exevido.url=https://exevido.ppe.moneta-containers.net/#/auth/login \ No newline at end of file +endpoints.exevido.url=https://exevido.ppe.moneta-containers.net/#/auth/login + +#IBM MQ First Vision +endpoints.imq-first-vision.connection-name-list=mq9multipe5x(1414),mq9multipe6x(1414) +endpoints.imq-first-vision.channel=CLIENT.CHANNEL +endpoints.imq-first-vision.queue-manager=MVSW2PPE +endpoints.imq-first-vision.ssl-cipher-suite=TLS_RSA_WITH_AES_256_CBC_SHA256 + +#IBM MQ queues +endpoints.imq-first-vision.payment-notifications.queue=MVSW2PPE.DELIVERY.NOTIFICATION +endpoints.imq-first-vision.payment-request.queue=MVSW2PPE.DELIVERY.REQUEST +endpoints.imq-first-vision.mf-requests.queue=MVSW2PPE.MF.REQUESTS +endpoints.imq-first-vision.mf-responses.queue=MVSW2PPE.MF.RESPONSES +endpoints.imq-first-vision.mf-ebcdic.queue=MVSW2PPE.MF.EBCDIC +endpoints.imq-first-vision.mf-utf8.queue=MVSW2PPE.MF.UTF8 + +#Vault path for IBM MQ credentials +vault.imq-first-vision.secrets.path=/kv/autotesty/ppe/imq-first-vision \ No newline at end of file diff --git a/tests/src/test/resources/envs/tst1 b/tests/src/test/resources/envs/tst1 index dbedecb..c8d351f 100644 --- a/tests/src/test/resources/envs/tst1 +++ b/tests/src/test/resources/envs/tst1 @@ -97,4 +97,21 @@ endpoints.szr-mock-api.url=https://api-szr.tst.moneta-containers.net endpoints.exevido.url=https://exevido.tst.moneta-containers.net/#/auth/login #Cashman -endpoints.cashman.url=https://cashmantst.mbid.cz/ \ No newline at end of file +endpoints.cashman.url=https://cashmantst.mbid.cz/ + +#IBM MQ First Vision +endpoints.imq-first-vision.connection-name-list=mq9multitst5x(1414),mq9multitst6x(1414) +endpoints.imq-first-vision.channel=CLIENT.CHANNEL +endpoints.imq-first-vision.queue-manager=MVSW2TST3 +endpoints.imq-first-vision.ssl-cipher-suite=TLS_RSA_WITH_AES_256_CBC_SHA256 + +#IBM MQ queues +endpoints.imq-first-vision.payment-notifications.queue=MVSW2TST3.DELIVERY.NOTIFICATION +endpoints.imq-first-vision.payment-request.queue=MVSW2TST3.DELIVERY.REQUEST +endpoints.imq-first-vision.mf-requests.queue=MVSW2TST3.MF.REQUESTS +endpoints.imq-first-vision.mf-responses.queue=MVSW2TST3.MF.RESPONSES +endpoints.imq-first-vision.mf-ebcdic.queue=MVSW2TST3.MF.EBCDIC +endpoints.imq-first-vision.mf-utf8.queue=MVSW2TST3.MF.UTF8 + +#Vault path for IBM MQ credentials +vault.imq-first-vision.secrets.path=/kv/autotesty/tst1/imq-first-vision \ No newline at end of file