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

Add and test errors instead of nil failures for creating a GraphQL file and/or stream #1089

Merged
merged 2 commits into from
Mar 18, 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
8 changes: 8 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
9B0E471E240B239D0093BDA7 /* ASTEnumValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0E471D240B239D0093BDA7 /* ASTEnumValue.swift */; };
9B1A38532332AF6F00325FB4 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1A38522332AF6F00325FB4 /* String+SHA.swift */; };
9B1CCDD92360F02C007C9032 /* Bundle+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */; };
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */; };
9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */; };
9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C85235F8125004C426D /* CLIDownloader.swift */; };
9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */; };
9B518C8D235F8B9E004C426D /* CLIDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */; };
Expand Down Expand Up @@ -360,6 +362,8 @@
9B0E471D240B239D0093BDA7 /* ASTEnumValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTEnumValue.swift; sourceTree = "<group>"; };
9B1A38522332AF6F00325FB4 /* String+SHA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = "<group>"; };
9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Helpers.swift"; sourceTree = "<group>"; };
9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFileTests.swift; sourceTree = "<group>"; };
9B21FD762422C8CC00998B5C /* TestFileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFileHelper.swift; sourceTree = "<group>"; };
9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = "<group>"; };
9B518C85235F8125004C426D /* CLIDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloader.swift; sourceTree = "<group>"; };
9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloaderTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -721,6 +725,7 @@
children = (
C3279FC52345233000224790 /* TestCustomRequestCreator.swift */,
9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */,
9B21FD762422C8CC00998B5C /* TestFileHelper.swift */,
);
name = TestHelpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1150,6 +1155,7 @@
9B78C71B2326E859000C8C32 /* ErrorGenerationTests.swift */,
9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */,
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */,
9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */,
9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */,
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */,
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */,
Expand Down Expand Up @@ -2013,6 +2019,8 @@
9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */,
9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */,
9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */,
9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */,
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */,
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */,
Expand Down
45 changes: 30 additions & 15 deletions Sources/Apollo/GraphQLFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ public struct GraphQLFile {
public let data: Data?
public let fileURL: URL?
public let contentLength: UInt64

public enum GraphQLFileError: Error, LocalizedError {
case couldNotCreateInputStream
case couldNotGetFileSize(fileURL: URL)

public var errorDescription: String? {
switch self {
case .couldNotCreateInputStream:
return "An input stream could not be created from either the passed-in file URL or data. Please check that you've passed at least one of these, and that for files you have proper permission to stream data."
case .couldNotGetFileSize(let fileURL):
return "Apollo could not get the file size for the file at \(fileURL). This likely indicates either a) The file is not at that URL or b) a permissions issue."
}
}
}

/// A convenience constant for declaring your mimetype is octet-stream.
public static let octetStreamMimeType = "application/octet-stream"
Expand All @@ -31,46 +45,47 @@ public struct GraphQLFile {
self.contentLength = UInt64(data.count)
}

/// Failable convenience initializer for files in the filesystem
/// Will return `nil` if the file URL cannot be used to create an `InputStream`, or if the file's size could not be determined.
/// Throwing convenience initializer for files in the filesystem
///
/// - Parameters:
/// - fieldName: The name of the field this file is being sent for
/// - originalName: The original name of the file
/// - mimeType: The mime type of the file to send to the server. Defaults to `GraphQLFile.octetStreamMimeType`.
/// - fileURL: The URL of the file to upload.
public init?(fieldName: String,
/// - Throws: If the file's size could not be determined
public init(fieldName: String,
originalName: String,
mimeType: String = GraphQLFile.octetStreamMimeType,
fileURL: URL) {
guard let contentLength = GraphQLFile.getFileSize(fileURL: fileURL) else {
return nil
}

fileURL: URL) throws {
self.contentLength = try GraphQLFile.getFileSize(fileURL: fileURL)
self.fieldName = fieldName
self.originalName = originalName
self.mimeType = mimeType
self.data = nil
self.fileURL = fileURL
self.contentLength = contentLength
}

/// Retrieves the InputStream
/// Uses either the data or the file URL to create an
/// `InputStream` that can be used to stream data into
/// a multipart-form.
///
/// - Returns: The created `InputStream`.
/// - Throws: If an input stream could not be created from either data or a file URL.
public func generateInputStream() throws -> InputStream {
if let data = data {
return InputStream(data: data)
} else if let fileURL = fileURL, let inputStream = InputStream(url: fileURL) {
} else if let fileURL = fileURL,
let inputStream = InputStream(url: fileURL) {
return inputStream
} else {
throw GraphQLFileError.couldNotCreateInputStream
}

throw GraphQLError("InputStream was not created.")
}

private static func getFileSize(fileURL: URL) -> UInt64? {
private static func getFileSize(fileURL: URL) throws -> UInt64 {
guard let fileSizeAttribute = try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size],
let fileSize = fileSizeAttribute as? NSNumber else {
return nil
throw GraphQLFileError.couldNotGetFileSize(fileURL: fileURL)
}

return fileSize.uint64Value
Expand Down
87 changes: 87 additions & 0 deletions Tests/ApolloTests/GraphQLFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// GraphQLFileTests.swift
// ApolloTests
//
// Created by Ellen Shapiro on 3/18/20.
// Copyright © 2020 Apollo GraphQL. All rights reserved.
//

import XCTest

@testable import Apollo

class GraphQLFileTests: XCTestCase {

func testCreatingFileWithKnownBadURLFails() {
let url = URL(fileURLWithPath: "/known/bad/path")
do {
_ = try GraphQLFile(fieldName: "test",
originalName: "test",
fileURL: url)
} catch {
switch error {
case GraphQLFile.GraphQLFileError.couldNotGetFileSize(let fileURL):
XCTAssertEqual(fileURL, url)
default:
XCTFail("Unexpected error creating file: \(error)")
}
}
}

func testCreatingFileWithKnownGoodURLSucceedsAndCreatesAndCanRecreateInputStream() throws {
let knownFileURL = TestFileHelper.testParentFolder()
.appendingPathComponent("a.txt")

let file = try GraphQLFile(fieldName: "test",
originalName: "test",
fileURL: knownFileURL)

let inputStream = try file.generateInputStream()

inputStream.open()
XCTAssertTrue(inputStream.hasBytesAvailable)
inputStream.close()

let inputStream2 = try file.generateInputStream()

inputStream2.open()
XCTAssertTrue(inputStream2.hasBytesAvailable)
inputStream2.close()
}

func testCreatingFileWithEmptyDataSucceedsAndCreatesInputStream() throws {
let data = Data()
XCTAssertTrue(data.isEmpty)

let file = GraphQLFile(fieldName: "test",
originalName: "test",
data: data)

let inputStream = try file.generateInputStream()

// Shouldn't have any bytes available if data is empty
inputStream.open()
XCTAssertFalse(inputStream.hasBytesAvailable)
inputStream.close()
}

func testCreatingFileWithNonEmptyDataSuccedsAndCreatesAndCanRecreateInputStream() throws {
let data = try XCTUnwrap("A test string".data(using: .utf8))
XCTAssertFalse(data.isEmpty)

let file = GraphQLFile(fieldName: "test",
originalName: "test",
data: data)

let inputStream = try file.generateInputStream()
inputStream.open()
XCTAssertTrue(inputStream.hasBytesAvailable)
inputStream.close()

let inputStream2 = try file.generateInputStream()

inputStream2.open()
XCTAssertTrue(inputStream2.hasBytesAvailable)
inputStream2.close()
}
}
42 changes: 18 additions & 24 deletions Tests/ApolloTests/RequestCreatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ class RequestCreatorTests: XCTestCase {
private let customRequestCreator = TestCustomRequestCreator()
private let apolloRequestCreator = ApolloRequestCreator()

private func testParentFolder(for file: StaticString = #file) -> URL {
let fileAsString = file.withUTF8Buffer {
String(decoding: $0, as: UTF8.self)
}
let url = URL(fileURLWithPath: fileAsString)
return url.deletingLastPathComponent()
}


private func checkString(_ string: String,
includes expectedString: String,
Expand All @@ -41,7 +35,7 @@ class RequestCreatorTests: XCTestCase {
}

private func fileURLForFile(named name: String, extension fileExtension: String) -> URL {
return self.testParentFolder()
return TestFileHelper.testParentFolder()
.appendingPathComponent(name)
.appendingPathExtension(fileExtension)
}
Expand Down Expand Up @@ -172,14 +166,14 @@ Charlie file content.
func testSingleFileWithApolloRequestCreator() throws {
let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt")

let alphaFile = GraphQLFile(fieldName: "upload",
originalName: "a.txt",
let alphaFile = try GraphQLFile(fieldName: "upload",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileUrl)

let data = try apolloRequestCreator.requestMultipartFormData(
for: HeroNameQuery(),
files: [alphaFile!],
files: [alphaFile],
sendOperationIdentifiers: false,
serializationFormat: JSONSerializationFormat.self,
manualBoundary: "TEST.BOUNDARY"
Expand Down Expand Up @@ -227,16 +221,16 @@ Alpha file content.

func testMultipleFilesWithApolloRequestCreator() throws {
let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt")
let alphaFile = GraphQLFile(fieldName: "uploads",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileURL)!
let alphaFile = try GraphQLFile(fieldName: "uploads",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileURL)

let betaFileURL = self.fileURLForFile(named: "b", extension: "txt")
let betaFile = GraphQLFile(fieldName: "uploads",
originalName: "b.txt",
mimeType: "text/plain",
fileURL: betaFileURL)!
let betaFile = try GraphQLFile(fieldName: "uploads",
originalName: "b.txt",
mimeType: "text/plain",
fileURL: betaFileURL)


let data = try apolloRequestCreator.requestMultipartFormData(
Expand Down Expand Up @@ -307,14 +301,14 @@ Bravo file content.
func testSingleFileWithCustomRequestCreator() throws {
let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt")

let alphaFile = GraphQLFile(fieldName: "upload",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileUrl)
let alphaFile = try GraphQLFile(fieldName: "upload",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileUrl)

let data = try customRequestCreator.requestMultipartFormData(
for: HeroNameQuery(),
files: [alphaFile!],
files: [alphaFile],
sendOperationIdentifiers: false,
serializationFormat: JSONSerializationFormat.self,
manualBoundary: "TEST.BOUNDARY"
Expand Down
20 changes: 20 additions & 0 deletions Tests/ApolloTests/TestFileHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// TestFileHelper.swift
// ApolloTests
//
// Created by Ellen Shapiro on 3/18/20.
// Copyright © 2020 Apollo GraphQL. All rights reserved.
//

import Foundation

struct TestFileHelper {

static func testParentFolder(for file: StaticString = #file) -> URL {
let fileAsString = file.withUTF8Buffer {
String(decoding: $0, as: UTF8.self)
}
let url = URL(fileURLWithPath: fileAsString)
return url.deletingLastPathComponent()
}
}