Skip to content

Add multipart request support #430

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

Closed
wants to merge 2 commits into from
Closed
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 @@ -73,6 +73,11 @@ public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
});
}

@Override
public Mono<GraphQlResponse> executeFileUpload(GraphQlRequest request) {
throw new UnsupportedOperationException("File upload is not supported");
}

private ExecutionGraphQlRequest toExecutionRequest(GraphQlRequest request) {
return new DefaultExecutionGraphQlRequest(
request.getDocument(), request.getOperationName(), request.getVariables(), request.getExtensions(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
.executeSubscription()
.cast(GraphQlResponse.class);
}
};

@Override
public Mono<GraphQlResponse> executeFileUpload(GraphQlRequest request) {
throw new UnsupportedOperationException("File upload is not supported");
}
};
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@

import java.lang.reflect.Type;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -38,6 +34,7 @@
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.ResponseError;
import org.springframework.graphql.client.GraphQlTransport;
import org.springframework.graphql.client.MultipartClientGraphQlRequest;
import org.springframework.graphql.support.DefaultGraphQlRequest;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -127,7 +124,9 @@ private final class DefaultRequest implements Request<DefaultRequest> {

private final Map<String, Object> extensions = new LinkedHashMap<>();

private DefaultRequest(String document) {
private final Map<String, Object> fileVariables = new LinkedHashMap<>();

private DefaultRequest(String document) {
Assert.notNull(document, "`document` is required");
this.document = document;
}
Expand All @@ -144,7 +143,19 @@ public DefaultRequest variable(String name, @Nullable Object value) {
return this;
}

@Override
@Override
public DefaultRequest fileVariable(String name, Object value) {
this.fileVariables.put(name, value);
return this;
}

@Override
public DefaultRequest fileVariables(Map<String, Object> variables) {
this.fileVariables.putAll(variables);
return this;
}

@Override
public DefaultRequest extension(String name, Object value) {
this.extensions.put(name, value);
return this;
Expand All @@ -156,6 +167,16 @@ public Response execute() {
return transport.execute(request()).map(response -> mapResponse(response, request())).block(responseTimeout);
}

@Override
public Response executeFileUpload() {
return transport.executeFileUpload(requestFileUpload()).map(response -> mapResponse(response, requestFileUpload())).block(responseTimeout);
}

@Override
public void executeFileUploadAndVerify() {
executeFileUpload().path("$.errors").pathDoesNotExist();
}

@Override
public void executeAndVerify() {
execute().path("$.errors").pathDoesNotExist();
Expand All @@ -170,6 +191,10 @@ private GraphQlRequest request() {
return new DefaultGraphQlRequest(this.document, this.operationName, this.variables, this.extensions);
}

private GraphQlRequest requestFileUpload() {
return new MultipartClientGraphQlRequest(this.document, this.operationName, this.variables, this.extensions, new HashMap<>(), this.fileVariables);
}

private DefaultResponse mapResponse(GraphQlResponse response, GraphQlRequest request) {
return new DefaultResponse(response, errorFilter, assertDecorator(request), jsonPathConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.springframework.graphql.client.GraphQlClient;
import reactor.core.publisher.Flux;

import org.springframework.core.ParameterizedTypeReference;
Expand Down Expand Up @@ -149,6 +151,10 @@ interface Request<T extends Request<T>> {
*/
T variable(String name, @Nullable Object value);

T fileVariable(String name, Object value);

T fileVariables(Map<String, Object> variables);

/**
* Add a value for a protocol extension.
* @param name the protocol extension name
Expand All @@ -166,7 +172,9 @@ interface Request<T extends Request<T>> {
*/
Response execute();

/**
void executeFileUploadAndVerify();

/**
* Execute the GraphQL request and verify the response contains no errors.
*/
void executeAndVerify();
Expand All @@ -180,6 +188,8 @@ interface Request<T extends Request<T>> {
*/
Subscription executeSubscription();

Response executeFileUpload();

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import java.util.Collections;
import java.util.Map;

import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand All @@ -30,6 +33,8 @@
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.Assert;

import static org.springframework.graphql.client.MultipartBodyCreator.convertRequestToMultipartData;

/**
* {@code GraphQlTransport} for GraphQL over HTTP via {@link WebTestClient}.
*
Expand Down Expand Up @@ -70,6 +75,25 @@ public Mono<GraphQlResponse> execute(GraphQlRequest request) {
return Mono.just(response);
}

@Override
public Mono<GraphQlResponse> executeFileUpload(GraphQlRequest request) {

Map<String, Object> responseMap = this.webTestClient.post()
.contentType(MediaType.MULTIPART_FORM_DATA)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromMultipartData(convertRequestToMultipartData(request)))
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
.expectBody(MAP_TYPE)
.returnResult()
.getResponseBody();

responseMap = (responseMap != null ? responseMap : Collections.emptyMap());
GraphQlResponse response = GraphQlTransport.createResponse(responseMap);
return Mono.just(response);
}

@Override
public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
throw new UnsupportedOperationException("Subscriptions not supported over HTTP");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.springframework.graphql.test.tester;

import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

public class HttpGraphQlTesterTests {

private static final String DOCUMENT = "{ Mutation }";

@Test
void shouldSendOneFile() {
MultipartHttpBuilderSetup testerSetup = new MultipartHttpBuilderSetup();

HttpGraphQlTester.Builder<?> builder = testerSetup.initBuilder();
HttpGraphQlTester tester = builder.build();
tester.document(DOCUMENT)
.variable("existingVar", "itsValue")
.fileVariable("fileInput", new ClassPathResource("/foo.txt"))
.executeFileUpload();
assertThat(testerSetup.getWebGraphQlRequest().getVariables().get("existingVar")).isEqualTo("itsValue");
assertThat(testerSetup.getWebGraphQlRequest().getVariables().get("fileInput")).isNotNull();
assertThat(((FilePart)testerSetup.getWebGraphQlRequest().getVariables().get("fileInput")).filename()).isEqualTo("foo.txt");
}

@Test
void shouldSendOneCollectionOfFiles() {
MultipartHttpBuilderSetup testerSetup = new MultipartHttpBuilderSetup();

HttpGraphQlTester.Builder<?> builder = testerSetup.initBuilder();
HttpGraphQlTester tester = builder.build();
List<ClassPathResource> resources = new ArrayList<>();
resources.add(new ClassPathResource("/foo.txt"));
resources.add(new ClassPathResource("/bar.txt"));
tester.document(DOCUMENT)
.variable("existingVar", "itsValue")
.fileVariable("filesInput", resources)
.executeFileUpload();
assertThat(testerSetup.getWebGraphQlRequest().getVariables().get("existingVar")).isEqualTo("itsValue");
assertThat(testerSetup.getWebGraphQlRequest().getVariables().get("filesInput")).isNotNull();
assertThat(((Collection<FilePart>)testerSetup.getWebGraphQlRequest().getVariables().get("filesInput")).size()).isEqualTo(2);
assertThat(((Collection<FilePart>)testerSetup.getWebGraphQlRequest().getVariables().get("filesInput")).stream().map(filePart -> filePart.filename()).collect(Collectors.toSet())).contains("foo.txt", "bar.txt");
}

private static class MultipartHttpBuilderSetup extends WebGraphQlTesterBuilderTests.WebBuilderSetup {

@Override
public HttpGraphQlTester.Builder<?> initBuilder() {
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler());
RouterFunction<ServerResponse> routerFunction = route().POST("/**", handler::handleMultipartRequest).build();
return HttpGraphQlTester.builder(WebTestClient.bindToRouterFunction(routerFunction).configureClient());
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ private interface TesterBuilderSetup {
}


private static class WebBuilderSetup implements TesterBuilderSetup {
static class WebBuilderSetup implements TesterBuilderSetup {

@Nullable
private WebGraphQlRequest request;
Expand Down
1 change: 1 addition & 0 deletions spring-graphql-test/src/test/resources/bar.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello from bar here!
1 change: 1 addition & 0 deletions spring-graphql-test/src/test/resources/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello here!
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ protected GraphQlClient buildGraphQlClient(GraphQlTransport transport) {
}

return new DefaultGraphQlClient(
this.documentSource, createExecuteChain(transport), createExecuteSubscriptionChain(transport));
this.documentSource, createExecuteChain(transport), createFileUploadChain(transport), createExecuteSubscriptionChain(transport));
}

/**
Expand All @@ -195,7 +195,18 @@ private Chain createExecuteChain(GraphQlTransport transport) {
.orElse(chain);
}

private SubscriptionChain createExecuteSubscriptionChain(GraphQlTransport transport) {
private Chain createFileUploadChain(GraphQlTransport transport) {

Chain chain = request -> transport.executeFileUpload(request).map(response ->
new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));

return this.interceptors.stream()
.reduce(GraphQlClientInterceptor::andThen)
.map(interceptor -> (Chain) (request) -> interceptor.intercept(request, chain))
.orElse(chain);
}

private SubscriptionChain createExecuteSubscriptionChain(GraphQlTransport transport) {

SubscriptionChain chain = request -> transport.executeSubscription(request)
.map(response -> new DefaultClientGraphQlResponse(request, response, getEncoder(), getDecoder()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* @author Rossen Stoyanchev
* @since 1.0.0
*/
final class DefaultClientGraphQlRequest extends DefaultGraphQlRequest implements ClientGraphQlRequest {
class DefaultClientGraphQlRequest extends DefaultGraphQlRequest implements ClientGraphQlRequest {

private final Map<String, Object> attributes = new ConcurrentHashMap<>();

Expand Down
Loading