0

I'm using a custom URLProtocol for mocking server communication in unit tests:

    override func startLoading() {

        let response: Result<Data, Error>
        if self.request.url == failureTransferURL {
            response = .failure(MockError())
        } else {
            response = .success(mockTransferData)
        }

        switch response {
        case let .success(data):

            // Step 2: Split data into chunks
            let chunkSize = 1024
            self.chunksRemaining = stride(from: 0, to: data.count, by: chunkSize).map {
                data[$0 ..< min($0 + chunkSize, data.count)]
            }

            self.chunkSendInterval = Self.transferDuration / TimeInterval(self.chunksRemaining.count)

            // Simulate response on a background thread.
            workerQueue.async {

                // Step 1: Simulate receiving an URLResponse. We need to do this
                // to let the client know the expected length of the data.
                let response = URLResponse(
                    url: self.request.url!,
                    mimeType: nil,
                    expectedContentLength: data.count,
                    textEncodingName: nil
                )

                self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)

                self.sendNextChunk()
            }

        case let .failure(error):
            // Simulate error.
            workerQueue.async {
                self.client?.urlProtocol(self, didFailWithError: error)
            }
        }
    }

    @objc
    private func sendNextChunk() {

        // Simulate response on a background thread.
        workerQueue.asyncAfter(deadline: .now() + self.chunkSendInterval) {

            guard !self.wasStopped else {
                self.client?.urlProtocol(self, didFailWithError: CancellationError())
                return
            }

            guard !self.chunksRemaining.isEmpty else {
                // Step 4: Finish loading (required).
                self.client?.urlProtocolDidFinishLoading(self)
                return
            }

            let chunk = self.chunksRemaining.removeFirst()
            CopresenceStudio.log.info("Sent Chunk")
            self.client?.urlProtocol(self, didLoad: chunk)
            self.sendNextChunk()
        }
    }

    override func stopLoading() {
        // Required by the superclass.
        wasStopped = true
    }

This works fine for downloads, and I get progress updates via the URLSessionDownloadDelegate.

However, when I create a session.uploadTask(...) and set the delegate, the 'progress' delegate method does not fire:

extension NetworkUploadMockTests: URLSessionTaskDelegate {

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64
    ) {
        // DOES NOT FIRE
        
        print("Sent \(totalBytesSent) / \(totalBytesExpectedToSend) Bytes.")
        self.uploadProgresses?.fulfill()
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: Error?
    ) {
        // DOES FIRE
        
        if error != nil {
            self.uploadWasCancelled?.fulfill()
        } else {
            self.uploadFinishes?.fulfill()
        }
    }
}

The unit test is pretty simple:

func testBasicUploading() throws {
        self.uploadFinishes = self.expectation(description: "Fake Upload succeeds")
        self.uploadProgresses = self.expectation(description: "Upload Progresses")
        self.uploadProgresses?.assertForOverFulfill = false

        self.urlSession = URLSession(
            configuration: .testing(transferDuration: 2.0), // just sets the custom protocols and how long it takes to load the data
            delegate: self,
            delegateQueue: nil
        )

        let urlRequest = URLRequest(url: uploadURL)
        let data = mockTransferData
        let task = urlSession!.uploadTask(with: urlRequest, from: data)
        task.delegate = self
        task.resume()

        self.wait(for: [self.uploadFinishes!, self.uploadProgresses!], timeout: 2.5)
    }

I can provide more code if necessary, but I guess it's just about knowing whether I made a false assumption about URLProtocol subclasses and what they can do; obviously all good for downloads, but are they able to fake the uploads as well?

horseshoe7
  • 2,745
  • 2
  • 35
  • 49

1 Answers1

0

It looks like this isn't possible out of the box, currently. (see here: https://github.com/apple/swift-corelibs-foundation/issues/3199)

I did however hack a workaround, where you keep a reference to the task about to be used to create the URLProtocol instance. (Note, this is for unit testing, which doesn't test multiple active data tasks, currently.)

Looks like the apple documentation is also out of date as it tells you to "Override init(task:cachedResponse:client:) instead of ..." but this leads to a compiler error.

then, in the 'sendNextChunk' method given in the question, you can just do a check for whether you're uploading or not:

self.client?.urlProtocol(self, didLoad: chunk)

            self.alreadySent += Int64(chunk.count)

            if self.isUploading, let session = Self.activeURLSession, let task = self.sessionTask {
                task.delegate?.urlSession?(
                    session,
                    task: task,
                    didSendBodyData: Int64(chunk.count),
                    totalBytesSent: self.alreadySent,
                    totalBytesExpectedToSend: self.totalSize
                )
            }

also, kept a reference to the URLSession that will be working with these.

Seems to do the trick.

horseshoe7
  • 2,745
  • 2
  • 35
  • 49