package io.github.azagniotov.stubby4j.stubs; import io.github.azagniotov.stubby4j.annotations.VisibleForTesting; import io.github.azagniotov.stubby4j.cli.ANSITerminal; import io.github.azagniotov.stubby4j.common.Common; import org.custommonkey.xmlunit.Diff; import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier; import org.json.JSONException; import org.skyscreamer.jsonassert.JSONCompare; import org.skyscreamer.jsonassert.JSONCompareMode; import org.xml.sax.SAXException; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import static io.github.azagniotov.stubby4j.utils.StringUtils.escapeSpecialRegexCharacters; import static io.github.azagniotov.stubby4j.utils.StringUtils.isNotSet; import static io.github.azagniotov.stubby4j.utils.StringUtils.isSet; import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.HEADERS; import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.POST; import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.QUERY; import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.URL; class StubMatcher { private final Map<String, String> regexGroups; StubMatcher(final Map<String, String> regexGroups) { this.regexGroups = regexGroups; } boolean matches(final StubRequest stubbedRequest, final StubRequest assertingRequest) { if (!urlsMatch(stubbedRequest.getUri(), assertingRequest.getUri())) { ANSITerminal.error(String.format("Failed to match on URL [%s] WITH [%s]", stubbedRequest.getUri(), assertingRequest.getUri())); return false; } ANSITerminal.info(String.format("Matched on URL [%s] WITH [%s]", stubbedRequest.getUri(), assertingRequest.getUri())); if (!listsIntersect(stubbedRequest.getMethod(), assertingRequest.getMethod())) { ANSITerminal.error(String.format("Failed to match on METHOD [%s] WITH [%s]", stubbedRequest.getMethod(), assertingRequest.getMethod())); return false; } ANSITerminal.info(String.format("Matched on METHOD [%s] WITH [%s]", stubbedRequest.getMethod(), assertingRequest.getMethod())); if (!postBodiesMatch(stubbedRequest.isPostStubbed(), stubbedRequest.getPostBody(), assertingRequest)) { ANSITerminal.error(String.format("Failed to match on POST BODY [%s] WITH [%s]", stubbedRequest.getPostBody(), assertingRequest.getPostBody())); return false; } ANSITerminal.info(String.format("Matched on POST BODY [%s] WITH [%s]", stubbedRequest.getPostBody(), assertingRequest.getPostBody())); if (!headersMatch(stubbedRequest.getHeaders(), assertingRequest.getHeaders())) { ANSITerminal.error(String.format("Failed to match on HEADERS [%s] WITH [%s]", stubbedRequest.getHeaders(), assertingRequest.getHeaders())); return false; } ANSITerminal.info(String.format("Matched on HEADERS [%s] WITH [%s]", stubbedRequest.getHeaders(), assertingRequest.getHeaders())); if (!queriesMatch(stubbedRequest.getQuery(), assertingRequest.getQuery())) { ANSITerminal.error(String.format("Failed to match on QUERY [%s] WITH [%s]", stubbedRequest.getQuery(), assertingRequest.getQuery())); return false; } ANSITerminal.info(String.format("Matched on QUERY [%s] WITH [%s]", stubbedRequest.getQuery(), assertingRequest.getQuery())); return true; } private boolean urlsMatch(final String stubbedUrl, final String assertingUrl) { return stringsMatch(stubbedUrl, assertingUrl, URL.toString()); } private boolean postBodiesMatch(final boolean isPostStubbed, final String stubbedPostBody, final StubRequest assertingRequest) { final String assertingPostBody = assertingRequest.getPostBody(); if (isPostStubbed) { final String assertingContentType = assertingRequest.getHeaders().get("content-type"); if (isNotSet(assertingPostBody)) { return false; } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) { return jsonMatch(stubbedPostBody, assertingPostBody); } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_XML)) { return xmlMatch(stubbedPostBody, assertingPostBody); } else { return stringsMatch(stubbedPostBody, assertingPostBody, POST.toString()); } } return true; } private boolean queriesMatch(final Map<String, String> stubbedQuery, final Map<String, String> assertingQuery) { return mapsMatch(stubbedQuery, assertingQuery, QUERY.toString()); } private boolean headersMatch(final Map<String, String> stubbedHeaders, final Map<String, String> assertingHeaders) { final Map<String, String> stubbedHeadersCopy = new HashMap<>(stubbedHeaders); for (final StubbableAuthorizationType authorizationType : StubbableAuthorizationType.values()) { // auth header is dealt with in StubRepository after request is matched stubbedHeadersCopy.remove(authorizationType.asYAMLProp()); } return mapsMatch(stubbedHeadersCopy, assertingHeaders, HEADERS.toString()); } @VisibleForTesting boolean mapsMatch(final Map<String, String> stubbedMappings, final Map<String, String> assertingMappings, final String mapName) { if (stubbedMappings.isEmpty()) { return true; } else if (assertingMappings.isEmpty()) { return false; } final Map<String, String> stubbedMappingsCopy = new HashMap<>(stubbedMappings); final Map<String, String> assertingMappingsCopy = new HashMap<>(assertingMappings); for (final Map.Entry<String, String> stubbedMappingEntry : stubbedMappingsCopy.entrySet()) { final boolean containsRequiredParam = assertingMappingsCopy.containsKey(stubbedMappingEntry.getKey()); if (!containsRequiredParam) { return false; } else { final String assertingValue = assertingMappingsCopy.get(stubbedMappingEntry.getKey()); final String templateTokenName = String.format("%s.%s", mapName, stubbedMappingEntry.getKey()); if (!stringsMatch(stubbedMappingEntry.getValue(), assertingValue, templateTokenName)) { return false; } } } return true; } @VisibleForTesting boolean stringsMatch(final String stubbedValue, final String assertingValue, final String templateTokenName) { if (isNotSet(stubbedValue)) { return true; } else if (isNotSet(assertingValue)) { return false; } return regexMatch(stubbedValue, assertingValue, templateTokenName) || stubbedValue.equals(assertingValue); } private boolean regexMatch(final String stubbedValue, final String assertingValue, final String templateTokenName) { return RegexParser.INSTANCE.match(stubbedValue, assertingValue, templateTokenName, regexGroups); } @VisibleForTesting boolean listsIntersect(final List<String> stubbedArray, final List<String> assertingArray) { if (stubbedArray.isEmpty()) { return true; } else if (!assertingArray.isEmpty()) { for (final String entry : assertingArray) { if (stubbedArray.contains(entry)) { return true; } } } return false; } private boolean jsonMatch(final String stubbedJson, final String assertingJson) { try { boolean passed = JSONCompare.compareJSON(stubbedJson, assertingJson, JSONCompareMode.NON_EXTENSIBLE).passed(); if (passed) { return true; } else { final String escapedStubbedPostBody = escapeSpecialRegexCharacters(stubbedJson); return stringsMatch(escapedStubbedPostBody, assertingJson, POST.toString()); } } catch (final JSONException e) { final String escapedStubbedPostBody = escapeSpecialRegexCharacters(stubbedJson); return stringsMatch(escapedStubbedPostBody, assertingJson, POST.toString()); } } private boolean xmlMatch(final String stubbedXml, final String assertingXml) { try { final Diff diff = new Diff(stubbedXml, assertingXml); diff.overrideElementQualifier(new ElementNameAndAttributeQualifier()); return (diff.similar() || diff.identical()); } catch (SAXException | IOException e) { return stringsMatch(stubbedXml, assertingXml, POST.toString()); } } }