/* * Copyright (c) 2014-2016, Inversoft Inc., All Rights Reserved * * 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.primeframework.mvc.test; import javax.servlet.http.Cookie; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.primeframework.mock.servlet.MockHttpServletRequest; import org.primeframework.mock.servlet.MockHttpServletResponse; import org.primeframework.mvc.action.ActionInvocation; import org.primeframework.mvc.action.ActionInvocationStore; import org.primeframework.mvc.action.ActionMapper; import org.primeframework.mvc.message.FieldMessage; import org.primeframework.mvc.message.Message; import org.primeframework.mvc.message.MessageStore; import org.primeframework.mvc.message.MessageType; import org.primeframework.mvc.message.l10n.MessageProvider; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.inject.Injector; import static java.util.Arrays.asList; /** * Result of a request to the {@link org.primeframework.mvc.test.RequestSimulator}. * * @author Brian Pontarelli */ public class RequestResult { public final String body; public final Injector injector; public final String redirect; public final MockHttpServletRequest request; public final MockHttpServletResponse response; public final int statusCode; public RequestResult(MockHttpServletRequest request, MockHttpServletResponse response, Injector injector) { this.request = request; this.response = response; this.injector = injector; this.body = response.getStream().toString(); this.redirect = response.getRedirect(); this.statusCode = response.getCode(); } /** * Compares two JSON objects to ensure they are equal. This is done by converting the JSON objects to Maps, Lists, and primitives and then * comparing them. The error is output so that IntelliJ can diff the two JSON objects in order to output the results. * * @param objectMapper The Jackson ObjectMapper used to convert the JSON strings to Maps. * @param actual The actual JSON. * @param expected The expected JSON. * @throws IOException If the ObjectMapper fails. */ public static void assertJSONEquals(ObjectMapper objectMapper, String actual, String expected) throws IOException { Object response = objectMapper.readValue(actual, Object.class); Object file = objectMapper.readValue(expected, Object.class); if (!response.equals(file)) { objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); String bodyString = objectMapper.writeValueAsString(response); String fileString = objectMapper.writeValueAsString(file); throw new AssertionError("The body doesn't match the expected JSON output. expected [" + fileString + "] but found [" + bodyString + "]"); } } /** * Verifies that the body equals the given string. * * @param string The string to compare against the body. * @return This. */ public RequestResult assertBody(String string) { if (!body.equals(string)) { throw new AssertionError("Body didn't match [" + string + "]\nRedirect: [" + redirect + "]\nBody:\n" + body); } return this; } /** * Verifies that the body contains all of the given Strings. * * @param strings The strings to check. * @return This. */ public RequestResult assertBodyContains(String... strings) { for (String string : strings) { if (!body.contains(string)) { throw new AssertionError("Body didn't contain [" + string + "]\nRedirect: [" + redirect + "]\nBody:\n" + body); } } return this; } /** * Verifies that the body contains the messages from the given key and optionally provided replacement values. This * uses the MessageProvider for the current * test URI and the given keys to look up the messages. * * @param key The key. * @param values The replacement values. * @return This. */ public RequestResult assertBodyContainsMessagesFromKey(String key, Object... values) { MessageProvider messageProvider = get(MessageProvider.class); ActionInvocationStore actionInvocationStore = get(ActionInvocationStore.class); ActionMapper actionMapper = get(ActionMapper.class); // Using the ActionMapper so that URL segments are properly handled and the correct URL is used for message lookups. ActionInvocation actionInvocation = actionMapper.map(null, request.getRequestURI(), true); actionInvocationStore.setCurrent(actionInvocation); String message = messageProvider.getMessage(key, values); if (!body.contains(message)) { throw new AssertionError("Body didn't contain [" + message + "] for the key [" + key + "]\nRedirect: [" + redirect + "]\nBody:\n" + body); } return this; } /** * Verifies that the body contains the messages from the given keys. This uses the MessageProvider for the current * test URI and the given keys to look up the messages. * * @param keys The keys. * @return This. */ public RequestResult assertBodyContainsMessagesFromKeys(String... keys) { for (String key : keys) { assertBodyContainsMessagesFromKey(key, "foo", "bar", "baz"); } return this; } /** * Verifies that the body does not contain any of the given Strings. * * @param strings The strings to check. * @return This. */ public RequestResult assertBodyDoesNotContain(String... strings) { for (String string : strings) { if (body.contains(string)) { throw new AssertionError("Body shouldn't contain [" + string + "]\nRedirect: [" + redirect + "]\nBody:\n" + body); } } return this; } /** * Verifies that the body equals the content of the given File. * * @param path The file to load and compare to the response. * @param values key value pairs of replacement values for use in the file. * @return This. */ public RequestResult assertBodyFile(Path path, Object... values) throws IOException { if (values.length == 0) { return assertBody(new String(Files.readAllBytes(path), "UTF-8")); } return assertBody(BodyTools.processTemplate(path, values)); } /** * Verifies that the body is empty. * * @return This */ public RequestResult assertBodyIsEmpty() { if (!body.isEmpty()) { throw new AssertionError("Body is not empty.\nBody:\n" + body); } return this; } /** * Assert the cookie exists by name. * * @param name The cookie name. * @return This. */ public RequestResult assertContainsCookie(String name) { Cookie actual = response.getCookies().stream().filter(c -> c.getName().equals(name)).findFirst().orElse(null); if (actual == null) { throw new AssertionError("Cookie [" + name + "] was not found in the response. Cookies found [" + String.join(", ", response.getCookies().stream().map(Cookie::getName).collect(Collectors.toList()))); } return this; } /** * Verifies that the system contains the given error message(s). The message(s) might be in the request, flash, * session or application scopes. * * @param messages The fully rendered error message(s) (not the code). * @return This. */ public RequestResult assertContainsErrors(String... messages) { return assertContainsMessages(MessageType.ERROR, messages); } /** * Verifies that the system has errors for the given fields. This doesn't assert the error itself, just that the * field * contains an error. * * @param fields The name of the field code(s). Not the fully rendered message(s) * @return This. */ public RequestResult assertContainsFieldErrors(String... fields) { MessageStore messageStore = get(MessageStore.class); Map<String, List<FieldMessage>> msgs = messageStore.getFieldMessages(); for (String field : fields) { List<FieldMessage> fieldMessages = msgs.get(field); if (fieldMessages == null) { StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); msgs.keySet().stream().forEach((f) -> sb.append("\t\t" + f + "\n")); throw new AssertionError("The MessageStore does not contain a error for the field [" + field + "]" + sb); } boolean found = false; for (FieldMessage fieldMessage : fieldMessages) { found |= fieldMessage.getType() == MessageType.ERROR; } if (!found) { StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); fieldMessages.stream().forEach((f) -> sb.append("\t\t[" + f.getType() + "]\n")); throw new AssertionError("The MessageStore contains messages but no errors for the field [" + field + "]" + sb); } } return this; } /** * Verifies that the system has general errors. This doesn't assert the error itself, just that the * general error code. * * @param messageCodes The name of the error code(s). Not the fully rendered message(s) * @return This. */ public RequestResult assertContainsGeneralErrorMessageCodes(String... messageCodes) { return assertContainsGeneralMessageCodes(MessageType.ERROR, messageCodes); } /** * Verifies that the system has info errors. This doesn't assert the message itself, just that the * general message code. * * @param messageCodes The name of the message code(s). Not the fully rendered message(s) * @return This. */ public RequestResult assertContainsGeneralInfoMessageCodes(String... messageCodes) { return assertContainsGeneralMessageCodes(MessageType.INFO, messageCodes); } /** * Verifies that the system has general messages. This doesn't assert the message itself, just that the * general message code. * * @param messageType The message type * @param errorCodes The name of the message code(s). Not the fully rendered message(s) * @return This. */ public RequestResult assertContainsGeneralMessageCodes(MessageType messageType, String... errorCodes) { MessageStore messageStore = get(MessageStore.class); List<Message> messages = messageStore.getGeneralMessages(); for (String errorCode : errorCodes) { Message message = messages.stream().filter((m) -> m.getCode().equals(errorCode)).findFirst().orElse(null); if (message == null) { StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); messages.stream().forEach((m) -> sb.append("\t\t" + m.getCode() + " Type: " + m.getType() + "\n")); throw new AssertionError("The MessageStore does not contain the general message [" + errorCode + "] Type: " + messageType + sb); } if (message.getType() != messageType) { StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); messages.stream().forEach((m) -> sb.append("\t\t" + m.getCode() + " Type: " + m.getType() + "\n")); throw new AssertionError("The MessageStore contains message for code [" + message.getCode() + "], but it is of type [" + message.getType() + "]" + sb); } } return this; } /** * Verifies that the system contains the given info message(s). The message(s) might be in the request, flash, * session * or application scopes. * * @param messages The fully rendered info message(s) (not the code). * @return This. */ public RequestResult assertContainsInfos(String... messages) { return assertContainsMessages(MessageType.INFO, messages); } /** * Verifies that the system contains the given message(s). The message(s) might be in the request, flash, session or * application scopes. * * @param type The message type (ERROR, INFO, WARNING). * @param messages The fully rendered message(s) (not the code). * @return This. */ public RequestResult assertContainsMessages(MessageType type, String... messages) { Set<String> inMessageStore = new HashSet<>(); MessageStore messageStore = get(MessageStore.class); List<Message> msgs = messageStore.getGeneralMessages(); for (Message msg : msgs) { if (msg.getType() == type) { inMessageStore.add(msg.toString()); } } if (!inMessageStore.containsAll(asList(messages))) { StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); msgs.forEach((f) -> sb.append("\t\t[" + f + "]\n")); throw new AssertionError("The MessageStore does not contain the [" + type + "] message " + asList(messages) + sb); } return this; } /** * Verifies that the system has no general error messages. * * @return This. */ public RequestResult assertContainsNoGeneralErrors() { return assertContainsNoMessages(MessageType.ERROR); } /** * Verifies that the system has no general messages of the specified type. * * @param messageType The message type * @return This. */ public RequestResult assertContainsNoMessages(MessageType messageType) { MessageStore messageStore = get(MessageStore.class); List<Message> messages = messageStore.getGeneralMessages().stream().filter((m) -> m.getType() == messageType).collect(Collectors.toList()); if (messages.isEmpty()) { return this; } StringBuilder sb = new StringBuilder("\n\tMessageStore contains:\n"); messages.stream().forEach((m) -> sb.append("\t\t" + m.getCode() + " Type: " + m.getType() + "\n")); throw new AssertionError("The MessageStore contains the following errors.]" + sb); } /** * Verifies that the system contains the given warning message(s). The message(s) might be in the request, flash, * session or application scopes. * * @param messages The fully rendered warning message(s) (not the code). * @return This. */ public RequestResult assertContainsWarnings(String... messages) { return assertContainsMessages(MessageType.WARNING, messages); } /** * Verifies the response Content-Type. * * @param contentType The expected content-type * @return This. */ public RequestResult assertContentType(String contentType) { String actual = response.getContentType(); if (actual != null && !actual.equals(contentType)) { throw new AssertionError("Content-Type [" + actual + "] is not equal to the expected value [" + contentType + "]"); } return this; } /** * Assert the cookie exists by name and then pass it to the provided consumer to allow the caller to assert on anything they wish. * * @param name The cookie name. * @param consumer The consumer used to perform assertions. * @return This. */ public RequestResult assertCookie(String name, Consumer<Cookie> consumer) { assertContainsCookie(name); Cookie actual = response.getCookies().stream().filter(c -> c.getName().equals(name)).findFirst().orElse(null); if (consumer != null) { consumer.accept(actual); } return this; } /** * Assert the cookie exists by name and the value matches that of the provided value. * * @param name The cookie name. * @param value The cookie value. * @return This. */ public RequestResult assertCookie(String name, String value) { assertContainsCookie(name); Cookie actual = response.getCookies().stream().filter(c -> c.getName().equals(name)).findFirst().orElse(null); if (actual.getValue() == null || !actual.getValue().equals(value)) { throw new AssertionError("Cookie [" + name + "] with value [" + actual + "] was not equal to the expected value [" + value + "]"); } return this; } /** * Verifies the response encoding. * * @param encoding The expected content-type * @return This. */ public RequestResult assertEncoding(String encoding) { String actual = response.getEncoding(); if (actual != null && !actual.equals(encoding)) { throw new AssertionError("Character Encoding [" + actual + "] is not equal to the expected value [" + encoding + "]"); } return this; } /** * Verifies that the HTTP response contains the specified header. * * @param header the name of the HTTP response header * @param value the value of the header * @return This. */ public RequestResult assertHeaderContains(String header, String value) { List<String> actual = response.getHeaders().get(header); if ((actual == null && value != null) || (actual != null && !actual.contains((value)))) { throw new AssertionError("Header [" + header + "] with value [" + actual + "] was not equal to the expected value [" + value + "]"); } return this; } /** * Verifies that the response body is equal to the JSON created from the given object. The object is marshalled using * Jackson. * * @param object The object. * @return This. * @throws IOException If the JSON marshalling failed. */ public RequestResult assertJSON(Object object) throws IOException { ObjectMapper objectMapper = injector.getInstance(ObjectMapper.class); String json = objectMapper.writeValueAsString(object); return assertJSON(json); } /** * De-serialize the JSON response using the type provided and allow the caller to assert on the result. * * @param type The object type. * @param type The consumer to pass the de-serialized object to. * @return This. * @throws IOException If the JSON marshalling failed. */ public <T> RequestResult assertJSON(Class<T> type, Consumer<T> consumer) throws IOException { ObjectMapper objectMapper = injector.getInstance(ObjectMapper.class); T response = objectMapper.readValue(body, type); consumer.accept(response); return this; } /** * Verifies that the response body is equal to the given JSON text. * * @param json The JSON text. * @return This. * @throws IOException If the JSON marshalling failed. */ public RequestResult assertJSON(String json) throws IOException { ObjectMapper objectMapper = injector.getInstance(ObjectMapper.class); assertJSONEquals(objectMapper, body, json); return this; } /** * Verifies that the response body is equal to the given JSON text file. * * @param jsonFile The JSON file to load and compare to the JSON response. * @param values key value pairs of replacement values for use in the JSON file. * @return This. * @throws IOException If the JSON marshalling failed. */ public RequestResult assertJSONFile(Path jsonFile, Object... values) throws IOException { return assertJSON(BodyTools.processTemplate(jsonFile, appendArray(values, "_to_milli", new ZonedDateTimeToMilliSeconds()))); } /** * De-serialize the JSON response using the type provided. To use actual values in the JSON use ${actual.foo} * to use the property named <code>foo</code>. * * @param type The object type of the JSON. * @param jsonFile The JSON file to load and compare to the JSON response. * @param values key value pairs of replacement values for use in the JSON file. * @return This. * @throws IOException If the JSON marshalling failed. */ public <T> RequestResult assertJSONFileWithActual(Class<T> type, Path jsonFile, Object... values) throws IOException { ObjectMapper objectMapper = injector.getInstance(ObjectMapper.class); T actual = objectMapper.readValue(body, type); return assertJSONFile(jsonFile, appendArray(values, "actual", actual, "_to_milli", new ZonedDateTimeToMilliSeconds())); } /** * Verifies that the redirect URI is the given URI. * * @param uri The redirect URI. * @return This. */ public RequestResult assertRedirect(String uri) { if (redirect == null || !redirect.equals(uri)) { throw new AssertionError("\nActual redirect not equal to the expected.\n Actual: \t" + redirect + "\n Expected:\t" + uri); } return this; } /** * Verifies that the request contains the attribute and the value is equal. * * @param name the attribute name. * @param value the attribute value. * @return This. */ public RequestResult assertRequestContainsAttribute(String name, Object value) { if (request.getAttribute(name) == null) { throw new AssertionError("Attribute [" + name + "] was not found in the request."); } if (!request.getAttribute(name).equals(value)) { throw new AssertionError("Attribute [" + name + "] was not equal to the expected value.\n\tActual: " + value + "\n\tExpected: " + request.getAttribute(name) + "\n"); } return this; } /** * Verifies that the response status code is equal to the given code. * * @param statusCode The status code. * @return This. */ public RequestResult assertStatusCode(int statusCode) { if (this.statusCode != statusCode) { throw new AssertionError("Status code [" + this.statusCode + "] was not equal to [" + statusCode + "]\nResponse body: [" + body + "]\nRedirect: [" + redirect + "]"); } return this; } /** * Retrieves the instance of the given type from the Guice Injector. * * @param type The type. * @param <T> The type. * @return The instance. */ public <T> T get(Class<T> type) { return injector.getInstance(type); } /** * Retrieve a cookie by name. If the cookie does not exist in the response it will fail. * * @param name The name of the cookie. * @return the Cookie. */ public Cookie getCookie(String name) { Cookie cookie = response.getCookies().stream().filter(c -> c.getName().equals(name)).findFirst().orElse(null); if (cookie == null) { throw new AssertionError("Cookie [" + name + "] was not found in the response. Cookies found [" + String.join(", ", response.getCookies().stream().map(Cookie::getName).collect(Collectors.toList()))); } return cookie; } /** * If the test is false, apply the consumer. * <p> * <pre> * .ifFalse(foo.isBar(), (requestResult) -> requestResult.assertBodyDoesNotContain("bar")) * </pre> * * @param test The boolean test to indicate if the consumer should be used. * @param consumer The consumer that accepts the RequestResult. * @return This. */ public RequestResult ifFalse(boolean test, Consumer<RequestResult> consumer) { if (!test) { consumer.accept(this); } return this; } /** * If the test is true, apply the consumer. Example: * <pre> * .ifTrue(foo.isBar(), (requestResult) -> requestResult.assertBodyContains("bar")) * </pre> * * @param test The boolean test to indicate if the consumer should be used. * @param consumer The consumer that accepts the RequestResult. * @return This. */ public RequestResult ifTrue(boolean test, Consumer<RequestResult> consumer) { if (test) { consumer.accept(this); } return this; } /** * Can be called to setup objects for assertions. * * @param consumer The consumer that accepts the RequestResult. * @return This. */ public RequestResult setup(Consumer<RequestResult> consumer) { consumer.accept(this); return this; } private Object[] appendArray(Object[] values, Object... objects) { ArrayList<Object> list = new ArrayList<>(Arrays.asList(values)); Collections.addAll(list, objects); return list.toArray(); } }