Skip to content

Commit

Permalink
[plugin-rest-api] Post executed requests in a curl syntax in allure a…
Browse files Browse the repository at this point in the history
…ttachments
  • Loading branch information
abudevich committed Jan 29, 2024
1 parent d1041c2 commit a3644a1
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 25 deletions.
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]);
}
}
110 changes: 110 additions & 0 deletions vividus-plugin-rest-api/src/main/java/org/vividus/http/CurlUtils.java
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 '@&lt;path_to_binary_content&gt;'");
return;
}
if (mimeType.contains("multipart"))
{
appendMultipartData(curlCommand, bodyAsString);
return;
}
curlCommand.append(END_OF_LINE).append("-d '").append(bodyAsString).append(SINGLE_QUOTE);
}

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("=@\"&lt;path-to-file&gt;").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,6 +18,7 @@

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;
Expand All @@ -30,6 +31,7 @@
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 +55,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 +79,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", 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">
<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">
</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

0 comments on commit a3644a1

Please sign in to comment.