// // FeedlyLogoutOperationTests.swift // AccountTests // // Created by Kiel Gillard on 15/11/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // import XCTest @testable import Account import RSCore class FeedlyLogoutOperationTests: 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() } private func getTokens(for account: Account) throws -> (accessToken: Credentials, refreshToken: Credentials) { guard let accessToken = try account.retrieveCredentials(type: .oauthAccessToken), let refreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) else { XCTFail("Unable to retrieve access and/or refresh token from account.") throw CredentialsError.incompleteCredentials } return (accessToken, refreshToken) } class TestFeedlyLogoutService: FeedlyLogoutService { var mockResult: Result? var logoutExpectation: XCTestExpectation? func logout(completion: @escaping (Result) -> ()) { guard let result = mockResult else { XCTFail("Missing mock result. Test may time out because the completion will not be called.") return } DispatchQueue.main.async { completion(result) self.logoutExpectation?.fulfill() } } } func testCancel() { let service = TestFeedlyLogoutService() service.logoutExpectation = expectation(description: "Did Call Logout") service.logoutExpectation?.isInverted = true let accessToken: Credentials let refreshToken: Credentials do { (accessToken, refreshToken) = try getTokens(for: account) } catch { XCTFail("Could not retrieve credentials to verify their integrity later.") return } let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") logout.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(logout) MainThreadOperationQueue.shared.cancelOperations([logout]) waitForExpectations(timeout: 1) XCTAssertTrue(logout.isCanceled) do { let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken) let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) XCTAssertEqual(accountAccessToken, accessToken) XCTAssertEqual(accountRefreshToken, refreshToken) } catch { XCTFail("Could not verify tokens were left intact. Did the operation delete them?") } } func testLogoutSuccess() { let service = TestFeedlyLogoutService() service.logoutExpectation = expectation(description: "Did Call Logout") service.mockResult = .success(()) let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") logout.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(logout) waitForExpectations(timeout: 1) XCTAssertFalse(logout.isCanceled) do { let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken) let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) XCTAssertNil(accountAccessToken) XCTAssertNil(accountRefreshToken) } catch { XCTFail("Could not verify tokens were deleted.") } } class TestLogoutDelegate: FeedlyOperationDelegate { var error: Error? var didFailExpectation: XCTestExpectation? func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { self.error = error didFailExpectation?.fulfill() } } func testLogoutMissingAccessToken() { support.removeCredentials(matching: .oauthAccessToken, from: account) let (_, service) = support.makeMockNetworkStack() service.credentials = nil let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log) let delegate = TestLogoutDelegate() delegate.didFailExpectation = expectation(description: "Did Fail") logout.delegate = delegate // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") logout.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(logout) waitForExpectations(timeout: 1) XCTAssertFalse(logout.isCanceled) do { let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken) XCTAssertNil(accountAccessToken) } catch { XCTFail("Could not verify tokens were deleted.") } XCTAssertNotNil(delegate.error, "Should have failed with error.") if let error = delegate.error { switch error { case CredentialsError.incompleteCredentials: break default: XCTFail("Expected \(CredentialsError.incompleteCredentials)") } } } func testLogoutFailure() { let service = TestFeedlyLogoutService() service.logoutExpectation = expectation(description: "Did Call Logout") service.mockResult = .failure(URLError(.timedOut)) let accessToken: Credentials let refreshToken: Credentials do { (accessToken, refreshToken) = try getTokens(for: account) } catch { XCTFail("Could not retrieve credentials to verify their integrity later.") return } let logout = FeedlyLogoutOperation(account: account, service: service, log: support.log) // If this expectation is not fulfilled, the operation is not calling `didFinish`. let completionExpectation = expectation(description: "Did Finish") logout.completionBlock = { _ in completionExpectation.fulfill() } MainThreadOperationQueue.shared.addOperation(logout) waitForExpectations(timeout: 1) XCTAssertFalse(logout.isCanceled) do { let accountAccessToken = try account.retrieveCredentials(type: .oauthAccessToken) let accountRefreshToken = try account.retrieveCredentials(type: .oauthRefreshToken) XCTAssertEqual(accountAccessToken, accessToken) XCTAssertEqual(accountRefreshToken, refreshToken) } catch { XCTFail("Could not verify tokens were left intact. Did the operation delete them?") } } }