All Downloads are FREE. Search and download functionalities are using the official Maven repository.

es.iti.wakamiti.rest.helpers.JsonXmlDiff Maven / Gradle / Ivy

The newest version!
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package es.iti.wakamiti.rest.helpers;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.restassured.http.ContentType;
import es.iti.wakamiti.api.WakamitiException;
import es.iti.wakamiti.rest.MatchMode;
import org.junit.ComparisonFailure;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static es.iti.wakamiti.rest.MatchMode.*;


/**
 * @author Luis Iñesta Gelabert - [email protected] | [email protected]
 */
public class JsonXmlDiff {

    private final ObjectMapper mapper;


    public JsonXmlDiff(ContentType contentType) {
        if (contentType == ContentType.JSON) {
            this.mapper = new ObjectMapper();
        } else if (contentType == ContentType.XML) {
            this.mapper = new XmlMapper();
        } else {
            throw new IllegalArgumentException("Only either JSON or XML types are allowed");
        }
        this.mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
    }

    private static String literalSegmentExpected(String prefix) {
        return prefix.isEmpty() ? "root segment expected" : "segment '" + prefix + "' expected";
    }

    private static  List asList(Iterator i) {
        List list = new ArrayList<>();
        i.forEachRemaining(list::add);
        return list;
    }

    private static String errorSize(JsonNode expectedNode, JsonNode actualNode, String segmentExpected) {
        return segmentExpected + " size: " + expectedNode.size() + ", actual size: " + actualNode.size();
    }

    public void assertContent(String expected, String actual, MatchMode matchMode) {
        try {
            List errors = new ArrayList<>();
            JsonNode expectedJson = mapper.readTree(expected);
            JsonNode actualJson = mapper.readTree(actual);
            compareJsonNode(matchMode, expectedJson, actualJson, "", errors);
            throwExceptionIfHasErrors(errors, expected, actual);
        } catch (JsonProcessingException e) {
            throw new WakamitiException(e);
        }
    }

    private void throwExceptionIfHasErrors(List errors, String expected, String actual)
            throws ComparisonFailure, JsonProcessingException {
        if (!errors.isEmpty()) {
            var message = errors.stream().collect(Collectors.joining(
                    "\n\t-", "The expected and actual responses have differences:\n\t-", "\n"
            ));
            throw new ComparisonFailure(message, format(expected), format(actual));
        }
    }

    private String format(String content) throws JsonProcessingException {
        return mapper.writeValueAsString(mapper.readTree(content));
    }

    private boolean compareJsonNode(
            MatchMode matchMode,
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix
    ) {
        List errors = new LinkedList<>();
        compareJsonNode(matchMode, expectedNode, actualNode, prefix, errors);
        return errors.isEmpty();
    }

    private void compareJsonNode(
            MatchMode matchMode,
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors
    ) {
        var expectedType = expectedNode.getNodeType();
        var actualType = actualNode.getNodeType();
        String segmentExpected = literalSegmentExpected(prefix);

        if (actualNode.isMissingNode() && !expectedNode.isMissingNode()
                && List.of(STRICT, STRICT_ANY_ORDER).contains(matchMode)) {
            errors.add(segmentExpected + ", but is not present");
            return;
        }

        if (expectedType != actualType) {
            errors.add(segmentExpected + " to be a " + expectedType + ", but it is " + actualType);
            return;
        }

        if (expectedNode.isArray()) {
            compareJsonArray(matchMode, expectedNode, actualNode, prefix, errors);
        } else if (expectedNode.isContainerNode()) {
            compareJsonObject(matchMode, expectedNode, actualNode, prefix, errors);
        } else if (expectedNode.isValueNode() && !expectedNode.equals(actualNode)) {
            errors.add(
                    segmentExpected + ": '" + expectedNode.asText() +
                            "', actual: '" + actualNode.asText() + "'"
            );
        }
    }

