diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1e459f3ec..804b155c403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add support for hiding connection status with `isInvisible` [#2373](https://github.com/GetStream/stream-chat-swift/pull/2373) - Add `.withAttachments` in `MessageSearchFilterScope` to filter messages with attachments only [#2417](https://github.com/GetStream/stream-chat-swift/pull/2417) - Add `.withoutAttachments` in `MessageSearchFilterScope` to filter messages without any attachments [#2417](https://github.com/GetStream/stream-chat-swift/pull/2417) +- Add retries mechanism to AuthenticationRepository [#2414](https://github.com/GetStream/stream-chat-swift/pull/2414) ### 🐞 Fixed - Fix connecting user with non-expiring tokens (ex: development token) [#2393](https://github.com/GetStream/stream-chat-swift/pull/2393) diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index e47110a616c..7931ac6e772 100644 --- a/Sources/StreamChat/APIClient/APIClient.swift +++ b/Sources/StreamChat/APIClient/APIClient.swift @@ -14,7 +14,7 @@ class APIClient { /// `APIClient` uses this object to decode the results of network requests. let decoder: RequestDecoder - + /// Used for reobtaining tokens when they expire and API client receives token expiration error let tokenRefresher: (@escaping () -> Void) -> Void @@ -45,12 +45,6 @@ class APIClient { /// Shows whether the token is being refreshed at the moment @Atomic private var isRefreshingToken: Bool = false - - /// Amount of consecutive token refresh attempts - @Atomic private var tokenRefreshConsecutiveFailures: Int = 0 - - /// Maximum amount of consecutive token refresh attempts before failing - let maximumTokenRefreshAttempts = 10 /// Maximum amount of times a request can be retried private let maximumRequestRetries = 3 @@ -166,11 +160,6 @@ class APIClient { endpoint: Endpoint, completion: @escaping (Result) -> Void ) { - if tokenRefreshConsecutiveFailures > maximumTokenRefreshAttempts { - completion(.failure(ClientError.TooManyTokenRefreshAttempts())) - return - } - guard !isRefreshingToken else { completion(.failure(ClientError.RefreshingToken())) return @@ -207,7 +196,6 @@ class APIClient { response: response, error: error ) - self.tokenRefreshConsecutiveFailures = 0 completion(.success(decodedResponse)) } catch { if error is ClientError.ExpiredToken == false { @@ -236,18 +224,11 @@ class APIClient { completion(ClientError.RefreshingToken()) return } - isRefreshingToken = true - // We stop the queue so no more operations are triggered during the refresh - operationQueue.isSuspended = true - - // Increase the amount of consecutive failures - _tokenRefreshConsecutiveFailures.mutate { $0 += 1 } + enterTokenFetchMode() tokenRefresher { [weak self] in - self?.isRefreshingToken = false - // We restart the queue now that token refresh is completed - self?.operationQueue.isSuspended = false + self?.exitTokenFetchMode() completion(ClientError.TokenRefreshed()) } } @@ -299,6 +280,18 @@ class APIClient { isInRecoveryMode = false operationQueue.isSuspended = false } + + func enterTokenFetchMode() { + // We stop the queue so no more operations are triggered during the refresh + isRefreshingToken = true + operationQueue.isSuspended = true + } + + func exitTokenFetchMode() { + // We restart the queue now that token refresh is completed + isRefreshingToken = false + operationQueue.isSuspended = false + } } extension URLRequest { diff --git a/Sources/StreamChat/APIClient/RequestDecoder.swift b/Sources/StreamChat/APIClient/RequestDecoder.swift index f9e62a019fd..8d9770fd488 100644 --- a/Sources/StreamChat/APIClient/RequestDecoder.swift +++ b/Sources/StreamChat/APIClient/RequestDecoder.swift @@ -90,12 +90,6 @@ extension ClientError { class RefreshingToken: ClientError {} class TokenRefreshed: ClientError {} class ConnectionError: ClientError {} - class TooManyTokenRefreshAttempts: ClientError { - override var localizedDescription: String { - "Authentication failed on expired tokens after too many refresh attempts, please check that your user tokens are created correctly." - } - } - class ResponseBodyEmpty: ClientError { override var localizedDescription: String { "Response body is empty." } } diff --git a/Sources/StreamChat/Repositories/AuthenticationRepository.swift b/Sources/StreamChat/Repositories/AuthenticationRepository.swift index e8d0ae54e6b..785a1a4c8fa 100644 --- a/Sources/StreamChat/Repositories/AuthenticationRepository.swift +++ b/Sources/StreamChat/Repositories/AuthenticationRepository.swift @@ -18,8 +18,20 @@ protocol AuthenticationRepositoryDelegate: AnyObject { } class AuthenticationRepository { + private enum Constants { + /// Maximum amount of consecutive token refresh attempts before failing + static let maximumTokenRefreshAttempts = 10 + } + private let tokenQueue: DispatchQueue = DispatchQueue(label: "io.getstream.auth-repository", attributes: .concurrent) - private var _isGettingToken: Bool = false + private var _isGettingToken: Bool = false { + didSet { + guard oldValue != _isGettingToken else { return } + _isGettingToken ? apiClient.enterTokenFetchMode() : apiClient.exitTokenFetchMode() + } + } + + private var _consecutiveRefreshFailures: Int = 0 private var _currentUserId: UserId? private var _currentToken: Token? private var _tokenProvider: TokenProvider? @@ -31,6 +43,11 @@ class AuthenticationRepository { set { tokenQueue.async(flags: .barrier) { self._isGettingToken = newValue }} } + private var consecutiveRefreshFailures: Int { + get { tokenQueue.sync { _consecutiveRefreshFailures } } + set { tokenQueue.async(flags: .barrier) { self._consecutiveRefreshFailures = newValue }} + } + private(set) var currentUserId: UserId? { get { tokenQueue.sync { _currentUserId } } set { tokenQueue.async(flags: .barrier) { self._currentUserId = newValue }} @@ -65,8 +82,6 @@ class AuthenticationRepository { private let apiClient: APIClient private let databaseContainer: DatabaseContainer private let connectionRepository: ConnectionRepository - /// A timer that runs token refreshing job - private var tokenRetryTimer: TimerControl? /// Retry timing strategy for refreshing an expired token private var tokenExpirationRetryStrategy: RetryStrategy private let timerType: Timer.Type @@ -121,7 +136,7 @@ class AuthenticationRepository { /// - tokenProvider: The block to be used to get a token. func connectUser(userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { self.tokenProvider = tokenProvider - getToken(userInfo: userInfo, tokenProvider: tokenProvider, completion: completion) + scheduleTokenFetch(isRetry: false, userInfo: userInfo, tokenProvider: tokenProvider, completion: completion) } /// Establishes a connection for a guest user. @@ -156,16 +171,7 @@ class AuthenticationRepository { return } - let tokenProviderCheckingSuccess: TokenProvider = { [weak self] completion in - tokenProvider { result in - if case .success = result { - self?.tokenExpirationRetryStrategy.resetConsecutiveFailures() - } - completion(result) - } - } - - scheduleTokenFetch(userInfo: nil, tokenProvider: tokenProviderCheckingSuccess, completion: completion) + scheduleTokenFetch(isRetry: false, userInfo: nil, tokenProvider: tokenProvider, completion: completion) } func prepareEnvironment( @@ -240,24 +246,24 @@ class AuthenticationRepository { waiters.forEach { $0.value(token) } } - private func scheduleTokenFetch(userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { - guard !isGettingToken else { + private func scheduleTokenFetch(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + guard !isGettingToken || isRetry else { tokenRequestCompletions.append(completion) return } - tokenRetryTimer = timerType.schedule( + timerType.schedule( timeInterval: tokenExpirationRetryStrategy.getDelayAfterTheFailure(), queue: .main ) { [weak self] in log.debug("Firing timer for a new token request", subsystems: .authentication) - self?.getToken(userInfo: nil, tokenProvider: tokenProvider, completion: completion) + self?.getToken(isRetry: isRetry, userInfo: nil, tokenProvider: tokenProvider, completion: completion) } } - private func getToken(userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { + private func getToken(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) { tokenRequestCompletions.append(completion) - guard !isGettingToken else { + guard !isGettingToken || isRetry else { log.debug("Trying to get a token while already getting one", subsystems: .authentication) return } @@ -276,11 +282,17 @@ class AuthenticationRepository { self._isGettingToken = false let completions = self._tokenRequestCompletions self._tokenRequestCompletions = [] + self._consecutiveRefreshFailures = 0 return completions } completionBlocks?.forEach { $0(error) } } + guard consecutiveRefreshFailures < Constants.maximumTokenRefreshAttempts else { + onCompletion(ClientError.TooManyFailedTokenRefreshAttempts()) + return + } + let onTokenReceived: (Token) -> Void = { [weak self, weak connectionRepository] token in self?.prepareEnvironment(userInfo: userInfo, newToken: token) { error in // Errors thrown during `prepareEnvironment` cannot be recovered @@ -297,13 +309,29 @@ class AuthenticationRepository { } } + let retryFetchIfPossible: (Error?) -> Void = { [weak self] error in + guard let self = self else { return } + self.consecutiveRefreshFailures += 1 + guard self.consecutiveRefreshFailures < Constants.maximumTokenRefreshAttempts else { + onCompletion(error ?? ClientError.TooManyFailedTokenRefreshAttempts()) + return + } + + // We don't need to pass the completion again, as it is already present in `tokenRequestCompletions` + self.scheduleTokenFetch(isRetry: true, userInfo: userInfo, tokenProvider: tokenProvider, completion: { _ in }) + } + log.debug("Requesting a new token", subsystems: .authentication) - tokenProvider { result in + tokenProvider { [weak self] result in switch result { - case let .success(newToken): + case let .success(newToken) where !newToken.isExpired: onTokenReceived(newToken) + self?.tokenExpirationRetryStrategy.resetConsecutiveFailures() + case .success: + retryFetchIfPossible(nil) case let .failure(error): - onCompletion(error) + log.info("Failed fetching token with error: \(error)") + retryFetchIfPossible(error) } } } @@ -334,3 +362,14 @@ class AuthenticationRepository { tokenWaiters[waiter] = nil } } + +extension ClientError { + public class TooManyFailedTokenRefreshAttempts: ClientError { + override public var localizedDescription: String { + """ + Token fetch has failed more than 10 times. + Please make sure that your `tokenProvider` is correctly functioning. + """ + } + } +} diff --git a/Sources/StreamChat/Repositories/ConnectionRepository.swift b/Sources/StreamChat/Repositories/ConnectionRepository.swift index 760653f7b5f..0203f04a2dd 100644 --- a/Sources/StreamChat/Repositories/ConnectionRepository.swift +++ b/Sources/StreamChat/Repositories/ConnectionRepository.swift @@ -149,14 +149,14 @@ class ConnectionRepository { case let .connected(connectionId: id): shouldNotifyConnectionIdWaiters = true connectionId = id - case let .disconnected(source): - if let error = source.serverError, - error.isInvalidTokenError { - onInvalidToken() - shouldNotifyConnectionIdWaiters = false - } else { - shouldNotifyConnectionIdWaiters = true - } + + case let .disconnecting(source) where source.serverError?.isInvalidTokenError == true, + let .disconnected(source) where source.serverError?.isInvalidTokenError == true: + onInvalidToken() + shouldNotifyConnectionIdWaiters = false + connectionId = nil + case .disconnected: + shouldNotifyConnectionIdWaiters = true connectionId = nil case .initialized, .connecting, diff --git a/StreamChatUITestsApp/StreamChat/ChannelList.swift b/StreamChatUITestsApp/StreamChat/ChannelList.swift index ffb71dbf979..e2083148cf3 100644 --- a/StreamChatUITestsApp/StreamChat/ChannelList.swift +++ b/StreamChatUITestsApp/StreamChat/ChannelList.swift @@ -48,5 +48,6 @@ final class ChannelList: ChatChannelListVC, ChatConnectionControllerDelegate { case .disconnected: title = "disconnected" } + navigationItem.titleView?.accessibilityIdentifier = title } } diff --git a/StreamChatUITestsApp/ViewController.swift b/StreamChatUITestsApp/ViewController.swift index da28af3fe34..31bac79ce77 100644 --- a/StreamChatUITestsApp/ViewController.swift +++ b/StreamChatUITestsApp/ViewController.swift @@ -36,12 +36,15 @@ final class ViewController: UIViewController { @objc func didTap() { // Setup chat client streamChat.setUpChat() - streamChat.connectUser(completion: { _ in}) + streamChat.connectUser(completion: { _ in }) + showChannelList() + } + private func showChannelList() { // create UI let channelList = streamChat.makeChannelListViewController() router = channelList.router as? CustomChannelListRouter - + // create connection switch if needed let switchControl = self.createIsConnectedSwitchIfNeeded() diff --git a/StreamChatUITestsAppUITests/Pages/ChannelListPage.swift b/StreamChatUITestsAppUITests/Pages/ChannelListPage.swift index 4f8466639cd..419e740de85 100644 --- a/StreamChatUITestsAppUITests/Pages/ChannelListPage.swift +++ b/StreamChatUITestsAppUITests/Pages/ChannelListPage.swift @@ -23,12 +23,8 @@ enum ChannelListPage { format: "identifier LIKE 'titleLabel' AND label LIKE '\(withName)'")).firstMatch } - static var connectionStatus: XCUIElement { - if ProcessInfo().operatingSystemVersion.majorVersion == 12 { - return app.navigationBars.otherElements.firstMatch - } else { - return app.navigationBars.staticTexts.firstMatch - } + static func connectionLabel(withStatus: ChannelListPage.ConnectionStatus) -> XCUIElement { + app.navigationBars.matching(NSPredicate(format: "identifier LIKE '\(withStatus.rawValue)'")).firstMatch } enum ConnectionStatus: String { diff --git a/StreamChatUITestsAppUITests/Robots/UserRobot+Asserts.swift b/StreamChatUITestsAppUITests/Robots/UserRobot+Asserts.swift index b97c688e964..0f24b11bb79 100644 --- a/StreamChatUITestsAppUITests/Robots/UserRobot+Asserts.swift +++ b/StreamChatUITestsAppUITests/Robots/UserRobot+Asserts.swift @@ -151,9 +151,8 @@ extension UserRobot { file: StaticString = #filePath, line: UInt = #line ) -> Self { - let expectedStatus = status.rawValue - let actualStatus = ChannelListPage.connectionStatus.waitForText(expectedStatus, timeout: timeout).text - XCTAssertEqual(actualStatus, expectedStatus, file: file, line: line) + let correctStatus = ChannelListPage.connectionLabel(withStatus: status).wait(timeout: timeout).exists + XCTAssertTrue(correctStatus, file: file, line: line) return self } } diff --git a/StreamChatUITestsAppUITests/Tests/Authentication_Tests.swift b/StreamChatUITestsAppUITests/Tests/Authentication_Tests.swift index 3d34fc5e117..d6582a39b35 100644 --- a/StreamChatUITestsAppUITests/Tests/Authentication_Tests.swift +++ b/StreamChatUITestsAppUITests/Tests/Authentication_Tests.swift @@ -11,7 +11,6 @@ final class Authentication_Tests: StreamTestCase { mockServerEnabled = false app.setLaunchArguments(.jwt) try super.setUpWithError() - throw XCTSkip("[CIS-2309] JWT authentication issues") } func test_tokenExpiriesBeforeUserLogsIn() { @@ -102,7 +101,7 @@ final class Authentication_Tests: StreamTestCase { AND("server returns an error") {} AND("JWT generation recovers on server side") {} THEN("app requests a token refresh a second time") { - userRobot.assertConnectionStatus(.connected, timeout: 30) + userRobot.assertConnectionStatus(.connected) } } } diff --git a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift index bcc65ee7365..4237dc9165b 100644 --- a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift +++ b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift @@ -416,38 +416,6 @@ final class APIClient_Tests: XCTestCase { XCTEnsureRequestsWereExecuted(times: 2) } - func test_requestFailedWithExpiredToken_retriesRequestUntilReachingMaximumAttempts() throws { - var tokenRefresherWasCalled = false - createClient(tokenRefresher: { completion in - tokenRefresherWasCalled = true - completion() - }) - - let encoderError = ClientError.ExpiredToken() - decoder.decodeRequestResponse = .failure(encoderError) - - var result: Result? - waitUntil(timeout: 0.5) { done in - apiClient.request( - endpoint: Endpoint.mock(), - completion: { - result = $0; done() - } - ) - } - - XCTAssertTrue(tokenRefresherWasCalled) - - guard let result = result, case let .failure(error) = result else { - XCTFail() - return - } - - XCTAssertTrue(error is ClientError.TooManyTokenRefreshAttempts) - // 1 request + 10 refresh attempts - XCTEnsureRequestsWereExecuted(times: 11) - } - // MARK: - Flush func test_flushRequestsQueue_whenThereAreOperationsOngoing_shouldStopQueuedOnes() { diff --git a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift index e4fa99ba6b6..958bb6b7c10 100644 --- a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift @@ -27,6 +27,7 @@ final class AuthenticationRepository_Tests: XCTestCase { tokenExpirationRetryStrategy: retryStrategy, timerType: DefaultTimer.self ) + retryStrategy.mock_nextRetryDelay.returns(0.01) } func test_concurrentAccess() { @@ -167,7 +168,11 @@ final class AuthenticationRepository_Tests: XCTestCase { // Token Provider Failure let testError = TestError() - let provider: TokenProvider = { $0(.failure(testError)) } + var tokenCalls = 0 + let provider: TokenProvider = { + tokenCalls += 1 + $0(.failure(testError)) + } let completionExpectation = expectation(description: "Connect completion") var receivedError: Error? @@ -179,13 +184,51 @@ final class AuthenticationRepository_Tests: XCTestCase { }) XCTAssertNotNil(repository.tokenProvider) - waitForExpectations(timeout: 0.1) + waitForExpectations(timeout: defaultTimeout) XCTAssertNil(repository.currentToken) XCTAssertEqual(receivedError, testError) + XCTAssertEqual(tokenCalls, 10) XCTAssertNotCall(ConnectionRepository_Mock.Signature.connect, on: connectionRepository) XCTAssertNotCall(ConnectionRepository_Mock.Signature.forceConnectionInactiveMode, on: connectionRepository) } + func test_connectUser_failsGettingToken8Times_retriesA9thTime_success() throws { + let userInfo = UserInfo(id: "123") + + // Token Provider Failure + let testError = TestError() + let providedToken = Token.unique() + var tokenCalls = 0 + let provider: TokenProvider = { + tokenCalls += 1 + if tokenCalls > 8 { + $0(.success(providedToken)) + } else { + $0(.failure(testError)) + } + } + + // Simulate Success on Connection Repository + connectionRepository.connectResult = .success(()) + + let completionExpectation = expectation(description: "Connect completion") + var receivedError: Error? + XCTAssertNil(repository.tokenProvider) + + repository.connectUser(userInfo: userInfo, tokenProvider: provider, completion: { error in + receivedError = error + completionExpectation.fulfill() + }) + + XCTAssertNotNil(repository.tokenProvider) + waitForExpectations(timeout: defaultTimeout) + XCTAssertEqual(repository.currentToken, providedToken) + XCTAssertNil(receivedError) + XCTAssertEqual(tokenCalls, 9) + XCTAssertCall(ConnectionRepository_Mock.Signature.connect, on: connectionRepository) + XCTAssertCall(ConnectionRepository_Mock.Signature.forceConnectionInactiveMode, on: connectionRepository) + } + func test_connectUser_isNotGettingToken_tokenProviderSuccess_connectFailure() throws { let userInfo = UserInfo(id: "123") @@ -516,6 +559,7 @@ final class AuthenticationRepository_Tests: XCTestCase { // Token Provider Failure let apiError = TestError() + apiClient.test_mockResponseResult(Result.failure(apiError)) let completionExpectation = expectation(description: "Connect completion") var receivedError: Error? @@ -526,11 +570,8 @@ final class AuthenticationRepository_Tests: XCTestCase { completionExpectation.fulfill() }) - let requestCompletion = try XCTUnwrap(apiClient.request_completion as? ((Result) -> Void)) - requestCompletion(.failure(apiError)) - XCTAssertNotNil(repository.tokenProvider) - waitForExpectations(timeout: 0.1) + waitForExpectations(timeout: defaultTimeout) XCTAssertNil(repository.currentToken) XCTAssertEqual(receivedError, apiError) let request = try XCTUnwrap(apiClient.request_endpoint) @@ -549,6 +590,15 @@ final class AuthenticationRepository_Tests: XCTestCase { let testError = TestError() connectionRepository.connectResult = .failure(testError) + // API Result + apiClient.test_mockResponseResult( + Result.success(GuestUserTokenPayload( + user: CurrentUserPayload.dummy(userId: "", role: .user), + token: apiToken + ) + ) + ) + let completionExpectation = expectation(description: "Connect completion") var receivedError: Error? XCTAssertNil(repository.tokenProvider) @@ -558,9 +608,6 @@ final class AuthenticationRepository_Tests: XCTestCase { completionExpectation.fulfill() }) - let requestCompletion = try XCTUnwrap(apiClient.request_completion as? ((Result) -> Void)) - requestCompletion(.success(GuestUserTokenPayload(user: CurrentUserPayload.dummy(userId: "", role: .user), token: apiToken))) - XCTAssertNotNil(repository.tokenProvider) waitForExpectations(timeout: 0.1) XCTAssertEqual(repository.currentToken, apiToken) @@ -580,6 +627,15 @@ final class AuthenticationRepository_Tests: XCTestCase { // Simulate Success on Connection Repository connectionRepository.connectResult = .success(()) + // API Result + apiClient.test_mockResponseResult( + Result.success(GuestUserTokenPayload( + user: CurrentUserPayload.dummy(userId: "", role: .user), + token: apiToken + ) + ) + ) + let completionExpectation = expectation(description: "Connect completion") var receivedError: Error? XCTAssertNil(repository.tokenProvider) @@ -589,9 +645,6 @@ final class AuthenticationRepository_Tests: XCTestCase { completionExpectation.fulfill() }) - let requestCompletion = try XCTUnwrap(apiClient.request_completion as? ((Result) -> Void)) - requestCompletion(.success(GuestUserTokenPayload(user: CurrentUserPayload.dummy(userId: "", role: .user), token: apiToken))) - XCTAssertNotNil(repository.tokenProvider) waitForExpectations(timeout: 0.1) XCTAssertEqual(repository.currentToken, apiToken) @@ -826,7 +879,6 @@ final class AuthenticationRepository_Tests: XCTestCase { private func refreshTokenAndWaitForResponse(mockedError: Error?) throws -> Error? { XCTAssertNotNil(repository.tokenProvider) - retryStrategy.mock_nextRetryDelay.returns(0.1) connectionRepository.connectResult = mockedError.map { .failure($0) } ?? .success(()) @@ -842,6 +894,7 @@ final class AuthenticationRepository_Tests: XCTestCase { } private func setTokenProvider(mockedResult: Result, delay: DispatchTimeInterval? = nil) throws { + retryStrategy.mock_nextRetryDelay.returns(0) let tokenProvider: TokenProvider = { completion in if let delay = delay { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { @@ -863,6 +916,7 @@ final class AuthenticationRepository_Tests: XCTestCase { waitForExpectations(timeout: 0.1) connectionRepository.cleanUp() + retryStrategy.clear() } } diff --git a/Tests/StreamChatTests/StreamChat/ChatClient_Tests.swift b/Tests/StreamChatTests/StreamChat/ChatClient_Tests.swift index fd6162561fd..dff8ddb7940 100644 --- a/Tests/StreamChatTests/StreamChat/ChatClient_Tests.swift +++ b/Tests/StreamChatTests/StreamChat/ChatClient_Tests.swift @@ -310,9 +310,13 @@ final class ChatClient_Tests: XCTestCase { // Create a new chat client var client: ChatClient! = ChatClient(config: config) - - client.connectAnonymousUser() - + + let expectation = self.expectation(description: "Connect completes") + client.connectAnonymousUser { _ in + expectation.fulfill() + } + waitForExpectations(timeout: defaultTimeout) + // Check all the mandatory background workers are initialized XCTAssert(client.backgroundWorkers.contains { $0 is MessageSender }) XCTAssert(client.backgroundWorkers.contains { $0 is NewUserQueryUpdater }) diff --git a/fastlane/sinatra.rb b/fastlane/sinatra.rb index 010bf7535e9..175109191d1 100644 --- a/fastlane/sinatra.rb +++ b/fastlane/sinatra.rb @@ -54,12 +54,14 @@ post '/jwt/revoke/:udid' do jwt[:expiration_timeout] = install_jwt_timeout(udid: params['udid'], duration: params['duration']) + halt(200) end post '/jwt/break/:udid' do jwt[:generation_error_timeout] = install_jwt_timeout(udid: params['udid'], duration: params['duration']) + halt(200) end def install_jwt_timeout(udid:, duration:) - { params['udid'] => Time.now.to_i + params['duration'].to_i } + { udid => Time.now.to_i + duration.to_i } end