From 4dbde0cce015ef819f915f3a2a97c9c95ceafed2 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Mon, 29 Jun 2020 17:00:42 -0500 Subject: [PATCH] Add and test better support for uploading files to multiple variables. --- Sources/Apollo/RequestCreator.swift | 33 +++++--- Tests/ApolloTests/RequestCreatorTests.swift | 88 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) diff --git a/Sources/Apollo/RequestCreator.swift b/Sources/Apollo/RequestCreator.swift index 022b3fb8ff..5450f74220 100644 --- a/Sources/Apollo/RequestCreator.swift +++ b/Sources/Apollo/RequestCreator.swift @@ -107,15 +107,15 @@ extension RequestCreator { // Make sure all fields for files are set to null, or the server won't look // for the files in the rest of the form data - let fieldsForFiles = Set(files.map { $0.fieldName }) + let fieldsForFiles = Set(files.map { $0.fieldName }).sorted() var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers) var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() for fieldName in fieldsForFiles { if let value = variables[fieldName], let arrayValue = value as? [JSONEncodable] { - let updatedArray: [JSONEncodable?] = arrayValue.map { _ in nil } - variables.updateValue(updatedArray, forKey: fieldName) + let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } + variables.updateValue(arrayOfNils, forKey: fieldName) } else { variables.updateValue(nil, forKey: fieldName) } @@ -125,20 +125,33 @@ extension RequestCreator { let operationData = try serializationFormat.serialize(value: fields) formData.appendPart(data: operationData, name: "operations") + // If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name. var map = [String: [String]]() - if files.count == 1 { - let firstFile = files.first! - map["0"] = ["variables.\(firstFile.fieldName)"] - } else { - for (index, file) in files.enumerated() { - map["\(index)"] = ["variables.\(file.fieldName).\(index)"] + var currentIndex = 0 + + var sortedFiles = [GraphQLFile]() + for fieldName in fieldsForFiles { + let filesForField = files.filter { $0.fieldName == fieldName } + if filesForField.count == 1 { + let firstFile = filesForField.first! + map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"] + sortedFiles.append(firstFile) + currentIndex += 1 + } else { + for (index, file) in filesForField.enumerated() { + map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"] + sortedFiles.append(file) + currentIndex += 1 + } } } + + assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.") let mapData = try serializationFormat.serialize(value: map) formData.appendPart(data: mapData, name: "map") - for (index, file) in files.enumerated() { + for (index, file) in sortedFiles.enumerated() { formData.appendPart(inputStream: try file.generateInputStream(), contentLength: file.contentLength, name: "\(index)", diff --git a/Tests/ApolloTests/RequestCreatorTests.swift b/Tests/ApolloTests/RequestCreatorTests.swift index 602b43a45b..ad5258a340 100644 --- a/Tests/ApolloTests/RequestCreatorTests.swift +++ b/Tests/ApolloTests/RequestCreatorTests.swift @@ -287,6 +287,94 @@ Bravo file content. } } + func testMultipleFilesWithMultipleFieldsWithApolloRequestCreator() throws { + let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt") + let alphaFile = try GraphQLFile(fieldName: "uploads", + originalName: "a.txt", + mimeType: "text/plain", + fileURL: alphaFileURL) + + let betaFileURL = self.fileURLForFile(named: "b", extension: "txt") + let betaFile = try GraphQLFile(fieldName: "uploads", + originalName: "b.txt", + mimeType: "text/plain", + fileURL: betaFileURL) + + let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt") + let charlieFile = try GraphQLFile(fieldName: "secondField", + originalName: "c.txt", + mimeType: "text/plain", + fileURL: charlieFileUrl) + + let data = try apolloRequestCreator.requestMultipartFormData( + for: HeroNameQuery(), + files: [alphaFile, betaFile, charlieFile], + sendOperationIdentifiers: false, + serializationFormat: JSONSerializationFormat.self, + manualBoundary: "TEST.BOUNDARY" + ) + + let stringToCompare = try self.string(from: data) + + if JSONSerialization.dataCanBeSorted() { + let expectedString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="operations" + + {"operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}} + --TEST.BOUNDARY + Content-Disposition: form-data; name="map" + + {"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]} + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + XCTAssertEqual(stringToCompare, expectedString) + } else { + // Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly. + let endString = """ + --TEST.BOUNDARY + Content-Disposition: form-data; name="0"; filename="c.txt" + Content-Type: text/plain + + Charlie file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="1"; filename="a.txt" + Content-Type: text/plain + + Alpha file content. + + --TEST.BOUNDARY + Content-Disposition: form-data; name="2"; filename="b.txt" + Content-Type: text/plain + + Bravo file content. + + --TEST.BOUNDARY-- + """ + self.checkString(stringToCompare, includes: endString) + } + } + + func testRequestBodyWithApolloRequestCreator() { let query = HeroNameQuery() let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false)