5

I have been working on an interceptor for adding auth headers to network requests in my app.

final class AuthInterceptor: URLProtocol {


    private var token: String = "my.access.token"   
    private var dataTask: URLSessionTask?
    private struct UnexpectedValuesRepresentationError: Error { }
    
    override class func canInit(with request: URLRequest) -> Bool {
        guard URLProtocol.property(forKey: "is_handled", in: request) as? Bool == nil else { return false }

        return true
        
        //return false // URL Loading System will handle the request using the system’s default behavior
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { return }
        URLProtocol.setProperty(true, forKey: "is_handled", in: mutableRequest)
        mutableRequest.addValue(token, forHTTPHeaderField: "Authorization")
        
        dataTask = URLSession.shared.dataTask(with: mutableRequest as URLRequest) { [weak self] data, response, error in
            guard let self = self else { return }
            if let error = error {
                self.client?.urlProtocol(self, didFailWithError: error)
            } else if let data = data, let response = response {
                self.client?.urlProtocol(self, didLoad: data)
                self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            } else {
                self.client?.urlProtocol(self, didFailWithError: UnexpectedValuesRepresentationError())
            }
            self.client?.urlProtocolDidFinishLoading(self)
        }
        
        dataTask?.resume()
    }
    
    override func stopLoading() {
        dataTask?.cancel()
        dataTask = nil
    }
}

As you can see I am currently just using private var token: String = "my.access.token" to mock a token. I'd like to introduce a TokenLoader that will fetch my token from it's cache.

As the URL Loading system will initialize instances of my protocol as needed, I am not sure how I can inject this. It is currently registered using: URLProtocol.registerClass(AuthInterceptor.self)

Imagine I had an interface like this -

public typealias LoadTokenResult = Result<String, Error>
public protocol TokenLoader {
  func load(_ key: String, completion: @escaping (LoadTokenResult) -> Void)
}

I'd like to ensure this is testable so I'd expect to be able to stub this in a test case or use a spy.

How can I achieve this?

Harry Blue
  • 4,202
  • 10
  • 39
  • 78

2 Answers2

1

As it is seen AuthInterceptor does not depend on TokenProvider by instance level, so you can inject class level dependency and use shared TokenProvider to extract/load tokens (for instance, depending on URL). This gives possibility to inject cached token provider for testing easily

final class AuthInterceptor: URLProtocol {

    static var tokenProvider: TokenLoader?  // << inject here

    // initial value, will be loaded from to tokenProvider result
    private var token: String?  

    // ... other code
    
    // somewhere inside when needed, like
    Self.tokenProvider?.load(key) { [weak self] result in
       self?.token = try? result.get()

       // proceed with token if present ...
    }
}

so in UT scenario you can do something like

override func setUp {
   AuthInterceptor.tokenProvider = MockTokenProvider()
}

override func tearDown {
   AuthInterceptor.tokenProvider = nil // or some default if needed
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
0

One possible option could be to use Generics for this.

protocol TokenProvider {
    static var token: String {get}
}

class MockTokenProvider: TokenProvider {
    static var token: String = "my.access.token"
}

And change AuthInterceptor like so:

    final class AuthInterceptor<T: TokenProvider>: URLProtocol {
      private var token: String = T.token
    ....
    ...
}

And register like so:

URLProtocol.registerClass(AuthInterceptor<MockTokenProvider>.self)

the above compiles in a playground, haven't tried it in a real project though..

Aris
  • 1,529
  • 9
  • 17
  • This surely increases testability. But for my own sake I more interested in the "pass a depencency" part of the question. I want to be able to pass an instance of a class to be able to fetch my access token. Perhaps another question is required for this. – Sunkas Oct 29 '20 at 12:14