Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit 8d946fb

Browse files
committed
feat: add function for GraphQLTestTemplate to upload files using Upload scalar
1 parent 65490fc commit 8d946fb

File tree

6 files changed

+155
-2
lines changed

6 files changed

+155
-2
lines changed

graphql-spring-boot-test/src/main/java/com/graphql/spring/boot/test/GraphQLTestTemplate.java

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
import lombok.NonNull;
1717
import org.springframework.beans.factory.annotation.Value;
1818
import org.springframework.boot.test.web.client.TestRestTemplate;
19+
import org.springframework.core.io.ClassPathResource;
1920
import org.springframework.core.io.Resource;
2021
import org.springframework.core.io.ResourceLoader;
2122
import org.springframework.http.HttpEntity;
2223
import org.springframework.http.HttpHeaders;
2324
import org.springframework.http.HttpMethod;
2425
import org.springframework.http.ResponseEntity;
2526
import org.springframework.lang.Nullable;
27+
import org.springframework.util.LinkedMultiValueMap;
2628
import org.springframework.util.MultiValueMap;
2729
import org.springframework.util.StreamUtils;
2830

@@ -239,13 +241,23 @@ public GraphQLResponse perform(
239241
ObjectNode variables,
240242
List<String> fragmentResources)
241243
throws IOException {
244+
String payload = getPayload(graphqlResource, operationName, variables, fragmentResources);
245+
return post(payload);
246+
}
247+
248+
/** Generate GraphQL payload, which consist of 3 elements: query, operationName and variables */
249+
private String getPayload(
250+
String graphqlResource,
251+
String operationName,
252+
ObjectNode variables,
253+
List<String> fragmentResources)
254+
throws IOException {
242255
StringBuilder sb = new StringBuilder();
243256
for (String fragmentResource : fragmentResources) {
244257
sb.append(loadQuery(fragmentResource));
245258
}
246259
String graphql = sb.append(loadQuery(graphqlResource)).toString();
247-
String payload = createJsonQuery(graphql, operationName, variables);
248-
return post(payload);
260+
return createJsonQuery(graphql, operationName, variables);
249261
}
250262