    private void compareJsonArray(
            MatchMode matchMode,
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors
    ) {
        String segmentExpected = literalSegmentExpected(prefix);
        switch (matchMode) {
            case STRICT:
                compareJsonArrayStrict(expectedNode, actualNode, prefix, errors, segmentExpected);
                break;
            case STRICT_ANY_ORDER:
                compareJsonArrayStrictAnyOrder(expectedNode, actualNode, prefix, errors, segmentExpected);
                break;
            case LOOSE:
                compareJsonArrayLoose(expectedNode, actualNode, prefix, errors, segmentExpected);
                break;
        }
    }

    private void compareJsonArrayStrict(
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors,
            String segmentExpected
    ) {
        if (expectedNode.size() != actualNode.size()) {
            errors.add(errorSize(expectedNode, actualNode, segmentExpected));
            return;
        }
        for (int i = 0; i < expectedNode.size(); i++) {
            compareJsonNode(STRICT, expectedNode.get(i), actualNode.get(i), prefix + "[" + i + "]", errors);
        }
    }

    private void compareJsonArrayStrictAnyOrder(
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors,
            String segmentExpected
    ) {
        if (expectedNode.size() != actualNode.size()) {
            errors.add(errorSize(expectedNode, actualNode, segmentExpected));
            return;
        }
        compareJsonArrayUnordered(expectedNode, actualNode, prefix, errors, STRICT_ANY_ORDER);
    }

    private void compareJsonArrayLoose(
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors,
            String segmentExpected
    ) {
        if (expectedNode.size() > actualNode.size()) {
            errors.add(segmentExpected + " minimum size: " + expectedNode.size() + ", actual size: " + actualNode.size());
            return;
        }
        compareJsonArrayUnordered(expectedNode, actualNode, prefix, errors, LOOSE);
    }

    private void compareJsonArrayUnordered(
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors,
            MatchMode mode
    ) {
        boolean currentElementMatch;
        for (int i = 0; i < expectedNode.size(); i++) {
            currentElementMatch = false;
            for (int j = 0; j < actualNode.size() && !currentElementMatch; j++) {
                if (compareJsonNode(mode, expectedNode.get(i), actualNode.get(j), prefix + "[" + i + "]")) {
                    currentElementMatch = true;
                }
            }
            if (!currentElementMatch) {
                compareJsonNode(mode, expectedNode.get(i), actualNode.get(i), prefix + "[" + i + "]", errors);
                return;
            }
        }
    }

    private void compareJsonObject(
            MatchMode matchMode,
            JsonNode expectedNode,
            JsonNode actualNode,
            String prefix,
            List errors
    ) {

        String segmentExpected = literalSegmentExpected(prefix);
        var expectedFields = asList(expectedNode.fieldNames());
        var actualFields = asList(actualNode.fieldNames());

        var missingExpectedFields = expectedFields.stream()
                .filter(Predicate.not(actualFields::contains))
                .collect(Collectors.toList());
        var nonExpectedActualFields = actualFields.stream()
                .filter(Predicate.not(expectedFields::contains))
                .collect(Collectors.toList());

        if (!missingExpectedFields.isEmpty()) {
            errors.add(
                    segmentExpected + " to have fields " + missingExpectedFields +
                            ", but they are not present"
            );
            return;
        }

        if (!nonExpectedActualFields.isEmpty() && List.of(STRICT, STRICT_ANY_ORDER).contains(matchMode)) {
            errors.add(
                    segmentExpected + " not to have fields " + nonExpectedActualFields +
                            ", but they are present"
            );
        }

        for (int i = 0; i < expectedFields.size(); i++) {
            String expectedField = expectedFields.get(i);
            String actualFieldInSamePosition = actualFields.get(i);
            if (matchMode == STRICT && !expectedField.equals(actualFieldInSamePosition)) {
                errors.add(
                        segmentExpected + " to have field '" + expectedField + "' at position " +
                                i + " but it was '" + actualFieldInSamePosition + "'"
                );
                continue;
            }
            compareJsonNode(
                    matchMode,
                    expectedNode.get(expectedField),
                    actualNode.get(expectedField),
                    prefix + (prefix.isEmpty() ? "" : ".") + expectedField, errors)
            ;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy