filestack/filestack-swift

View on GitHub
Tests/FilestackSDKTests/UploadTests.swift

Summary

Maintainability
D
2 days
Test Coverage
//
//  UploadTests.swift
//  FilestackSDK
//
//  Created by Ruben Nine on 23/08/2017.
//  Copyright © 2017 Filestack. All rights reserved.
//

import OHHTTPStubs
import OHHTTPStubsSwift
import XCTest
@testable import FilestackSDK

class UploadTests: XCTestCase {
    private let largeFileSize: Int = 6_034_668
    private let sampleFileSize: Int = 200_367
    private let sampleFileURL = Helpers.url(forResource: "sample", withExtension: "jpg", subdirectory: "Fixtures")!
    private let largeFileURL = Helpers.url(forResource: "large", withExtension: "jpg", subdirectory: "Fixtures")!
    private let chunkSize = 1 * Int(pow(Double(1024), Double(2)))
    private let partSize = 8 * Int(pow(Double(1024), Double(2)))
    private var currentPart = 1
    private var currentOffset = 0
    private var client: Client!

    private let defaultStoreOptions = StorageOptions(location: .s3, access: .private)

    override func setUp() {
        UploadService.shared.useBackgroundSession = false
        currentPart = 1
        currentOffset = 0
        client = Client(apiKey: "MY-OTHER-API-KEY", security: Seeds.Securities.basic)

        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
        HTTPStubs.removeAllStubs()
        client = nil
    }

    func testRegularMultiPartUpload() {
        var hitCount = 0

        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")

        var response: JSONResponse?

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: false,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(using: largeFileURL, options: uploadOptions) { resp in
            response = resp
            expectation.fulfill()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 1)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 1)