251263
/**
@@ -279,6 +291,76 @@ public GraphQLResponse postMultipart(String query, String variables) {
279291
return postRequest(RequestFactory.forMultipart(query, variables, headers));
280292
}
281293

294+
/**
295+
* Handle the multipart files upload request to GraphQL servlet
296+
*
297+
* <p>In contrast with usual the GraphQL request with body as json payload (consist of query,
298+
* operationName and variables), multipart file upload request will use multipart/form-data body
299+
* with the following structure:
300+
*
301+
* <ul>
302+
* <li><b>operations</b> the payload that we used to use for the normal GraphQL request
303+
* <li><b>map</b> a map for referencing between one part of multi-part request and the
304+
* corresponding <i>Upload</i> element inside <i>variables</i>
305+
* <li>a consequence of upload files embedded into the multi-part request, keyed as numeric
306+
* number starting from 1, valued as File payload of usual multipart file upload
307+
* </ul>
308+
*
309+
* <p>Example uploading two files:
310+
*
311+
* <p>* Please note that we can't embed binary data into json. Clients library supporting graphql
312+
* file upload will set variable.files to null for every element inside the array, but each file
313+
* will be a part of multipart request. GraphQL Servlet will use <i>map</i> part to walk through
314+
* variables.files and validate the request in combination with other binary file parts
315+
*
316+
* <p>-------------- request beginning ---------------
317+
*
318+
* <p>operations: { "query": "mutation($files:[Upload]!) {uploadFiles(files:$files)}",
319+
* "operationName": "uploadFiles", "variables": { "files": [null, null] } }
320+
*
321+
* <p>-----------------------------------------------
322+
*
323+
* <p>map: { "1":["variables.files.0"], "2":["variables.files.1"] }
324+
*
325+
* <p>-----------------------------------------------
326+
*
327+
* <p>1: --file 1 binary code--
328+
*
329+
* <p>-----------------------------------------------
330+
*
331+
* <p>2: --file 2 binary code--
332+
*
333+
* <p>-------------- request end ---------------------
334+
*
335+
* @param graphqlResource path to the classpath resource containing the GraphQL query
336+
* @param variables the input variables for the GraphQL query
337+
* @param files ClassPathResource instance for each file that will be uploaded to GraphQL server.
338+
* When Spring RestTemplate processes the request, it will automatically produce a valid part
339+
* representing given file inside multipart request (including size, submittedFileName, etc.)
340+
* @return {@link GraphQLResponse} containing the result of query execution
341+
* @throws IOException if the resource cannot be loaded from the classpath
342+
*/
343+
public GraphQLResponse postFiles(
344+
String graphqlResource, ObjectNode variables, List<ClassPathResource> files)
345+
throws IOException {
346+
347+
MultiValueMap<String, Object> values = new LinkedMultiValueMap<>();
348+
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
349+
350+
for (int i = 0; i < files.size(); i++) {
351+
String valueKey = String.valueOf(i + 1); // map value and part index starts at 1
352+
map.add(valueKey, String.format("variables.files.%d", i));
353+
354+
values.add(valueKey, files.get(i));
355+
}
356+
357+
String payload = getPayload(graphqlResource, null, variables, Collections.emptyList());
358+
values.add("operations", payload);
359+
values.add("map", map);
360+
361+
return postRequest(RequestFactory.forMultipart(values, headers));
362+
}
363+
282364
/**
283365
* Performs a GraphQL request with the provided payload.
284366
*

graphql-spring-boot-test/src/main/java/com/graphql/spring/boot/test/RequestFactory.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.http.HttpHeaders;
55
import org.springframework.http.MediaType;
66
import org.springframework.util.LinkedMultiValueMap;
7+
import org.springframework.util.MultiValueMap;
78

89
class RequestFactory {
910

@@ -23,4 +24,10 @@ static HttpEntity<Object> forMultipart(String query, String variables, HttpHeade
2324
values.add("variables", forJson(variables, new HttpHeaders()));
2425
return new HttpEntity<>(values, headers);
2526
}
27+
28+
static HttpEntity<Object> forMultipart(
29+
MultiValueMap<String, Object> values, HttpHeaders headers) {
30+
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
31+
return new HttpEntity<>(values, headers);
32+
}
2633
}

graphql-spring-boot-test/src/test/java/com/graphql/spring/boot/test/GraphQLTestTemplateIntegrationTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package com.graphql.spring.boot.test;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.node.ArrayNode;
45
import com.fasterxml.jackson.databind.node.ObjectNode;
56
import com.graphql.spring.boot.test.beans.FooBar;
67
import graphql.GraphQLError;
78
import java.io.IOException;
9+
import java.util.Arrays;
810
import java.util.Collections;
11+
import java.util.List;
912
import java.util.UUID;
13+
import java.util.stream.Collectors;
1014
import org.junit.jupiter.api.BeforeEach;
1115
import org.junit.jupiter.api.DisplayName;
1216
import org.junit.jupiter.api.Test;
1317
import org.springframework.beans.factory.annotation.Autowired;
1418
import org.springframework.boot.test.context.SpringBootTest;
1519
import org.springframework.boot.test.web.client.TestRestTemplate;
20+
import org.springframework.core.io.ClassPathResource;
1621
import org.springframework.core.io.ResourceLoader;
1722
import org.springframework.http.HttpHeaders;
1823

@@ -26,9 +31,11 @@ class GraphQLTestTemplateIntegrationTest {
2631
private static final String QUERY_WITH_VARIABLES = "query-with-variables.graphql";
2732
private static final String COMPLEX_TEST_QUERY = "complex-query.graphql";
2833
private static final String MULTIPLE_QUERIES = "multiple-queries.graphql";
34+
private static final String UPLOAD_MUTATION = "upload-files.graphql";
2935
private static final String INPUT_STRING_VALUE = "input-value";
3036
private static final String INPUT_STRING_NAME = "input";
3137
private static final String INPUT_HEADER_NAME = "headerName";
38+
private static final String FILES_STRING_NAME = "files";
3239
private static final String TEST_HEADER_NAME = "x-test";
3340
private static final String TEST_HEADER_VALUE = String.valueOf(UUID.randomUUID());
3441
private static final String FOO = "FOO";
@@ -39,6 +46,7 @@ class GraphQLTestTemplateIntegrationTest {
3946
private static final String DATA_FIELD_OTHER_QUERY = "$.data.otherQuery";
4047
private static final String DATA_FIELD_QUERY_WITH_HEADER = "$.data.queryWithHeader";
4148
private static final String DATA_FIELD_DUMMY = "$.data.dummy";
49+
private static final String DATA_FILE_UPLOAD_FILES = "$.data.uploadFiles";
4250
private static final String OPERATION_NAME_WITH_VARIABLES = "withVariable";
4351
private static final String OPERATION_NAME_TEST_QUERY_1 = "testQuery1";
4452
private static final String OPERATION_NAME_TEST_QUERY_2 = "testQuery2";
@@ -224,4 +232,24 @@ void testPost() {
224232
.asString()
225233
.isEqualTo(TEST_HEADER_VALUE);
226234
}
235+
236+
@Test
237+
@DisplayName("Test perform with file uploads.")
238+
void testPerformWithFileUploads() throws IOException {
239+
// GIVEN
240+
final ObjectNode variables = objectMapper.createObjectNode();
241+
ArrayNode nodes = objectMapper.valueToTree(Arrays.asList(null, null));
242+
variables.putArray(FILES_STRING_NAME).addAll(nodes);
243+
244+
List<String> fileNames = List.of("multiple-queries.graphql", "simple-test-query.graphql");
245+
List<ClassPathResource> testUploadFiles =
246+
fileNames.stream().map(ClassPathResource::new).collect(Collectors.toList());
247+
// WHEN - THEN
248+
graphQLTestTemplate
249+
.postFiles(UPLOAD_MUTATION, variables, testUploadFiles)
250+
.assertThatNoErrorsArePresent()
251+
.assertThatField(DATA_FILE_UPLOAD_FILES)
252+
.asListOf(String.class)
253+
.isEqualTo(fileNames);
254+
}
227255
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.graphql.spring.boot.test.beans;
2+
3+
import graphql.kickstart.servlet.apollo.ApolloScalars;
4+
import graphql.kickstart.tools.GraphQLMutationResolver;
5+
import graphql.schema.DataFetchingEnvironment;
6+
import graphql.schema.GraphQLScalarType;
7+
import java.util.List;
8+
import java.util.stream.Collectors;
9+
import javax.servlet.http.Part;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.stereotype.Service;
12+
13+
@Service
14+
public class DummyMutation implements GraphQLMutationResolver {
15+
16+
@Bean
17+
private GraphQLScalarType getUploadScalar() {
18+
// since the test doesn't inject this built-in Scalar,
19+
// so we inject here for test run purpose
20+
return ApolloScalars.Upload;
21+
}
22+
23+
public List<String> uploadFiles(List<Part> files, DataFetchingEnvironment env) {
24+
List<Part> actualFiles = env.getArgument("files");
25+
return actualFiles.stream().map(Part::getSubmittedFileName).collect(Collectors.toList());
26+
}
27+
}

graphql-spring-boot-test/src/test/resources/test-schema.graphqls

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
scalar Upload
2+
13
type FooBar {
24
foo: String!
35
bar: String!
@@ -17,4 +19,8 @@ type Query {
1719
fooBar(foo: String, bar: String): FooBar!
1820
queryWithVariables(input: String!): String!
1921
queryWithHeader(headerName: String!): String
22+
}
23+
24+
type Mutation {
25+
uploadFiles(files: [Upload]!): [String!]
2026
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mutation($files: [Upload]!) {
2+
uploadFiles(files: $files)
3+
}

0 commit comments

Comments
 (0)