Skip to content

Commit 020f089

Browse files
Merge pull request #1089 from apollographql/cleanup/errors
Add and test errors instead of nil failures for creating a GraphQL file and/or stream
2 parents 1fd7c7e + b3fbea8 commit 020f089

5 files changed

Lines changed: 163 additions & 39 deletions

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
9B0E471E240B239D0093BDA7 /* ASTEnumValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0E471D240B239D0093BDA7 /* ASTEnumValue.swift */; };
1717
9B1A38532332AF6F00325FB4 /* String+SHA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1A38522332AF6F00325FB4 /* String+SHA.swift */; };
1818
9B1CCDD92360F02C007C9032 /* Bundle+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */; };
19+
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */; };
20+
9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */; };
1921
9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C85235F8125004C426D /* CLIDownloader.swift */; };
2022
9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */; };
2123
9B518C8D235F8B9E004C426D /* CLIDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */; };
@@ -360,6 +362,8 @@
360362
9B0E471D240B239D0093BDA7 /* ASTEnumValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTEnumValue.swift; sourceTree = "<group>"; };
361363
9B1A38522332AF6F00325FB4 /* String+SHA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SHA.swift"; sourceTree = "<group>"; };
362364
9B1CCDD82360F02C007C9032 /* Bundle+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Helpers.swift"; sourceTree = "<group>"; };
365+
9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFileTests.swift; sourceTree = "<group>"; };
366+
9B21FD762422C8CC00998B5C /* TestFileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFileHelper.swift; sourceTree = "<group>"; };
363367
9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = "<group>"; };
364368
9B518C85235F8125004C426D /* CLIDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloader.swift; sourceTree = "<group>"; };
365369
9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloaderTests.swift; sourceTree = "<group>"; };
@@ -721,6 +725,7 @@
721725
children = (
722726
C3279FC52345233000224790 /* TestCustomRequestCreator.swift */,
723727
9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */,
728+
9B21FD762422C8CC00998B5C /* TestFileHelper.swift */,
724729
);
725730
name = TestHelpers;
726731
sourceTree = "<group>";
@@ -1150,6 +1155,7 @@
11501155
9B78C71B2326E859000C8C32 /* ErrorGenerationTests.swift */,
11511156
9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */,
11521157
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */,
1158+
9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */,
11531159
9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */,
11541160
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */,
11551161
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */,
@@ -2013,6 +2019,8 @@
20132019
9FE1C6E71E634C8D00C02284 /* PromiseTests.swift in Sources */,
20142020
9B64F6762354D219002D1BB5 /* URL+QueryDict.swift in Sources */,
20152021
9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */,
2022+
9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */,
2023+
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */,
20162024
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
20172025
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
20182026
C338DF1722DD9DE9006AF33E /* RequestCreatorTests.swift in Sources */,

