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-rest-api] Post executed requests in a curl syntax in allure attachments #4724

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 the original author or authors.
* Copyright 2019-2024 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.
Expand All @@ -16,6 +16,7 @@

package org.vividus.http;

import java.nio.charset.Charset;
import java.util.Optional;
import java.util.stream.Stream;

Expand All @@ -24,11 +25,12 @@
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.MessageSupport;

public final class MimeTypeUtils
public final class ContentTypeHeaderParser
{
private MimeTypeUtils()
private ContentTypeHeaderParser()
{
}

Expand All @@ -40,17 +42,29 @@ private MimeTypeUtils()
*/
public static String getMimeTypeFromHeadersWithDefault(Header... headers)
{
return getMimeTypeFromHeaders(headers).orElseGet(ContentType.DEFAULT_TEXT::getMimeType);
return getMimeType(headers).orElseGet(ContentType.DEFAULT_TEXT::getMimeType);
}

public static Optional<String> getMimeTypeFromHeaders(Header... headers)
public static Optional<String> getMimeType(Header... headers)
{
return getContentTypeHeader(headers).map(HeaderElement::getName);
}

public static Optional<Charset> getCharset(Header... headers)
{
return getContentTypeHeader(headers)
.map(h -> h.getParameterByName("charset"))
.map(NameValuePair::getValue)
.map(Charset::forName);
}

private static Optional<HeaderElement> getContentTypeHeader(Header... headers)
{
return Stream.of(headers)
.filter(h -> HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(h.getName())
&& StringUtils.isNotBlank(h.getValue()))
.findFirst()
.map(MessageSupport::parse)
.map(elements -> elements[0])
.map(HeaderElement::getName);
.map(elements -> elements[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2019-2024 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
*
* https://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.vividus.http;

import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpRequest;

public final class CurlUtils
{
private static final Pattern NAME_FILE_NAME_PATTERN = Pattern.compile("name=\"(.+)\"; filename=\"(.+)\"");
private static final Pattern NAME_CONTENT_PATTERN = Pattern.compile("name=\"(.+)\"\r\nContent-Type:.+\r\n\r\n(.*)");
private static final String SINGLE_QUOTE = "'";
private static final String END_OF_LINE = " \\\n";
private static final String DOUBLE_QUOTE = "\"";

private CurlUtils()
{
}

public static String buildCurlCommand(HttpRequest request, String mimeType,
byte[] body, boolean binary) throws URISyntaxException
{
StringBuilder curlCommand = new StringBuilder("curl ");
appendMethodAndUri(curlCommand, request);
appendHeaders(curlCommand, request.getHeaders());
if (body != null)
{
Charset charset = ContentTypeHeaderParser.getCharset(request.getHeaders()).orElse(StandardCharsets.UTF_8);
appendBody(curlCommand, mimeType, charset, body, binary);
}
return curlCommand.toString();
}

private static void appendMethodAndUri(StringBuilder curlCommand, HttpRequest request) throws URISyntaxException
{
curlCommand.append("-X ").append(request.getMethod()).append(" '")
.append(request.getUri()).append(SINGLE_QUOTE);
}

private static void appendHeaders(StringBuilder curlCommand, Header... headers)
{
Stream.of(headers).forEach(h -> curlCommand.append(END_OF_LINE)
.append("-H '").append(h.getName()).append(": ").append(h.getValue()).append(SINGLE_QUOTE));
}

private static void appendBody(StringBuilder curlCommand, String mimeType, Charset charset,
byte[] body, boolean binary)
{
String bodyAsString = new String(body, charset);
if (binary)
{
curlCommand.append(END_OF_LINE).append("--data-binary '@<path_to_binary_content>'");
abudevich marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (mimeType.contains("multipart"))
{
appendMultipartData(curlCommand, bodyAsString);
return;
}
curlCommand.append(END_OF_LINE).append("-d '").append(bodyAsString).append(SINGLE_QUOTE);
abudevich marked this conversation as resolved.
Show resolved Hide resolved
}

private static void appendMultipartData(StringBuilder curlCommand, String bodyAsString)
{
String regex = bodyAsString.split("\\R", 2)[0];
String[] formDataArray = bodyAsString.split(regex + ".*");

Stream.of(formDataArray).forEach(e ->
{
Matcher matcher = NAME_FILE_NAME_PATTERN.matcher(e);
String formStringStart = "-F '";
if (matcher.find())
{
curlCommand.append(END_OF_LINE).append(formStringStart).append(matcher.group(1))
.append("=@\"<path-to-file>").append(matcher.group(2))
.append(DOUBLE_QUOTE).append(SINGLE_QUOTE);
}
else
{
matcher = NAME_CONTENT_PATTERN.matcher(e);
if (matcher.find())
{
curlCommand.append(END_OF_LINE).append(formStringStart).append(matcher.group(1))
.append("=\"").append(matcher.group(2)).append(DOUBLE_QUOTE).append(SINGLE_QUOTE);
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 the original author or authors.
* Copyright 2019-2024 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.
Expand Down Expand Up @@ -125,7 +125,7 @@ public void handle(HttpResponse httpResponse)

if (httpResponse.getResponseBody() != null)
{
String mimeType = MimeTypeUtils.getMimeTypeFromHeadersWithDefault(headers);
String mimeType = ContentTypeHeaderParser.getMimeTypeFromHeadersWithDefault(headers);
if (mimeType.startsWith("text/") || LOGGED_CONTENT_TYPES.contains(mimeType))
{
loggingEventBuilder = loggingEventBuilder.addArgument(httpResponse::getResponseBodyAsString);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 the original author or authors.
* Copyright 2019-2024 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.
Expand All @@ -18,18 +18,21 @@

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.text.StringEscapeUtils;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpEntityContainer;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -53,16 +56,18 @@ public void process(HttpRequest request, EntityDetails entityDetails, HttpContex
{
byte[] body = null;
String mimeType = null;
boolean binaryContent = false;
if (request instanceof HttpEntityContainer httpEntityContainer)
{
HttpEntity entity = httpEntityContainer.getEntity();
if (entity != null)
{
mimeType = MimeTypeUtils.getMimeTypeFromHeaders(request.getHeaders())
mimeType = ContentTypeHeaderParser.getMimeType(request.getHeaders())
.orElseGet(() ->
Optional.ofNullable(ContentType.parseLenient(entity.getContentType()))
.orElse(ContentType.DEFAULT_TEXT).getMimeType()
);
binaryContent = entity instanceof ByteArrayEntity;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream((int) entity.getContentLength()))
{
// https://github.com/apache/httpcomponents-client/commit/09cefc2b8970eea56d81b1a886d9bb769a48daf3
Expand All @@ -75,25 +80,37 @@ public void process(HttpRequest request, EntityDetails entityDetails, HttpContex
}
}
}
attachApiMessage("Request: " + request, request.getHeaders(), body, mimeType, -1);
String curlCommand = null;
try
{
curlCommand = CurlUtils.buildCurlCommand(request, mimeType, body, binaryContent);
}
catch (URISyntaxException e)
{
LOGGER.error("Error is occurred on building cURL command", e);
}
attachApiMessage("Request: " + request, request.getHeaders(), body, mimeType, -1, curlCommand);
}

@Override
public void handle(HttpResponse response) throws IOException
public void handle(HttpResponse response)
{
Header[] headers = response.getResponseHeaders();
String attachmentTitle = String.format("Response: %s %s", response.getMethod(), response.getFrom());
String mimeType = MimeTypeUtils.getMimeTypeFromHeadersWithDefault(headers);
attachApiMessage(attachmentTitle, headers, response.getResponseBody(), mimeType, response.getStatusCode());
String mimeType = ContentTypeHeaderParser.getMimeTypeFromHeadersWithDefault(headers);
attachApiMessage(attachmentTitle, headers, response.getResponseBody(), mimeType,
response.getStatusCode(), null);
}

private void attachApiMessage(String title, Header[] headers, byte[] body, String mimeType, int statusCode)
private void attachApiMessage(String title, Header[] headers, byte[] body, String mimeType,
int statusCode, String curlCommand)
{
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("headers", headers);
dataMap.put("body", body != null ? new String(body, StandardCharsets.UTF_8) : null);
dataMap.put("bodyContentType", mimeType);
dataMap.put("statusCode", statusCode);
dataMap.put("curlCommand", StringEscapeUtils.escapeHtml4(curlCommand));

attachmentPublisher.publishAttachment("/org/vividus/http/attachment/api-message.ftl", dataMap, title);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,23 @@
.panel-heading a.collapsed:after {
content:"\F105";
}

.container {
position: relative;
}
.copy-button {
position: absolute;
top: 0px;
right: 15px;
}
#copy-toast {
position: absolute;
display: none;
top: 35px;
right: 0px;
font-size: 13px;
background-color: #666362;
color: #fff;
}
</style>

<div class="panel-group" id="accordion">
Expand Down Expand Up @@ -94,6 +110,25 @@
</div>
</div>
</#if>

<#if curlCommand??>
<div class="panel panel-info">
abudevich marked this conversation as resolved.
Show resolved Hide resolved
<div class="panel-heading">
<h4 class="panel-title toggleable">
<a data-toggle="collapse" data-target="#collapse-curl" href="#collapse-curl" class="collapsed">cURL command</a>
</h4>
</div>
<div id="collapse-curl" class="panel-collapse collapse">
<div class="container">
<pre><code class="language-shell">${curlCommand}</code></pre>
<button class="copy-button" title="Copy to clipboard" onclick="copyCurlCommand()">
<img src="../../webjars/bootstrap/3.4.1/fonts/clipboard.svg">
abudevich marked this conversation as resolved.
Show resolved Hide resolved
</button>
<span id="copy-toast">Copied!</span>
</div>
</div>
</div>
</#if>
</div>

<script src="../../webjars/jquery/3.6.4/jquery.min.js"></script>
Expand All @@ -110,6 +145,15 @@
hljs.highlightElement(e);
});
});

function copyCurlCommand() {
var text = document.querySelector('#collapse-curl code').textContent;
navigator.clipboard.writeText(text);
document.getElementById("copy-toast").style.display = "inline";
setTimeout( function() {
document.getElementById("copy-toast").style.display = "none";
}, 1000);
}
</script>
</body>
</html>
Loading
Loading