        XCTAssertEqual(hitCount, 1)
        XCTAssertEqual(response?.json?["handle"] as? String, "6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["size"] as? Int, largeFileSize)
        XCTAssertEqual(response?.json?["filename"] as? String, "large.jpg")
        XCTAssertEqual(response?.json?["status"] as? String, "Stored")
        XCTAssertEqual(response?.json?["url"] as? String, "https://cdn.filestackcontent.com/6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["mimetype"] as? String, "image/jpeg")
        XCTAssertEqual(response?.context as? URL, largeFileURL)

        XCTAssertTrue(FileManager.default.fileExists(atPath: largeFileURL.path))
    }

    func testIntelligentMultiPartUpload() {
        stubMultipartStartRequest(supportsIntelligentIngestion: true)
        stubMultipartPostPartRequest()
        stubMultipartPutRequest()
        stubMultipartCommitRequest()
        stubMultipartCompleteRequest()

        let expectation = self.expectation(description: "request should succeed")
        var response: JSONResponse?

        let uploader = client.upload(using: largeFileURL) { resp in
            response = resp
            expectation.fulfill()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 1)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 1)

        XCTAssertEqual(response?.json?["handle"] as? String, "6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["size"] as? Int, largeFileSize)
        XCTAssertEqual(response?.json?["filename"] as? String, "large.jpg")
        XCTAssertEqual(response?.json?["status"] as? String, "Stored")
        XCTAssertEqual(response?.json?["url"] as? String, "https://cdn.filestackcontent.com/6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["mimetype"] as? String, "image/jpeg")
        XCTAssertEqual(response?.context as? URL, largeFileURL)
    }

    func testCancellingResumableMultiPartUpload() {
        stubMultipartStartRequest(supportsIntelligentIngestion: true)
        stubMultipartPostPartRequest()
        stubMultipartPutRequest()
        stubMultipartCommitRequest()
        stubMultipartCompleteRequest(requestTime: 5.0, responseTime: 5.0)

        let expectation = self.expectation(description: "request should succeed")
        var response: JSONResponse?

        let uploader = client.upload(using: sampleFileURL) { resp in
            response = resp
            expectation.fulfill()
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            uploader.cancel()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.progress.totalUnitCount, 0)
        XCTAssertEqual(uploader.progress.completedUnitCount, 0)
        XCTAssertEqual(uploader.progress.fileTotalCount, 1)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 1)
        XCTAssertTrue(uploader.progress.isCancelled)

        XCTAssertNotNil(response?.error)
        XCTAssertEqual(response?.context as? URL, sampleFileURL)
    }

    func testCancellingStartedUploadWithoutUploadablesShouldCallCompletionHandler() {
        let expectation = self.expectation(description: "request should succeed")
        var response: [JSONResponse] = []

        let uploadOptions = UploadOptions(preferIntelligentIngestion: true,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: false)

        let uploader = client.upload(options: uploadOptions) { resp in
            response = resp
            expectation.fulfill()
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            uploader.cancel()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(response, [])
        XCTAssertTrue(uploader.progress.isCancelled)
    }

    func testCancellingNotStartedUploadWithoutUploadablesShouldCallCompletionHandler() {
        let expectation = self.expectation(description: "request should succeed")
        var response: [JSONResponse] = []

        let uploadOptions = UploadOptions(preferIntelligentIngestion: true,
                                          startImmediately: false,
                                          deleteTemporaryFilesAfterUpload: false)

        let uploader = client.upload(options: uploadOptions) { resp in
            response = resp
            expectation.fulfill()
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            uploader.cancel()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(response, [])
        XCTAssertTrue(uploader.progress.isCancelled)
    }

    func testMultiPartUploadWithWorkflowsAndUploadTags() {
        var hitCount = 0
        let workflows = ["workflow-1", "workflow-2", "workflow-3"]
        let uploadTags = ["key1": "value1", "key2": "value2"]
        let storeOptions = StorageOptions(location: .s3, workflows: workflows)

        let uploadOptions = UploadOptions(preferIntelligentIngestion: true,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: false,
                                          storeOptions: storeOptions)

        uploadOptions.uploadTags = uploadTags

        stubRegularMultipartRequest(hitCount: &hitCount, workflows: workflows, uploadTags: uploadTags)

        let expectation = self.expectation(description: "request should succeed")

        var response: JSONResponse?

        let uploader = client.upload(using: largeFileURL, options: uploadOptions) { resp in
            response = resp
            expectation.fulfill()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(hitCount, 1)
        XCTAssertNotNil(response?.json)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(largeFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 1)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 1)

        XCTAssertEqual(response?.json?["handle"] as? String, "6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["size"] as? Int, largeFileSize)
        XCTAssertEqual(response?.json?["filename"] as? String, "large.jpg")
        XCTAssertEqual(response?.json?["status"] as? String, "Stored")
        XCTAssertEqual(response?.json?["url"] as? String, "https://cdn.filestackcontent.com/6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(response?.json?["mimetype"] as? String, "image/jpeg")
        XCTAssertEqual(response?.json?["upload_tags"] as? [String: String], uploadTags)
        XCTAssertEqual(response?.context as? URL, largeFileURL)

        let returnedWorkflows: [String: Any]! = response?.json?["workflows"] as? [String: Any]

        XCTAssertNotNil(returnedWorkflows["workflow-1"])
        XCTAssertNotNil(returnedWorkflows["workflow-2"])
        XCTAssertNotNil(returnedWorkflows["workflow-3"])
    }

    func testMultiFileUploadWithOneFile() {
        var hitCount = 0
        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")

        var responses: [JSONResponse]!

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: false,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(using: [sampleFileURL], options: uploadOptions) { resp in
            responses = resp
            expectation.fulfill()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(hitCount, 1)
        XCTAssertEqual(responses.count, 1)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(sampleFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(sampleFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 1)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 1)

        let json = responses.first!.json!

        XCTAssertEqual(json["handle"] as? String, "6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(json["size"] as? Int, largeFileSize)
        XCTAssertEqual(json["filename"] as? String, "large.jpg")
        XCTAssertEqual(json["status"] as? String, "Stored")
        XCTAssertEqual(json["url"] as? String, "https://cdn.filestackcontent.com/6GKA0wnQWO7tKaGu2YXA")
        XCTAssertEqual(json["mimetype"] as? String, "image/jpeg")
        XCTAssertEqual(responses.first!.context as? URL, sampleFileURL)
    }

    func testMultiFileUploadWithFewFiles() {
        var hitCount = 0
        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")

        var responses: [JSONResponse]!

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: false,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(using: [sampleFileURL, largeFileURL], options: uploadOptions) { resp in
            responses = resp
            expectation.fulfill()
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(sampleFileSize + largeFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(sampleFileSize + largeFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 2)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 2)

        XCTAssertEqual(responses.count, 2)
        XCTAssertEqual(responses[0].context as? URL, sampleFileURL)
        XCTAssertEqual(responses[1].context as? URL, largeFileURL)
    }

    func testMultiFileUploadWithoutAutostart() {
        var hitCount = 0
        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")

        var responses: [JSONResponse]!

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: false,
                                          deleteTemporaryFilesAfterUpload: false,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(options: uploadOptions) { resp in
            responses = resp
            expectation.fulfill()
        }

        uploader.add(uploadables: [sampleFileURL, largeFileURL])
        uploader.start()

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.progress.totalUnitCount, Int64(sampleFileSize + largeFileSize))
        XCTAssertEqual(uploader.progress.completedUnitCount, Int64(sampleFileSize + largeFileSize))
        XCTAssertEqual(uploader.progress.fileTotalCount, 2)
        XCTAssertEqual(uploader.progress.fileCompletedCount, 2)

        XCTAssertEqual(responses.count, 2)
        XCTAssertEqual(responses[0].context as? URL, sampleFileURL)
        XCTAssertEqual(responses[1].context as? URL, largeFileURL)
    }

    func testUploadFileAtTemporaryLocation() {
        var hitCount = 0

        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")

        let fm = FileManager.default
        let temporaryURL = fm.temporaryDirectory.appendingPathComponent(largeFileURL.lastPathComponent)

        if fm.fileExists(atPath: temporaryURL.path) {
            XCTAssertNoThrow(try? fm.removeItem(at: temporaryURL))
        }

        XCTAssertNoThrow(try fm.copyItem(at: largeFileURL, to: temporaryURL))

        var response: JSONResponse?

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: true,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(using: temporaryURL, options: uploadOptions) { resp in
            XCTAssertTrue(fm.fileExists(atPath: temporaryURL.path), "File should exist")

            response = resp

            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                XCTAssertFalse(fm.fileExists(atPath: temporaryURL.path), "File should no longer exist")
                expectation.fulfill()
            }
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.state, .completed)
        XCTAssertEqual(hitCount, 1)
        XCTAssertEqual(response?.context as? URL, temporaryURL)
    }

    func testUploadFileAtPermanentLocation() {
        var hitCount = 0

        stubRegularMultipartRequest(hitCount: &hitCount)

        let expectation = self.expectation(description: "request should succeed")
        let fm = FileManager.default

        var response: JSONResponse?

        let uploadOptions = UploadOptions(preferIntelligentIngestion: false,
                                          startImmediately: true,
                                          deleteTemporaryFilesAfterUpload: true,
                                          storeOptions: defaultStoreOptions)

        let uploader = client.upload(using: largeFileURL, options: uploadOptions) { resp in
            XCTAssertTrue(fm.fileExists(atPath: self.largeFileURL.path), "File should exist")

            response = resp

            DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                XCTAssertTrue(fm.fileExists(atPath: self.largeFileURL.path), "File should still exist")
                expectation.fulfill()
            }
        }

        waitForExpectations(timeout: 15)

        XCTAssertEqual(uploader.state, .completed)
        XCTAssertEqual(hitCount, 1)
        XCTAssertEqual(response?.context as? URL, largeFileURL)
    }
}

// MARK: - Private Functions

private extension UploadTests {
    func stubRegularMultipartRequest(hitCount: inout Int, workflows: [String]? = nil, uploadTags: [String: String]? = nil) {
        stubMultipartStartRequest(supportsIntelligentIngestion: false)
        stubMultipartPostPartRequest(parts: ["PART-1"], hitCount: &hitCount)
        stubMultipartPutRequest(part: "PART-1")
        stubMultipartCompleteRequest(workflows: workflows, uploadTags: uploadTags)
    }

    func stubMultipartStartRequest(supportsIntelligentIngestion: Bool) {
        let uploadMultipartStartStubConditions =
            isScheme(Constants.uploadURL.scheme!) &&
            isHost(Constants.uploadURL.host!) &&
            isPath("/multipart/start") &&
            isMethodPOST()

        stub(condition: uploadMultipartStartStubConditions) { _ in
            let headers = ["Content-Type": "application/json"]
            var json = ["location_url": "upload-eu-west-1.filestackapi.com",
                        "uri": "/SOME-URI-HERE",
                        "upload_id": "SOME-UPLOAD-ID",
                        "region": "us-east-1"]
            if supportsIntelligentIngestion {
                json["upload_type"] = "intelligent_ingestion"
            }

            return HTTPStubsResponse(jsonObject: json, statusCode: 200, headers: headers)
        }
    }

    func stubMultipartPutRequest(part: String? = nil) {
        var uploadMultipartPutPartStubConditions = isScheme("https") &&
            isHost("s3.amazonaws.com") &&
            isMethodPUT()

        if let part = part {
            uploadMultipartPutPartStubConditions = uploadMultipartPutPartStubConditions && isPath("/\(part)")
        }

        stub(condition: uploadMultipartPutPartStubConditions) { _ in
            let headers = ["Content-Length": "0",
                           "Date": "Wed, 26 Jul 2017 09:16:37 GMT",
                           "Etag": "c9609eb741008bc1556f341f937a287e",
                           "Server": "AmazonS3",
                           "x-amz-id-2": "LxaKVvjp9jAK+ErminkrN8HV0VMOA/Bjkbf4A0cCaDRC6smJZerZqN9PqzRzGfn9p8vvTb6YIfM=",
                           "x-amz-request-id": "7D827E4E5CFD2E7A"]
            return HTTPStubsResponse(data: Data(), statusCode: 200, headers: headers)
        }
    }

    func stubMultipartPostPartRequest() {
        let uploadMultipartPostPartStubConditions =
            isScheme(Constants.uploadURL.scheme!) &&
            isHost(Constants.uploadURL.host!) &&
            isPath("/multipart/upload") &&
            isMethodPOST()

        stub(condition: uploadMultipartPostPartStubConditions) { _ in
            self.currentOffset += self.chunkSize
            if self.currentOffset >= self.partSize {
                self.currentOffset = 0
                self.currentPart += 1
            }
            let partName = "PART-\(self.currentPart)/\(self.currentOffset)"
            let headers = ["Content-Type": "application/json"]

            return HTTPStubsResponse(jsonObject: self.json(partName: partName), statusCode: 200, headers: headers)
        }
    }

    func stubMultipartPostPartRequest(parts: [String], hitCount: inout Int) {
        let partName = parts[hitCount]

        let uploadMultipartPostPartStubConditions =
            isScheme(Constants.uploadURL.scheme!) &&
            isHost(Constants.uploadURL.host!) &&
            isPath("/multipart/upload") &&
            isMethodPOST()

        stub(condition: uploadMultipartPostPartStubConditions) { _ in
            let headers = ["Content-Type": "application/json"]
            return HTTPStubsResponse(jsonObject: self.json(partName: partName), statusCode: 200, headers: headers)
        }

        hitCount += 1
    }

    func stubMultipartCompleteRequest(requestTime: TimeInterval = 0,
                                      responseTime: TimeInterval = 0,
                                      workflows: [String]? = nil,
                                      uploadTags: [String: String]? = nil) {
        let uploadMultipartCompleteStubConditions = isScheme(Constants.uploadURL.scheme!) &&
            isHost(Constants.uploadURL.host!) &&
            isPath("/multipart/complete") &&
            isMethodPOST()

        stub(condition: uploadMultipartCompleteStubConditions) { _ in
            let headers = ["Content-Type": "application/json"]
            var json: [String: Any] = ["handle": "6GKA0wnQWO7tKaGu2YXA",
                                       "size": self.largeFileSize,
                                       "filename": "large.jpg",
                                       "status": "Stored",
                                       "url": "https://cdn.filestackcontent.com/6GKA0wnQWO7tKaGu2YXA",
                                       "mimetype": "image/jpeg"]

            if let workflows = workflows {
                // Add workflows to JSON response with a fixed jobid.
                var workflowsDic: [String: [String: String]] = [:]
                for workflow in workflows {
                    workflowsDic[workflow] = ["jobid": "some-jobid"]
                }
                json["workflows"] = workflowsDic
            }

            if let uploadTags = uploadTags {
                json["upload_tags"] = uploadTags
            }

            let response = HTTPStubsResponse(jsonObject: json, statusCode: 200, headers: headers)
            response.requestTime = requestTime
            response.responseTime = responseTime
            return response
        }
    }

    func stubMultipartCommitRequest() {
        let uploadMultipartCommitStubConditions =
            isScheme(Constants.uploadURL.scheme!) &&
            isHost(Constants.uploadURL.host!) &&
            isPath("/multipart/commit") &&
            isMethodPOST()

        stub(condition: uploadMultipartCommitStubConditions) { _ in
            let headers = ["Content-Type": "text/plain; charset=utf-8"]
            return HTTPStubsResponse(data: Data(), statusCode: 200, headers: headers)
        }
    }

    func json(partName: String) -> [String: Any] {
        let authorization = """
        AWS4-HMAC-SHA256 Credential=AKIAIBGGXL3I2XTGV4IQ/20170726/us-east-1/s3/aws4_request, \
        SignedHeaders=content-length;content-md5;host;x-amz-date, \
        Signature=6638349931141536177e23f93b4eade99113ccc27ff96cc414b90dee260841c2
        """
        return ["location_url": "upload-eu-west-1.filestackapi.com",
                "url": "https://s3.amazonaws.com/\(partName)",
                "headers": ["Authorization": authorization,
                            "Content-MD5": "yWCet0EAi8FVbzQfk3oofg==",
                            "x-amz-content-sha256": "UNSIGNED-PAYLOAD",
                            "x-amz-date": "20170726T095615Z"]]
    }
}