Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[plugin-web-app-to-rest-api] Treat RFC-non-compliant URL as assertion failure #3341

Merged
merged 1 commit into from
Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.tuple.Pair;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.model.ExamplesTable;
import org.jsoup.nodes.Element;
Expand Down Expand Up @@ -105,7 +105,17 @@ public void checkResources(String cssSelector, String html) throws InterruptedEx
execute(() -> {
Collection<Element> resourcesToValidate = getElementsByCssSelector(html, cssSelector);
Stream<WebPageResourceValidation> validations = createResourceValidations(resourcesToValidate,
p -> createResourceValidation(p.getLeft(), p.getRight()));
resourceValidation -> {
URI uriToCheck = resourceValidation.getUri();
if (isNotAbsolute(uriToCheck))
{
softAssert.recordFailedAssertion(String.format(
"Unable to resolve %s resource since the main application page URL is not set",
uriToCheck));

resourceValidation.setCheckStatus(CheckStatus.BROKEN);
}
});
validateResources(validations);
});
}
Expand All @@ -127,63 +137,87 @@ private WebPageResourceValidation validate(WebPageResourceValidation r)
}

private Stream<WebPageResourceValidation> createResourceValidations(Collection<Element> elements,
Function<Pair<URI, String>, WebPageResourceValidation> resourceValidationFactory)
{
return elements.parallelStream().map(e ->
Pair.of(getHrefAttribute(e).orElseGet(() -> e.attr("src")).trim(), getSelector(e)))
.filter(p -> !p.getKey().isEmpty() || softAssert.recordFailedAssertion(
"Element by selector " + p.getValue() + " doesn't contain href/src attributes"))
.map(p -> Pair.of(createUri(p.getKey()), p.getValue()))
.map(resourceValidationFactory)
.peek(rv ->
{
if ((!isSchemaAllowed(rv.getUri()) || excludeHrefsPattern.matcher(rv.toString()).matches())
&& rv.getCheckStatus() != CheckStatus.BROKEN)
{
rv.setCheckStatus(CheckStatus.FILTERED);
}
});
}

private static boolean isSchemaAllowed(URI uri)
Consumer<WebPageResourceValidation> resourceValidator)
{
return Optional.ofNullable(uri.getScheme()).map(ALLOWED_SCHEMES::contains).orElse(false);
return elements.stream()
.map(this::parseElement)
.filter(Optional::isPresent)
.map(Optional::get)
.peek(rv ->
{
resourceValidator.accept(rv);
if ((!isSchemaAllowed(rv.getUri()) || excludeHrefsPattern.matcher(rv.toString()).matches())
&& rv.getCheckStatus() != CheckStatus.BROKEN)
{
rv.setCheckStatus(CheckStatus.FILTERED);
}
})
.parallel();
}

private String getSelector(Element element)
private Optional<WebPageResourceValidation> parseElement(Element element)
{
String elementUriAsString = getElementUri(element).trim();
String elementCssSelector = getCssSelector(element);
if (elementUriAsString.isEmpty())
{
softAssert.recordFailedAssertion(
String.format("Element by selector %s doesn't contain href/src attributes", elementCssSelector));
return Optional.empty();
}
try
{
return element.cssSelector();
URI elementUri = resolveUri(elementUriAsString);
return Optional.of(new WebPageResourceValidation(elementUri, elementCssSelector));
}
catch (SelectorParseException exception)
catch (URISyntaxException e)
{
return "N/A";
softAssert.recordFailedAssertion(
String.format("Element by selector %s has href/src attribute with invalid URL", elementCssSelector),
e);
return Optional.empty();
}
}

private static Optional<String> getHrefAttribute(Element element)
private static boolean isSchemaAllowed(URI uri)
{
return Optional.ofNullable(uri.getScheme()).map(ALLOWED_SCHEMES::contains).orElse(false);
}

private static String getElementUri(Element element)
{
String href = element.attr(HREF_ATTR);
if (!href.isEmpty())
{
if (URL_FRAGMENT.equals(href))
{
return Optional.of(href);
return href;
}

String absUrl = element.absUrl(HREF_ATTR);
// For scripts e.g. href="javascript:alert('...');" the abs url will be empty
return Optional.of(absUrl.isEmpty() ? href : absUrl);
return absUrl.isEmpty() ? href : absUrl;
}

return Optional.empty();
return element.attr("src");
}

