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 18, 2024
1 parent 9bffd6a commit 53e80ec
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 13 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 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,101 @@
/*
* 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.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=\"(.+)\"\nContent-Type:.+\n\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,
Charset charset, byte[] body) throws URISyntaxException
{
StringBuilder curlCommand = new StringBuilder("curl ");
appendMethodAndUri(curlCommand, request);
appendHeaders(curlCommand, request.getHeaders());
if (body != null)
{
appendBody(curlCommand, mimeType, charset, body);
}
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)
{
String bodyAsString = new String(body, charset);
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("=@<path-to-file>").append(matcher.group(2)).append(DOUBLE_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);
}
}
});
}
}
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,8 @@

package org.vividus.http;

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

Expand All @@ -24,6 +26,7 @@
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
Expand All @@ -44,13 +47,28 @@ public static String getMimeTypeFromHeadersWithDefault(Header... headers)
}

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

public static Charset getCharsetFromHeaders(Header... headers)
{
Optional<HeaderElement> header = getContentTypeHeader(headers);
if (header.isPresent())
{
NameValuePair pair = header.get().getParameterByName("charset");
return pair == null ? StandardCharsets.UTF_8 : Charset.forName(pair.getValue());
}
return StandardCharsets.UTF_8;
}

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
@@ -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 @@ -52,6 +54,7 @@ public PublishingAttachmentInterceptor(IAttachmentPublisher attachmentPublisher)
public void process(HttpRequest request, EntityDetails entityDetails, HttpContext context)
{
byte[] body = null;
byte[] bodyForCurl = null;
String mimeType = null;
if (request instanceof HttpEntityContainer httpEntityContainer)
{
Expand All @@ -73,27 +76,42 @@ public void process(HttpRequest request, EntityDetails entityDetails, HttpContex
{
LOGGER.error("Error is occurred at HTTP message parsing", e);
}
bodyForCurl = entity instanceof ByteArrayEntity
? "<binary_content>".getBytes(StandardCharsets.UTF_8) : body;
}
}
attachApiMessage("Request: " + request, request.getHeaders(), body, mimeType, -1);
String curlCommand = null;
try
{
curlCommand = CurlUtils.buildCurlCommand(request, mimeType,
MimeTypeUtils.getCharsetFromHeaders(request.getHeaders()), bodyForCurl);
}
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());
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,14 @@
.panel-heading a.collapsed:after {
content:"\F105";
}
.container {
position: relative;
}
.copy-button {
position: absolute;
top: 0px;
right: 15px;
}
</style>

<div class="panel-group" id="accordion">
Expand Down Expand Up @@ -94,6 +101,24 @@
</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>
</div>
</div>
</div>
</#if>
</div>

<script src="../../webjars/jquery/3.6.4/jquery.min.js"></script>
Expand All @@ -110,6 +135,11 @@
hljs.highlightElement(e);
});
});
function copyCurlCommand() {
var text = document.querySelector('#collapse-curl code').textContent;
navigator.clipboard.writeText(text);
}
</script>
</body>
</html>
Loading

0 comments on commit 53e80ec

Please sign in to comment.