Skip to content

Commit

Permalink
fix(citrusframework#1058): Introduce default fallback text message va…
Browse files Browse the repository at this point in the history
…lidator

- Avoid throwing no message validator found exception in favor of performing default fallback text equals message validation
- Provides better context of test failure with message payload mismatch instead of raising generic no proper message validator found error
  • Loading branch information
christophd committed Nov 15, 2023
1 parent ba23d17 commit dc7cd92
Show file tree
Hide file tree
Showing 58 changed files with 305 additions and 773 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2006-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.validation;

import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.ValidationException;
import org.citrusframework.message.Message;
import org.citrusframework.validation.context.ValidationContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Default message validator implementation performing text equals on given message payloads.
* Validator auto converts message payloads into a String representation in order to perform text equals validation.
* Both received and control message should have textual message payloads.
* By default, the validator ignores leading and trailing whitespaces and normalizes the line endings before the validation.
* Usually this validator implementation is used as a fallback option when no other matching validator implementation could be found.
*
* @author Christoph Deppisch
*/
public class DefaultTextEqualsMessageValidator extends DefaultMessageValidator {

private static final Logger logger = LoggerFactory.getLogger(DefaultTextEqualsMessageValidator.class);

private boolean normalizeLineEndings = true;
private boolean trim = true;

@Override
public void validateMessage(Message receivedMessage, Message controlMessage,
TestContext context, ValidationContext validationContext) {
if (controlMessage == null || controlMessage.getPayload() == null || controlMessage.getPayload(String.class).isEmpty()) {
logger.debug("Skip message payload validation as no control message was defined");
return;
}

logger.debug("Start to verify message payload ...");

String controlPayload = controlMessage.getPayload(String.class);
String receivedPayload = receivedMessage.getPayload(String.class);

if (trim) {
controlPayload = controlPayload.trim();
receivedPayload = receivedPayload.trim();
}

if (normalizeLineEndings) {
controlPayload = normalizeLineEndings(controlPayload);
receivedPayload = normalizeLineEndings(receivedPayload);
}

if (!receivedPayload.equals(controlPayload)) {
throw new ValidationException("Validation failed - message payload not equal!");
}
}

public DefaultTextEqualsMessageValidator normalizeLineEndings() {
this.normalizeLineEndings = true;
return this;
}

public DefaultTextEqualsMessageValidator enableTrim() {
this.trim = true;
return this;
}

/**
* Normalize the text by replacing line endings by a linux representation.
*/
private static String normalizeLineEndings(String text) {
return text.replace("\r\n", "\n").replace("
", "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ public class MessageValidatorRegistry {
/** Default message header validator - gets looked up via resource path */
private MessageValidator<? extends ValidationContext> defaultMessageHeaderValidator;

/** Default empty message validator */
/** Default message validators used as a fallback option */
private final DefaultEmptyMessageValidator defaultEmptyMessageValidator = new DefaultEmptyMessageValidator();
private final DefaultTextEqualsMessageValidator defaultTextEqualsMessageValidator = new DefaultTextEqualsMessageValidator();

/**
* Finds matching message validators for this message type.
Expand All @@ -69,6 +70,18 @@ public class MessageValidatorRegistry {
* @return the list of matching message validators.
*/
public List<MessageValidator<? extends ValidationContext>> findMessageValidators(String messageType, Message message) {
return findMessageValidators(messageType, message, false);
}

/**
* Finds matching message validators for this message type.
*
* @param messageType the message type
* @param message the message object
* @param mustFindValidator is default fallback validator allowed
* @return the list of matching message validators.
*/
public List<MessageValidator<? extends ValidationContext>> findMessageValidators(String messageType, Message message, boolean mustFindValidator) {
List<MessageValidator<? extends ValidationContext>> matchingValidators = new ArrayList<>();

for (MessageValidator<? extends ValidationContext> validator : messageValidators.values()) {
Expand Down Expand Up @@ -99,8 +112,13 @@ public List<MessageValidator<? extends ValidationContext>> findMessageValidators
}

if (isEmptyOrDefault(matchingValidators)) {
logger.warn(String.format("Unable to find proper message validator. Message type is '%s' and message payload is '%s'", messageType, message.getPayload(String.class)));
throw new CitrusRuntimeException("Failed to find proper message validator for message");
if (mustFindValidator) {
logger.warn(String.format("Unable to find proper message validator. Message type is '%s' and message payload is '%s'", messageType, message.getPayload(String.class)));
throw new CitrusRuntimeException("Failed to find proper message validator for message");
}

logger.warn("Unable to find proper message validator - fallback to default text equals validation.");
matchingValidators.add(defaultTextEqualsMessageValidator);
}

if (logger.isDebugEnabled()) {
Expand Down Expand Up @@ -184,7 +202,7 @@ public MessageValidator<? extends ValidationContext> getMessageValidator(String
}

/**
* Adds given message validator and allows overwrite of existing message validators in registry with same name.
* Adds given message validator and allows to overwrite of existing message validators in registry with same name.
* @param name
* @param messageValidator
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.citrusframework.messaging.Consumer;
import org.citrusframework.messaging.SelectiveConsumer;
import org.citrusframework.spi.ReferenceResolverAware;
import org.citrusframework.util.StringUtils;
import org.citrusframework.validation.DefaultMessageHeaderValidator;
import org.citrusframework.validation.HeaderValidator;
import org.citrusframework.validation.MessageValidator;
Expand All @@ -60,7 +61,6 @@
import org.citrusframework.variable.dictionary.DataDictionary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.citrusframework.util.StringUtils;

/**
* This action receives messages from a service destination. Action uses a {@link org.citrusframework.endpoint.Endpoint}
Expand Down Expand Up @@ -251,21 +251,13 @@ protected void validateMessage(Message message, TestContext context) {
}
}
} else {
boolean mustFindValidator = validationContexts.stream()
.anyMatch(item -> JsonPathMessageValidationContext.class.isAssignableFrom(item.getClass()) ||
XpathMessageValidationContext.class.isAssignableFrom(item.getClass()) ||
ScriptValidationContext.class.isAssignableFrom(item.getClass()));

List<MessageValidator<? extends ValidationContext>> validators =
context.getMessageValidatorRegistry().findMessageValidators(messageType, message);

if (validators.isEmpty()) {
if (controlMessage.getPayload() instanceof String &&
StringUtils.hasText(controlMessage.getPayload(String.class))) {
throw new CitrusRuntimeException(String.format("Unable to find proper message validator for message type '%s' and validation contexts '%s'", messageType, validationContexts));
} else if (validationContexts.stream().anyMatch(item -> JsonPathMessageValidationContext.class.isAssignableFrom(item.getClass())
|| XpathMessageValidationContext.class.isAssignableFrom(item.getClass())
|| ScriptValidationContext.class.isAssignableFrom(item.getClass()))) {
throw new CitrusRuntimeException(String.format("Unable to find proper message validator for message type '%s' and validation contexts '%s'", messageType, validationContexts));
} else {
logger.warn(String.format("Unable to find proper message validator for message type '%s' and validation contexts '%s'", messageType, validationContexts));
}
}
context.getMessageValidatorRegistry().findMessageValidators(messageType, message, mustFindValidator);

for (MessageValidator<? extends ValidationContext> messageValidator : validators) {
messageValidator.validateMessage(message, controlMessage, context, validationContexts);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import org.citrusframework.spi.ReferenceResolver;
import org.citrusframework.spi.Resource;
import org.citrusframework.validation.AbstractValidationProcessor;
import org.citrusframework.validation.TextEqualsMessageValidator;
import org.citrusframework.validation.DefaultTextEqualsMessageValidator;
import org.citrusframework.validation.builder.DefaultMessageBuilder;
import org.citrusframework.validation.builder.StaticMessageBuilder;
import org.citrusframework.validation.context.HeaderValidationContext;
Expand Down Expand Up @@ -84,7 +84,7 @@ public class ReceiveMessageActionBuilderTest extends UnitTestSupport {
@BeforeMethod
public void prepareTestContext() {
MockitoAnnotations.openMocks(this);
context.getMessageValidatorRegistry().addMessageValidator("default", new TextEqualsMessageValidator());
context.getMessageValidatorRegistry().addMessageValidator("default", new DefaultTextEqualsMessageValidator());
}

@Test
Expand Down Expand Up @@ -654,7 +654,7 @@ public void testReceiveBuilderWithValidator() {
when(configuration.getTimeout()).thenReturn(100L);
when(messageEndpoint.getActor()).thenReturn(null);
when(messageConsumer.receive(any(TestContext.class), anyLong())).thenReturn(new DefaultMessage("TestMessage").setHeader("operation", "sayHello"));
final TextEqualsMessageValidator validator = new TextEqualsMessageValidator();
final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator();

DefaultTestCaseRunner runner = new DefaultTestCaseRunner(context);
runner.run(receive(messageEndpoint)
Expand Down Expand Up @@ -684,7 +684,7 @@ public void testReceiveBuilderWithValidator() {

@Test
public void testReceiveBuilderWithValidatorName() {
final TextEqualsMessageValidator validator = new TextEqualsMessageValidator();
final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator();

reset(referenceResolver, messageEndpoint, messageConsumer, configuration);
when(messageEndpoint.createConsumer()).thenReturn(messageConsumer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2006-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.validation;

import java.nio.charset.StandardCharsets;

import org.citrusframework.UnitTestSupport;
import org.citrusframework.exceptions.ValidationException;
import org.citrusframework.message.DefaultMessage;
import org.citrusframework.message.Message;
import org.citrusframework.validation.context.DefaultValidationContext;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
* @author Christoph Deppisch
*/
public class DefaultTextEqualsMessageValidatorTest extends UnitTestSupport {

private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator();
private final DefaultValidationContext validationContext = new DefaultValidationContext();

@Test(dataProvider = "successTests")
public void testValidate(Object received, Object control) {
Message receivedMessage = new DefaultMessage(received);
Message controlMessage = new DefaultMessage(control);

validator.validateMessage(receivedMessage, controlMessage, context, validationContext);
}

@Test(dataProvider = "errorTests", expectedExceptions = ValidationException.class)
public void testValidateError(Object received, Object control) throws Exception {
Message receivedMessage = new DefaultMessage(received);
Message controlMessage = new DefaultMessage(control);

validator.validateMessage(receivedMessage, controlMessage, context, validationContext);
}

@DataProvider
private Object[][] successTests() {
return new Object[][] {
new Object[]{ null, null },
new Object[]{ "", null },
new Object[]{ null, "" },
new Object[]{ "Hello World!", "Hello World!" },
new Object[]{ "Hello World! ", "Hello World!" },
new Object[]{ "Hello World!", "Hello World! " },
new Object[]{ "Hello World!\n", "Hello World!" },
new Object[]{ "Hello World!\n", "Hello World!\n" },
new Object[]{ "\nHello World!", "\nHello World!" },
new Object[]{ "Hello\nWorld!\n", "Hello\nWorld!\n" },
new Object[]{ "Hello\r\nWorld!\r\n", "Hello\nWorld!\n" },
new Object[]{ "Hello World!", null }, // empty control message
new Object[]{ "Hello World!", "" }, // no control message
new Object[]{ "Hello World!".getBytes(StandardCharsets.UTF_8), "" } // no control message
};
}

@DataProvider
private Object[][] errorTests() {
return new Object[][] {
new Object[]{ null, "Hello World!" },
new Object[]{ "", "Hello World!" },
new Object[]{ "Hello World!", "Hello World!" },
new Object[]{ "Hello World!", "Hello World!" },
new Object[]{ "Hello\nWorld!", "Hello World!" },
new Object[]{ "Hello World!", "Hello\nWorld!" },
new Object[]{ "Hello!", "Hi!" },
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public class MessageValidatorRegistryTest {
@Mock
private SchemaValidator<?> xmlSchemaValidator;

private MessageValidatorRegistry messageValidatorRegistry = new MessageValidatorRegistry();
private final MessageValidatorRegistry messageValidatorRegistry = new MessageValidatorRegistry();

@BeforeClass
public void setupMocks() {
Expand Down Expand Up @@ -116,7 +116,6 @@ public void setupMocks() {

messageValidatorRegistry.addSchemaValidator("jsonSchemaValidator", jsonSchemaValidator);
messageValidatorRegistry.addSchemaValidator("xmlSchemaValidator", xmlSchemaValidator);

}

@Test
Expand Down Expand Up @@ -318,12 +317,18 @@ public void shouldAddDefaultEmptyMessagePayloadValidator() {
Assert.assertEquals(matchingValidators.get(1).getClass(), DefaultMessageHeaderValidator.class);

try {
messageValidatorRegistry.findMessageValidators(MessageType.JSON.name(), new DefaultMessage("Hello"));
messageValidatorRegistry.findMessageValidators(MessageType.JSON.name(), new DefaultMessage("Hello"), true);
Assert.fail("Missing exception due to no proper message validator found");
} catch (CitrusRuntimeException e) {
Assert.assertEquals(e.getMessage(), "Failed to find proper message validator for message");
}

matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.JSON.name(), new DefaultMessage("Hello"));
Assert.assertNotNull(matchingValidators);
Assert.assertEquals(matchingValidators.size(), 2L);
Assert.assertEquals(matchingValidators.get(0).getClass(), DefaultMessageHeaderValidator.class);
Assert.assertEquals(matchingValidators.get(1).getClass(), DefaultTextEqualsMessageValidator.class);

messageValidatorRegistry.addMessageValidator("plainTextMessageValidator", plainTextMessageValidator);

matchingValidators = messageValidatorRegistry.findMessageValidators(MessageType.JSON.name(), new DefaultMessage("Hello"));
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.citrusframework.http.message.HttpMessageBuilder;
import org.citrusframework.http.message.HttpMessageHeaders;
import org.citrusframework.http.server.HttpServer;
import org.citrusframework.http.validation.TextEqualsMessageValidator;
import org.citrusframework.message.DefaultMessage;
import org.citrusframework.message.DefaultMessageQueue;
import org.citrusframework.message.Message;
Expand All @@ -48,6 +47,7 @@
import org.citrusframework.spi.BindToRegistry;
import org.citrusframework.util.SocketUtils;
import org.citrusframework.validation.DefaultMessageHeaderValidator;
import org.citrusframework.validation.DefaultTextEqualsMessageValidator;
import org.citrusframework.validation.DelegatingPayloadVariableExtractor;
import org.citrusframework.validation.context.DefaultValidationContext;
import org.citrusframework.validation.context.HeaderValidationContext;
Expand Down Expand Up @@ -75,7 +75,7 @@ public class HttpClientTest extends AbstractGroovyActionDslTest {
private final DefaultMessageHeaderValidator headerValidator = new DefaultMessageHeaderValidator();

@BindToRegistry
private final TextEqualsMessageValidator validator = new TextEqualsMessageValidator().enableTrim();
private final DefaultTextEqualsMessageValidator validator = new DefaultTextEqualsMessageValidator().enableTrim();

private final int port = SocketUtils.findAvailableTcpPort(8080);
private final String uri = "http://localhost:" + port + "/test";
Expand Down
Loading

0 comments on commit dc7cd92

Please sign in to comment.