private URI createUri(String uri)
private String getCssSelector(Element element)
{
URI uriToCheck = URI.create(uri);
if (!isAbsolute(uriToCheck))
try
{
return element.cssSelector();
}
catch (SelectorParseException exception)
{
return "N/A";
}
}

private URI resolveUri(String uri) throws URISyntaxException
{
URI uriToCheck = new URI(uri);
if (isNotAbsolute(uriToCheck))
{
URI mainApplicationPageUrl = webApplicationConfiguration.getMainApplicationPageUrlUnsafely();
if (mainApplicationPageUrl != null)
Expand All @@ -194,9 +228,9 @@ private URI createUri(String uri)
return uriToCheck;
}

private static boolean isAbsolute(URI uri)
private static boolean isNotAbsolute(URI uri)
{
return uri.isAbsolute() || uri.toString().charAt(0) != '/';
return !uri.isAbsolute() && uri.toString().charAt(0) == '/';
}

/**
Expand All @@ -207,11 +241,13 @@ private static boolean isAbsolute(URI uri)
* a. If status code acceptable than check considered as passed;
* b. If status code not acceptable but one of (404, 405, 501, 503) then GET request will be sendt;
* c. If GET status code acceptable than check considered as passed otherwise failed;
* <b>Example</b>
* <b>Example:</b>
* <pre>
* Then all resources by selector a are valid on:
* |pages|
* |https://vividus.org|
* |/test-automation-made-awesome|
* </pre>
* @param cssSelector to locate resources
* @param pages where resources will be validated
* @throws InterruptedException when a thread is interrupted
Expand All @@ -222,25 +258,34 @@ public void checkResources(String cssSelector, ExamplesTable pages) throws Inter
{
execute(() -> {
Stream<WebPageResourceValidation> resourcesToValidate =
pages.getRows()
.parallelStream()
.map(m -> m.get("pages"))
.map(this::createUri)
.flatMap(uri ->
pages.getRows().stream().map(m -> m.get("pages")).parallel()
.flatMap(rawPageUrl ->
{
String pageUrl = uri.toString();
if (!isAbsolute(uri))
String pageUrl;
try
{
URI uri = resolveUri(rawPageUrl);
pageUrl = uri.toString();
if (isNotAbsolute(uri))
{
return Stream.of(createUnresolvablePageValidation(pageUrl));
}
}
catch (URISyntaxException e)
{
return Stream.of(createUnresolvablePageValidation(pageUrl));
softAssert.recordFailedAssertion("Invalid page URL", e);
return Stream.of(createBrokenPageValidation(rawPageUrl));
}


try
{
httpRequestExecutor.executeHttpRequest(HttpMethod.GET, pageUrl, Optional.empty());
return Optional.ofNullable(httpTestContext.getResponse().getResponseBodyAsString())
.map(response -> getElementsByCssSelector(pageUrl, response, cssSelector))
.map(elements -> createResourceValidations(elements,
p -> new WebPageResourceValidation(p.getLeft(), p.getRight(), pageUrl)))
rV -> rV.setPageURL(pageUrl)
))
.orElseGet(() -> Stream.of(createMissingPageBodyValidation(pageUrl)));
}
catch (IOException toReport)
Expand All @@ -258,20 +303,6 @@ private void execute(Runnable executable) throws InterruptedException, Execution
(t, e) -> softAssert.recordFailedAssertion("Exception occured in thread with name: " + t.getName(), e));
}

private WebPageResourceValidation createResourceValidation(URI uriToCheck, String cssSelector)
{
WebPageResourceValidation resourceValidation = new WebPageResourceValidation(uriToCheck, cssSelector);
if (!isAbsolute(uriToCheck))
{
softAssert.recordFailedAssertion(
String.format("Unable to resolve %s resource since the main application page URL is not set",
uriToCheck));

resourceValidation.setCheckStatus(CheckStatus.BROKEN);
}
return resourceValidation;
}

private WebPageResourceValidation createUnreachablePageValidation(String pageURL, Exception exception)
{
softAssert.recordFailedAssertion("Unable to get page with URL: " + pageURL, exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
Expand All @@ -40,11 +41,16 @@
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.jbehave.core.model.ExamplesTable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
Expand Down Expand Up @@ -96,6 +102,11 @@ class ResourceCheckStepsTests
private static final URI VIVIDUS_QUERY_URI_2 = URI.create("https://vividus.org/products?name=smetanka");
private static final String SELECTOR_QUERY_2 = "#query-params-two";

private static final String INVALID_URL = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|"
+ "Google+Sans:400,500,700|Google+Sans+Text:400&lang=en";
private static final String INVALID_URL_ERROR =
"java.net.URISyntaxException: Illegal character in query at index 62: " + INVALID_URL;

private static final String FIRST_PAGE =
"<!DOCTYPE html>\n"
+ "<html>\n"
Expand Down Expand Up @@ -139,6 +150,7 @@ class ResourceCheckStepsTests
+ "<body>\r\n"
+ " <a id='link-id' href='https://vividus.org/about'>About</a>\r\n"
+ " <video id='video-id'>Some video without attributes</a>\r\n"
+ " <a id='link-id-2' href='" + INVALID_URL + "'>Fonts</a>\r\n"
+ "</body>\r\n"
+ "</html>\r\n";

Expand Down Expand Up @@ -218,28 +230,38 @@ void shouldConsiderResourceAsBrokenIfUnableToResolveTheUrlFromHtmlDocument()
"Unable to resolve /faq resource since the main application page URL is not set");
}

@Test
void shouldConsiderResourceAsBrokenIfUnableToResolveTheUrlFromPage()
throws IOException, InterruptedException, ExecutionException
static Stream<Arguments> brokenPageUrls()
{
return Stream.of(
arguments("/relative", (Consumer<ISoftAssert>) softAssert -> verify(softAssert).recordFailedAssertion(
"Unable to resolve /relative page since the main application page URL is not set")),
arguments(INVALID_URL, (Consumer<ISoftAssert>) softAssert -> verify(softAssert).recordFailedAssertion(
eq("Invalid page URL"), argThat(e -> INVALID_URL_ERROR.equals(e.toString()))))
);
}

@ParameterizedTest
@MethodSource("brokenPageUrls")
void shouldConsiderResourceAsBrokenIfUnableToResolveTheUrlFromPage(String pageUrl,
Consumer<ISoftAssert> softAssertVerifier) throws InterruptedException, ExecutionException
{
mockResourceValidator();
runExecutor();
resourceCheckSteps.setUriToIgnoreRegex(Optional.empty());
resourceCheckSteps.init();
ExamplesTable examplesTable = new ExamplesTable("|pages|\n|/faq|");
ExamplesTable examplesTable = new ExamplesTable("{valueSeparator=!}\n|pages|\n!" + pageUrl + "!");
resourceCheckSteps.checkResources(LINK_SELECTOR, examplesTable);

verify(attachmentPublisher).publishAttachment(eq(TEMPLATE_NAME), argThat(m -> {
@SuppressWarnings(UNCHECKED)
Set<WebPageResourceValidation> validationsToReport = ((Map<String, Set<WebPageResourceValidation>>) m)
.get(RESULTS);
assertThat(validationsToReport, hasSize(1));
validate(validationsToReport.iterator(), null, N_A, CheckStatus.BROKEN, FAQ_URL);
validate(validationsToReport.iterator(), null, N_A, CheckStatus.BROKEN, pageUrl);
return true;
}), eq(REPORT_NAME));
verifyNoInteractions(httpRequestExecutor);
verify(softAssert)
.recordFailedAssertion("Unable to resolve /faq page since the main application page URL is not set");
softAssertVerifier.accept(softAssert);
}

@Test
Expand Down Expand Up @@ -308,6 +330,9 @@ void shouldCheckResourcesFromPagesWithEmptyResource() throws IOException, Interr
return true;
}), eq(REPORT_NAME));
verify(softAssert).recordFailedAssertion("Element by selector #video-id doesn't contain href/src attributes");
verify(softAssert).recordFailedAssertion(
eq("Element by selector #link-id-2 has href/src attribute with invalid URL"),
argThat(e -> INVALID_URL_ERROR.equals(e.toString())));
}

@Test
Expand Down