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

Issue a PUT request after a GET request responding with 202 and a Location header #2099

Merged
merged 7 commits into from
Aug 25, 2020
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
11 changes: 4 additions & 7 deletions core/src/main/java/io/cucumber/core/plugin/PublishFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@
import io.cucumber.plugin.event.EventPublisher;

import java.io.IOException;
import java.net.URI;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map;

import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_URL_PROPERTY_NAME;
import static io.cucumber.core.options.CurlOption.HttpMethod.PUT;

public final class PublishFormatter implements ConcurrentEventListener, ColorAware {

/**
* Where to publishes messages by default
*/
public static final String DEFAULT_CUCUMBER_MESSAGE_STORE_URL = "https://messages.cucumber.io/api/reports";
public static final String DEFAULT_CUCUMBER_MESSAGE_STORE_URL = "https://messages.cucumber.io/api/reports -X GET";

private final UrlReporter urlReporter = new UrlReporter(System.err);
private final MessageFormatter delegate;
Expand Down Expand Up @@ -50,10 +47,10 @@ public void setMonochrome(boolean monochrome) {
private static CurlOption createCurlOption(String token) {
Map<String, String> properties = CucumberProperties.create();
String url = properties.getOrDefault(PLUGIN_PUBLISH_URL_PROPERTY_NAME, DEFAULT_CUCUMBER_MESSAGE_STORE_URL);
if (token == null) {
return CurlOption.create(PUT, URI.create(url));
if (token != null) {
url = url + String.format(" -H 'Authorization: Bearer %s'", token);
}
return CurlOption.create(PUT, URI.create(url), new SimpleEntry<>("Authorization", "Bearer " + token));
return CurlOption.parse(url);
}

}
39 changes: 26 additions & 13 deletions core/src/main/java/io/cucumber/core/plugin/UrlOutputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,49 @@ public void flush() throws IOException {
@Override
public void close() throws IOException {
tempOutputStream.close();
URL url = sendRequest(option.getUri().toURL(), option.getMethod());
if (urlReporter != null) {
urlReporter.report(url);
}
}

URL url = option.getUri().toURL();
private URL sendRequest(URL url, CurlOption.HttpMethod method) throws IOException {
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
for (Entry<String, String> header : option.getHeaders()) {
urlConnection.setRequestProperty(header.getKey(), header.getValue());
}
urlConnection.setInstanceFollowRedirects(true);
urlConnection.setRequestMethod(option.getMethod().name());
urlConnection.setDoOutput(true);
Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
try (OutputStream outputStream = urlConnection.getOutputStream()) {
Files.copy(temp, outputStream);
handleResponse(urlConnection, requestHeaders);
}
if (urlReporter != null) {
urlReporter.report(urlConnection.getURL());
urlConnection.setInstanceFollowRedirects(true);
urlConnection.setRequestMethod(method.name());
if (method == CurlOption.HttpMethod.GET) {
throwExceptionIfUnsuccessful(urlConnection, requestHeaders);
String location = urlConnection.getHeaderField("Location");
if (urlConnection.getResponseCode() == 202 && location != null) {
return sendRequest(new URL(location), CurlOption.HttpMethod.PUT);
}
} else {
urlConnection.setDoOutput(true);
try (OutputStream outputStream = urlConnection.getOutputStream()) {
Files.copy(temp, outputStream);
throwExceptionIfUnsuccessful(urlConnection, requestHeaders);
}
}
return urlConnection.getURL();
}

private static void handleResponse(HttpURLConnection urlConnection, Map<String, List<String>> requestHeaders)
private static void throwExceptionIfUnsuccessful(
HttpURLConnection urlConnection, Map<String, List<String>> requestHeaders
)
throws IOException {
Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
int responseCode = urlConnection.getResponseCode();
boolean success = 200 <= responseCode && responseCode < 300;
boolean unsuccessful = responseCode >= 400;

InputStream inputStream = urlConnection.getErrorStream() != null ? urlConnection.getErrorStream()
: urlConnection.getInputStream();
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) {
String responseBody = br.lines().collect(Collectors.joining(System.lineSeparator()));
if (!success) {
if (unsuccessful) {
String method = urlConnection.getRequestMethod();
URL url = urlConnection.getURL();
throw createCurlLikeException(method, url, requestHeaders, responseHeaders, responseBody);
Expand Down
54 changes: 44 additions & 10 deletions core/src/test/java/io/cucumber/core/plugin/UrlOutputStreamTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

Expand Down Expand Up @@ -45,12 +46,12 @@ void throws_exception_for_500_status(Vertx vertx, VertxTestContext testContext)
String requestBody = "hello";
TestServer testServer = new TestServer(port, testContext, requestBody, HttpMethod.PUT, null, null, 500,
"Oh noes");
CurlOption option = CurlOption.parse(format("http://localhost:%d", port));
CurlOption option = CurlOption.parse(format("http://localhost:%d/s3", port));

verifyRequest(option, testServer, vertx, testContext, requestBody);
assertThat(testContext.awaitCompletion(TIMEOUT_SECONDS, TimeUnit.SECONDS), is(true));
assertThat(exception.getMessage(), equalTo("HTTP request failed:\n" +
"> PUT http://localhost:" + port + "\n" +
"> PUT http://localhost:" + port + "/s3\n" +
"< HTTP/1.1 500 Internal Server Error\n" +
"< transfer-encoding: chunked\n" +
"Oh noes"));
Expand All @@ -75,16 +76,33 @@ private void verifyRequest(
}

@Test
void follows_307_temporary_redirects(Vertx vertx, VertxTestContext testContext) throws InterruptedException {
void it_sends_the_body_twice_for_307_redirect_with_put(Vertx vertx, VertxTestContext testContext) throws Exception {
String requestBody = "hello";
TestServer testServer = new TestServer(port, testContext, requestBody, HttpMethod.PUT, null, null, 200, "");
TestServer testServer = new TestServer(port, testContext, requestBody + requestBody, HttpMethod.PUT, null, null,
200, "");
CurlOption url = CurlOption.parse(format("http://localhost:%d/redirect", port));
verifyRequest(url, testServer, vertx, testContext, requestBody);

assertThat(testContext.awaitCompletion(TIMEOUT_SECONDS, TimeUnit.SECONDS), is(true));
}

@Test
void it_sends_the_body_once_for_202_and_location_with_get(Vertx vertx, VertxTestContext testContext)
throws Exception {
String requestBody = "hello";
TestServer testServer = new TestServer(port, testContext, requestBody, HttpMethod.PUT, null, null, 200, "");
CurlOption url = CurlOption.parse(format("http://localhost:%d/accept -X GET", port));
verifyRequest(url, testServer, vertx, testContext, requestBody);

assertThat(testContext.awaitCompletion(TIMEOUT_SECONDS, TimeUnit.SECONDS), is(true));
if (exception != null) {
throw exception;
}
assertThat(testServer.receivedBody.toString("utf-8"), is(equalTo(requestBody)));
}

@Test
@Disabled
void throws_exception_for_307_temporary_redirect_without_location(Vertx vertx, VertxTestContext testContext)
throws InterruptedException {
String requestBody = "hello";
Expand Down Expand Up @@ -144,6 +162,7 @@ public static class TestServer extends AbstractVerticle {
private final String expectedContentType;
private final int statusCode;
private final String responseBody;
private final Buffer receivedBody = Buffer.buffer(0);

public TestServer(
int port,
Expand All @@ -167,31 +186,46 @@ public TestServer(
@Override
public void start(Promise<Void> startPromise) {
Router router = Router.router(vertx);
router.route("/accept").handler(ctx -> {
ctx.request().handler(receivedBody::appendBuffer);

String contentLengthString = ctx.request().getHeader("Content-Length");
int contentLength = contentLengthString == null ? 0 : Integer.parseInt(contentLengthString);
if (contentLength > 0) {
ctx.response().setStatusCode(500);
ctx.response().end("Unexpected body");
} else {
ctx.response().setStatusCode(202);
ctx.response().headers().add("Location", "http://localhost:" + port + "/s3");
ctx.response().end();
}
});
router.route("/redirect").handler(ctx -> {
ctx.request().handler(receivedBody::appendBuffer);
ctx.response().setStatusCode(307);
ctx.response().headers().add("Location", "http://localhost:" + port);
ctx.response().headers().add("Location", "http://localhost:" + port + "/s3");
ctx.response().end();
});
router.route("/redirect-no-location").handler(ctx -> {
ctx.request().handler(receivedBody::appendBuffer);
ctx.response().setStatusCode(307);
ctx.response().end();
});

router.route().handler(ctx -> {
router.route("/s3").handler(ctx -> {
ctx.response().setStatusCode(statusCode);
testContext.verify(() -> {
assertThat(ctx.request().method(), is(equalTo(expectedMethod)));
assertThat(ctx.request().query(), is(equalTo(expectedQuery)));
assertThat(ctx.request().getHeader("Content-Type"), is(equalTo(expectedContentType)));

Buffer body = Buffer.buffer(0);
ctx.request().handler(body::appendBuffer);
ctx.request().handler(receivedBody::appendBuffer);
ctx.request().endHandler(e -> {
String receivedBody = body.toString("utf-8");
String receivedBodyString = receivedBody.toString("utf-8");
ctx.response().setChunked(true);
ctx.response().write(responseBody);
ctx.response().end();
testContext.verify(() -> assertThat(receivedBody, is(equalTo(expectedBody))));
testContext.verify(() -> assertThat(receivedBodyString, is(equalTo(expectedBody))));
});
});
});
Expand Down
38 changes: 19 additions & 19 deletions core/src/test/java/io/cucumber/core/runner/CachingGlueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
import io.cucumber.datatable.TableCellByTypeTransformer;
import io.cucumber.datatable.TableEntryByTypeTransformer;
import io.cucumber.docstring.DocStringType;
import org.junit.jupiter.api.Test;
import io.cucumber.messages.Messages;
import io.cucumber.plugin.event.EventHandler;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Type;
import java.net.URI;
Expand Down Expand Up @@ -462,24 +462,24 @@ void scenario_scoped_hooks_have_higher_order() {

assertThat(hooks, contains(hookDefinition2, hookDefinition1, hookDefinition3));
}
@Test
public void emits_hook_messages_to_bus() {
List<Messages.Envelope> events = new ArrayList<>();
EventHandler<Messages.Envelope> messageEventHandler = e -> events.add(e);
EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID);
bus.registerHandlerFor(Messages.Envelope.class, messageEventHandler);
CachingGlue glue = new CachingGlue(bus);
glue.addBeforeHook(new MockedScenarioScopedHookDefinition());
glue.addAfterHook(new MockedScenarioScopedHookDefinition());
glue.addBeforeStepHook(new MockedScenarioScopedHookDefinition());
glue.addAfterStepHook(new MockedScenarioScopedHookDefinition());

glue.prepareGlue(stepTypeRegistry);
assertThat(events.size(), is(4));

@Test
public void emits_hook_messages_to_bus() {

List<Messages.Envelope> events = new ArrayList<>();
EventHandler<Messages.Envelope> messageEventHandler = e -> events.add(e);

EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID);
bus.registerHandlerFor(Messages.Envelope.class, messageEventHandler);
CachingGlue glue = new CachingGlue(bus);

glue.addBeforeHook(new MockedScenarioScopedHookDefinition());
glue.addAfterHook(new MockedScenarioScopedHookDefinition());
glue.addBeforeStepHook(new MockedScenarioScopedHookDefinition());
glue.addAfterStepHook(new MockedScenarioScopedHookDefinition());

glue.prepareGlue(stepTypeRegistry);
assertThat(events.size(), is(4));
}

private static class MockedScenarioScopedStepDefinition extends StubStepDefinition implements ScenarioScoped {
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,12 @@
<new>.*SipozeKe.*</new>
<justification>Restore uppercase Creole keyword in Gherkin 15.0.2</justification>
</item>
<item>
<code>java.field.constantValueChanged</code>
<old>field io.cucumber.core.plugin.PublishFormatter.DEFAULT_CUCUMBER_MESSAGE_STORE_URL</old>
<justification>https://github.com/cucumber/cucumber-jvm/pull/2099</justification>
<newValue>https://messages.cucumber.io/api/reports -X GET</newValue>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
Expand Down