Sources/Apollo/GraphQLFile.swift

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ public struct GraphQLFile {
88
public let data: Data?
99
public let fileURL: URL?
1010
public let contentLength: UInt64
11+
12+
public enum GraphQLFileError: Error, LocalizedError {
13+
case couldNotCreateInputStream
14+
case couldNotGetFileSize(fileURL: URL)
15+
16+
public var errorDescription: String? {
17+
switch self {
18+
case .couldNotCreateInputStream:
19+
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."
20+
case .couldNotGetFileSize(let fileURL):
21+
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."
22+
}
23+
}
24+
}
1125

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

34-
/// Failable convenience initializer for files in the filesystem
35-
/// Will return `nil` if the file URL cannot be used to create an `InputStream`, or if the file's size could not be determined.
48+
/// Throwing convenience initializer for files in the filesystem
3649
///
3750
/// - Parameters:
3851
/// - fieldName: The name of the field this file is being sent for
3952
/// - originalName: The original name of the file
4053
/// - mimeType: The mime type of the file to send to the server. Defaults to `GraphQLFile.octetStreamMimeType`.
4154
/// - fileURL: The URL of the file to upload.
42-
public init?(fieldName: String,
55+
/// - Throws: If the file's size could not be determined
56+
public init(fieldName: String,
4357
originalName: String,
4458
mimeType: String = GraphQLFile.octetStreamMimeType,
45-
fileURL: URL) {
46-
guard let contentLength = GraphQLFile.getFileSize(fileURL: fileURL) else {
47-
return nil
48-
}
49-
59+
fileURL: URL) throws {
60+
self.contentLength = try GraphQLFile.getFileSize(fileURL: fileURL)
5061
self.fieldName = fieldName
5162
self.originalName = originalName
5263
self.mimeType = mimeType
5364
self.data = nil
5465
self.fileURL = fileURL
55-
self.contentLength = contentLength
5666
}
5767

58-
/// Retrieves the InputStream
68+
/// Uses either the data or the file URL to create an
69+
/// `InputStream` that can be used to stream data into
70+
/// a multipart-form.
5971
///
72+
/// - Returns: The created `InputStream`.
73+
/// - Throws: If an input stream could not be created from either data or a file URL.
6074
public func generateInputStream() throws -> InputStream {
6175
if let data = data {
6276
return InputStream(data: data)
63-
} else if let fileURL = fileURL, let inputStream = InputStream(url: fileURL) {
77+
} else if let fileURL = fileURL,
78+
let inputStream = InputStream(url: fileURL) {
6479
return inputStream
80+
} else {
81+
throw GraphQLFileError.couldNotCreateInputStream
6582
}
66-
67-
throw GraphQLError("InputStream was not created.")
6883
}
6984

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

7691
return fileSize.uint64Value
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// GraphQLFileTests.swift
3+
// ApolloTests
4+
//
5+
// Created by Ellen Shapiro on 3/18/20.
6+
// Copyright © 2020 Apollo GraphQL. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
@testable import Apollo
12+
13+
class GraphQLFileTests: XCTestCase {
14+
15+
func testCreatingFileWithKnownBadURLFails() {
16+
let url = URL(fileURLWithPath: "/known/bad/path")
17+
do {
18+
_ = try GraphQLFile(fieldName: "test",
19+
originalName: "test",
20+
fileURL: url)
21+
} catch {
22+
switch error {
23+
case GraphQLFile.GraphQLFileError.couldNotGetFileSize(let fileURL):
24+
XCTAssertEqual(fileURL, url)
25+
default:
26+
XCTFail("Unexpected error creating file: \(error)")
27+
}
28+
}
29+
}
30+
31+
func testCreatingFileWithKnownGoodURLSucceedsAndCreatesAndCanRecreateInputStream() throws {
32+
let knownFileURL = TestFileHelper.testParentFolder()
33+
.appendingPathComponent("a.txt")
34+
35+
let file = try GraphQLFile(fieldName: "test",
36+
originalName: "test",
37+
fileURL: knownFileURL)
38+
39+
let inputStream = try file.generateInputStream()
40+
41+
inputStream.open()
42+
XCTAssertTrue(inputStream.hasBytesAvailable)
43+
inputStream.close()
44+
45+
let inputStream2 = try file.generateInputStream()
46+
47+
inputStream2.open()
48+
XCTAssertTrue(inputStream2.hasBytesAvailable)
49+
inputStream2.close()
50+
}
51+
52+
func testCreatingFileWithEmptyDataSucceedsAndCreatesInputStream() throws {
53+
let data = Data()
54+
XCTAssertTrue(data.isEmpty)
55+
56+
let file = GraphQLFile(fieldName: "test",
57+
originalName: "test",
58+
data: data)
59+
60+
let inputStream = try file.generateInputStream()
61+
62+
// Shouldn't have any bytes available if data is empty
63+
inputStream.open()
64+
XCTAssertFalse(inputStream.hasBytesAvailable)
65+
inputStream.close()
66+
}
67+
68+
func testCreatingFileWithNonEmptyDataSuccedsAndCreatesAndCanRecreateInputStream() throws {
69+
let data = try XCTUnwrap("A test string".data(using: .utf8))
70+
XCTAssertFalse(data.isEmpty)
71+
72+
let file = GraphQLFile(fieldName: "test",
73+
originalName: "test",
74+
data: data)
75+
76+
let inputStream = try file.generateInputStream()
77+
inputStream.open()
78+
XCTAssertTrue(inputStream.hasBytesAvailable)
79+
inputStream.close()
80+
81+
let inputStream2 = try file.generateInputStream()
82+
83+
inputStream2.open()
84+
XCTAssertTrue(inputStream2.hasBytesAvailable)
85+
inputStream2.close()
86+
}
87+
}

Tests/ApolloTests/RequestCreatorTests.swift

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@ class RequestCreatorTests: XCTestCase {
1414
private let customRequestCreator = TestCustomRequestCreator()
1515
private let apolloRequestCreator = ApolloRequestCreator()
1616

17-
private func testParentFolder(for file: StaticString = #file) -> URL {
18-
let fileAsString = file.withUTF8Buffer {
19-
String(decoding: $0, as: UTF8.self)
20-
}
21-
let url = URL(fileURLWithPath: fileAsString)
22-
return url.deletingLastPathComponent()
23-
}
17+
2418

2519
private func checkString(_ string: String,
2620
includes expectedString: String,
@@ -41,7 +35,7 @@ class RequestCreatorTests: XCTestCase {
4135
}
4236

4337
private func fileURLForFile(named name: String, extension fileExtension: String) -> URL {
44-
return self.testParentFolder()
38+
return TestFileHelper.testParentFolder()
4539
.appendingPathComponent(name)
4640
.appendingPathExtension(fileExtension)
4741
}
@@ -172,14 +166,14 @@ Charlie file content.
172166
func testSingleFileWithApolloRequestCreator() throws {
173167
let alphaFileUrl = self.fileURLForFile(named: "a", extension: "txt")
174168

175-
let alphaFile = GraphQLFile(fieldName: "upload",
176-
originalName: "a.txt",
169+
let alphaFile = try GraphQLFile(fieldName: "upload",
170+
originalName: "a.txt",
177171
mimeType: "text/plain",
178172
fileURL: alphaFileUrl)
179173

180174
let data = try apolloRequestCreator.requestMultipartFormData(
181175
for: HeroNameQuery(),
182-
files: [alphaFile!],
176+
files: [alphaFile],
183177
sendOperationIdentifiers: false,
184178
serializationFormat: JSONSerializationFormat.self,
185179
manualBoundary: "TEST.BOUNDARY"
@@ -227,16 +221,16 @@ Alpha file content.
227221

228222
func testMultipleFilesWithApolloRequestCreator() throws {
229223
let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt")
230-
let alphaFile = GraphQLFile(fieldName: "uploads",
231-
originalName: "a.txt",
232-
mimeType: "text/plain",
233-
fileURL: alphaFileURL)!
224+
let alphaFile = try GraphQLFile(fieldName: "uploads",
225+
originalName: "a.txt",
226+
mimeType: "text/plain",
227+
fileURL: alphaFileURL)
234228

235229
let betaFileURL = self.fileURLForFile(named: "b", extension: "txt")
236-
let betaFile = GraphQLFile(fieldName: "uploads",
237-
originalName: "b.txt",
238-
mimeType: "text/plain",
239-
fileURL: betaFileURL)!
230+
let betaFile = try GraphQLFile(fieldName: "uploads",
231+
originalName: "b.txt",
232+
mimeType: "text/plain",
233+
fileURL: betaFileURL)
240234

241235

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

310-
let alphaFile = GraphQLFile(fieldName: "upload",
311-
originalName: "a.txt",
312-
mimeType: "text/plain",
313-
fileURL: alphaFileUrl)
304+
let alphaFile = try GraphQLFile(fieldName: "upload",
305+
originalName: "a.txt",
306+
mimeType: "text/plain",
307+
fileURL: alphaFileUrl)
314308

315309
let data = try customRequestCreator.requestMultipartFormData(
316310
for: HeroNameQuery(),
317-
files: [alphaFile!],
311+
files: [alphaFile],
318312
sendOperationIdentifiers: false,
319313
serializationFormat: JSONSerializationFormat.self,
320314
manualBoundary: "TEST.BOUNDARY"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// TestFileHelper.swift
3+
// ApolloTests
4+
//
5+
// Created by Ellen Shapiro on 3/18/20.
6+
// Copyright © 2020 Apollo GraphQL. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
struct TestFileHelper {
12+
13+
static func testParentFolder(for file: StaticString = #file) -> URL {
14+
let fileAsString = file.withUTF8Buffer {
15+
String(decoding: $0, as: UTF8.self)
16+
}
17+
let url = URL(fileURLWithPath: fileAsString)
18+
return url.deletingLastPathComponent()
19+
}
20+
}

0 commit comments

Comments
 (0)