// // FeedlyRefreshAccessTokenOperationTests.swift // AccountTests // // Created by Kiel Gillard on 4/11/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // import XCTest @testable import Account import RSWeb import RSCore class FeedlyRefreshAccessTokenOperationTests: XCTestCase { private var account: Account! private let support = FeedlyTestSupport() override func setUp() { super.setUp() account = support.makeTestAccount() } override func tearDown() { if let account = account { support.destroy(account) } super.tearDown() } class TestRefreshTokenService: OAuthAccessTokenRefreshing { var mockResult: Result? var refreshAccessTokenExpectation: XCTestExpectation? var parameterTester: ((String, OAuthAuthorizationClient) -> ())? func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) { guard let result = mockResult else { XCTFail("Missing mock result. Test may time out because the completion will not be called.") return } parameterTester?(refreshToken, client) DispatchQueue.main.async { completion(result) self.refreshAccessTokenExpectation?.fulfill() } } } func testCancel() { let service = TestRefreshTokenService() service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh") service.refreshAccessTokenExpectation?.isInverted = true let client = support.makeMockOAuthClient() let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") refresh.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(refresh) MainThreadOperationQueue.shared.cancelOperations([refresh]) waitForExpectations(timeout: 1) XCTAssertTrue(refresh.isCanceled) } class TestRefreshTokenDelegate: FeedlyOperationDelegate { var error: Error? var didFailExpectation: XCTestExpectation? func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { self.error = error didFailExpectation?.fulfill() } } func testMissingRefreshToken() { support.removeCredentials(matching: .oauthRefreshToken, from: account) let service = TestRefreshTokenService() service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh") service.refreshAccessTokenExpectation?.isInverted = true let client = support.makeMockOAuthClient() let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log) let delegate = TestRefreshTokenDelegate() delegate.didFailExpectation = expectation(description: "Did Fail") refresh.delegate = delegate // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") refresh.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(refresh) waitForExpectations(timeout: 1) XCTAssertNotNil(delegate.error, "Should have failed with error.") if let error = delegate.error { switch error { case let error as TransportError: switch error { case .httpError(status: let status): XCTAssertEqual(status, 403, "Expected 403 Forbidden.") default: XCTFail("Expected 403 Forbidden") } default: XCTFail("Expected \(TransportError.httpError(status: 403))") } } } func testRefreshTokenSuccess() { let service = TestRefreshTokenService() service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh") let mockAccessToken = Credentials(type: .oauthAccessToken, username: "Test", secret: UUID().uuidString) let mockRefreshToken = Credentials(type: .oauthRefreshToken, username: "Test", secret: UUID().uuidString) let grant = OAuthAuthorizationGrant(accessToken: mockAccessToken, refreshToken: mockRefreshToken) service.mockResult = .success(grant) let client = support.makeMockOAuthClient() service.parameterTester = { serviceRefreshToken, serviceClient in if let accountRefreshToken = try! self.account.retrieveCredentials(type: .oauthRefreshToken) { XCTAssertEqual(serviceRefreshToken, accountRefreshToken.secret) } else { XCTFail("Could not verify correct refresh token used.") } XCTAssertEqual(serviceClient, client) } let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") refresh.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(refresh) waitForExpectations(timeout: 1) do { let accessToken = try account.retrieveCredentials(type: .oauthAccessToken) XCTAssertEqual(accessToken, mockAccessToken) let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) XCTAssertEqual(refreshToken, mockRefreshToken) } catch { XCTFail("Could not verify refresh and access tokens because \(error).") } } func testRefreshTokenFailure() { let accessTokenBefore: Credentials let refreshTokenBefore: Credentials do { guard let accessToken = try account.retrieveCredentials(type: .oauthAccessToken), let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) else { XCTFail("Initial refresh and/or access token does not exist.") return } accessTokenBefore = accessToken refreshTokenBefore = refreshToken } catch { XCTFail("Caught error getting initial refresh and access tokens because \(error).") return } let service = TestRefreshTokenService() service.refreshAccessTokenExpectation = expectation(description: "Did Call Refresh") service.mockResult = .failure(URLError(.timedOut)) let client = support.makeMockOAuthClient() service.parameterTester = { serviceRefreshToken, serviceClient in if let accountRefreshToken = try! self.account.retrieveCredentials(type: .oauthRefreshToken) { XCTAssertEqual(serviceRefreshToken, accountRefreshToken.secret) } else { XCTFail("Could not verify correct refresh token used.") } XCTAssertEqual(serviceClient, client) } let refresh = FeedlyRefreshAccessTokenOperation(account: account, service: service, oauthClient: client, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") refresh.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(refresh) waitForExpectations(timeout: 1) do { let accessToken = try account.retrieveCredentials(type: .oauthAccessToken) XCTAssertEqual(accessToken, accessTokenBefore) let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) XCTAssertEqual(refreshToken, refreshTokenBefore) } catch { XCTFail("Could not verify refresh and access tokens because \(error).") } } }