Skip to content

Commit bf8a735

Browse files
nuno-vieirapolqf
andauthored
Safer connectUser() by logging out the user first if needed (#2577)
* Safer connectUser() by logging out the user first if needed * Clear active channels on log out * Update connect guest user in E2E Test to use a different ID when connecting a guest user * Add comment back * Add test coverage * Update CHANGELOG.md * Add test coverage to clearing out active controllers when logging out * Update CHANGELOG.md * Improve authentication flow to log out when needed * Add EnvironmentState tests * Add tests for prepareEnvironment assertion on id mismatch * Update AuthenticationRepository tests * Fix log in flow test error --------- Co-authored-by: Pol Quintana <[email protected]>
1 parent ccd4350 commit bf8a735

File tree

8 files changed

+325
-128
lines changed

8 files changed

+325
-128
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1515
- Fix crash when accessing FetchCache with an unexecuted NSFetchRequest [#2572](https://github.com/GetStream/stream-chat-swift/pull/2572)
1616
- Fix an issue which was blocking a Guest Authentication operation to retrieve a connection token [#2574](https://github.com/GetStream/stream-chat-swift/pull/2574)
1717
- Make connect/disconnect safer when network is offline [#2571](https://github.com/GetStream/stream-chat-swift/pull/2571)
18+
- Make connect safer by logging out the user first if needed [#2577](https://github.com/GetStream/stream-chat-swift/pull/2577)
1819

1920
## StreamChatUI
2021
### ✅ Added

Sources/StreamChat/ChatClient.swift

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,7 @@ public class ChatClient {
369369
/// Connects anonymous user
370370
/// - Parameter completion: The completion that will be called once the **first** user session for the given token is setup.
371371
public func connectAnonymousUser(completion: ((Error?) -> Void)? = nil) {
372-
authenticationRepository.connectUser(
373-
userInfo: nil,
374-
tokenProvider: { $0(.success(.anonymous)) },
372+
authenticationRepository.connectAnonymousUser(
375373
completion: { completion?($0) }
376374
)
377375
}
@@ -403,6 +401,10 @@ public class ChatClient {
403401
public func logout(completion: @escaping () -> Void) {
404402
authenticationRepository.logOutUser()
405403

404+
// Stop tracking active components
405+
activeChannelControllers.removeAllObjects()
406+
activeChannelListControllers.removeAllObjects()
407+
406408
let group = DispatchGroup()
407409
group.enter()
408410
disconnect {
@@ -473,21 +475,8 @@ public class ChatClient {
473475
}
474476

475477
extension ChatClient: AuthenticationRepositoryDelegate {
476-
/// Clears state related to the current user to leave the client ready for another user
477-
/// Will clear:
478-
/// - Background workers
479-
/// - References to active controllers
480-
/// - Database
481-
/// - Parameter completion: A block to be executed when the process is completed. Contains an error if something went wrong
482-
func clearCurrentUserData(completion: @escaping (Error?) -> Void) {
483-
createBackgroundWorkers()
484-
485-
// Stop tracking active components
486-
activeChannelControllers.removeAllObjects()
487-
activeChannelListControllers.removeAllObjects()
488-
489-
// Reset all existing local data.
490-
databaseContainer.removeAllData(force: true, completion: completion)
478+
func logOutUser(completion: @escaping () -> Void) {
479+
logout(completion: completion)
491480
}
492481

493482
func didFinishSettingUpAuthenticationEnvironment(for state: EnvironmentState) {

Sources/StreamChat/Repositories/AuthenticationRepository.swift

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,21 @@ enum EnvironmentState {
1010
case firstConnection
1111
case newToken
1212
case newUser
13+
14+
init(currentUserId: UserId?, newUserId: UserId) {
15+
if currentUserId == nil {
16+
self = .firstConnection
17+
} else if currentUserId == newUserId {
18+
self = .newToken
19+
} else {
20+
self = .newUser
21+
}
22+
}
1323
}
1424

1525
protocol AuthenticationRepositoryDelegate: AnyObject {
1626
func didFinishSettingUpAuthenticationEnvironment(for state: EnvironmentState)
17-
func clearCurrentUserData(completion: @escaping (Error?) -> Void)
27+
func logOutUser(completion: @escaping () -> Void)
1828
}
1929

2030
class AuthenticationRepository {
@@ -130,26 +140,57 @@ class AuthenticationRepository {
130140
updateToken(token: token, notifyTokenWaiters: completeTokenWaiters)
131141
}
132142

133-
/// Establishes a connection for a user.
143+
/// Establishes a connection for a non anonymous user.
134144
/// - Parameters:
135145
/// - userInfo: The user information that will be created OR updated if it exists.
136146
/// - tokenProvider: The block to be used to get a token.
137-
func connectUser(userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
138-
self.tokenProvider = tokenProvider
139-
scheduleTokenFetch(isRetry: false, userInfo: userInfo, tokenProvider: tokenProvider, completion: completion)
147+
func connectUser(userInfo: UserInfo, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
148+
var logOutFirst: Bool {
149+
if let currentUserId = currentUserId, currentUserId.isGuest {
150+
return true
151+
}
152+
153+
let state = EnvironmentState(currentUserId: currentUserId, newUserId: userInfo.id)
154+
return state == .newUser
155+
}
156+
157+
executeTokenFetch(logOutFirst: logOutFirst, userInfo: userInfo, tokenProvider: tokenProvider, completion: completion)
140158
}
141159

142160
/// Establishes a connection for a guest user.
143161
/// - Parameters:
144162
/// - userInfo: The user information that will be created OR updated if it exists.
145163
func connectGuestUser(userInfo: UserInfo, completion: @escaping (Error?) -> Void) {
146-
connectUser(
147-
userInfo: userInfo,
148-
tokenProvider: { [weak self] completion in
149-
self?.fetchGuestToken(userInfo: userInfo, completion: completion)
150-
},
151-
completion: completion
152-
)
164+
let tokenProvider: TokenProvider = { [weak self] completion in
165+
self?.fetchGuestToken(userInfo: userInfo, completion: completion)
166+
}
167+
executeTokenFetch(logOutFirst: true, userInfo: userInfo, tokenProvider: tokenProvider, completion: completion)
168+
}
169+
170+
/// Establishes a connection for an anonymous user.
171+
func connectAnonymousUser(completion: @escaping (Error?) -> Void) {
172+
let tokenProvider: TokenProvider = { $0(.success(.anonymous)) }
173+
executeTokenFetch(logOutFirst: true, userInfo: nil, tokenProvider: tokenProvider, completion: completion)
174+
}
175+
176+
private func executeTokenFetch(logOutFirst: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
177+
log.assert(delegate != nil, "Delegate should not be nil at this point")
178+
179+
let handleTokenFetch = { [weak self] in
180+
self?.tokenProvider = tokenProvider
181+
self?.scheduleTokenFetch(isRetry: false, userInfo: userInfo, tokenProvider: tokenProvider, completion: completion)
182+
}
183+
184+
guard logOutFirst else {
185+
handleTokenFetch()
186+
return
187+
}
188+
189+
if let delegate = delegate {
190+
delegate.logOutUser(completion: handleTokenFetch)
191+
} else {
192+
handleTokenFetch()
193+
}
153194
}
154195

155196
func clearTokenProvider() {
@@ -176,42 +217,30 @@ class AuthenticationRepository {
176217

177218
func prepareEnvironment(
178219
userInfo: UserInfo?,
179-
newToken: Token,
180-
completion: @escaping (Error?) -> Void
220+
newToken: Token
181221
) {
182-
let state: EnvironmentState
183-
if currentUserId == nil {
184-
state = .firstConnection
185-
} else if newToken.userId == currentUserId {
186-
state = .newToken
187-
} else {
188-
state = .newUser
189-
}
222+
let state = EnvironmentState(currentUserId: currentUserId, newUserId: newToken.userId)
190223

191224
log.assert(delegate != nil, "Delegate should not be nil at this point")
192225

226+
if let userInfo = userInfo, !newToken.userId.isGuest {
227+
log.assert(
228+
userInfo.id == newToken.userId,
229+
"The id of the retrieved token should match the user information passed to connect"
230+
)
231+
}
232+
193233
switch state {
194234
case .firstConnection, .newToken:
195235
connectionRepository.updateWebSocketEndpoint(with: newToken, userInfo: userInfo)
196236
setToken(token: newToken, completeTokenWaiters: true)
197237
delegate?.didFinishSettingUpAuthenticationEnvironment(for: state)
198-
completion(nil)
199238

200239
case .newUser:
201240
completeTokenWaiters(token: nil)
241+
connectionRepository.updateWebSocketEndpoint(with: newToken, userInfo: userInfo)
202242
setToken(token: newToken, completeTokenWaiters: false)
203243
delegate?.didFinishSettingUpAuthenticationEnvironment(for: state)
204-
205-
// Setting a new connection is not possible in connectionless mode.
206-
guard connectionRepository.isClientInActiveMode else {
207-
completion(ClientError.ClientIsNotInActiveMode())
208-
return
209-
}
210-
211-
connectionRepository.disconnect(source: .userInitiated) { [weak delegate, weak connectionRepository] in
212-
connectionRepository?.updateWebSocketEndpoint(with: newToken, userInfo: userInfo)
213-
delegate?.clearCurrentUserData(completion: completion)
214-
}
215244
}
216245
}
217246

@@ -294,19 +323,12 @@ class AuthenticationRepository {
294323
}
295324

296325
let onTokenReceived: (Token) -> Void = { [weak self, weak connectionRepository] token in
297-
self?.prepareEnvironment(userInfo: userInfo, newToken: token) { error in
298-
// Errors thrown during `prepareEnvironment` cannot be recovered
299-
if let error = error {
300-
onCompletion(error)
301-
return
302-
}
303-
304-
// We manually change the `connectionStatus` for passive client
305-
// to `disconnected` when environment was prepared correctly
306-
// (e.g. current user session is successfully restored).
307-
connectionRepository?.forceConnectionStatusForInactiveModeIfNeeded()
308-
connectionRepository?.connect(userInfo: userInfo, completion: onCompletion)
309-
}
326+
self?.prepareEnvironment(userInfo: userInfo, newToken: token)
327+
// We manually change the `connectionStatus` for passive client
328+
// to `disconnected` when environment was prepared correctly
329+
// (e.g. current user session is successfully restored).
330+
connectionRepository?.forceConnectionStatusForInactiveModeIfNeeded()
331+
connectionRepository?.connect(userInfo: userInfo, completion: onCompletion)
310332
}
311333

312334
let retryFetchIfPossible: (Error?) -> Void = { [weak self] error in
@@ -377,3 +399,9 @@ extension ClientError {
377399
}
378400
}
379401
}
402+
403+
private extension UserId {
404+
var isGuest: Bool {
405+
hasPrefix(UserRole.guest.rawValue)
406+
}
407+
}

StreamChatUITestsApp/ViewController.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,8 @@ extension StreamChatWrapper {
205205
}
206206

207207
func connectGuestUser(completion: @escaping (Error?) -> Void) {
208-
let userCredentials = UserCredentials.default
209-
let tokenProvider = mockTokenProvider(for: userCredentials)
210208
client?.connectGuestUser(
211-
userInfo: userCredentials.userInfo,
209+
userInfo: .init(id: "123"),
212210
completion: completion
213211
)
214212
}

TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/AuthenticationRepository_Mock.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class AuthenticationRepository_Mock: AuthenticationRepository, Spy {
1010
enum Signature {
1111
static let connectTokenProvider = "connectUser(userInfo:tokenProvider:completion:)"
1212
static let connectGuest = "connectGuestUser(userInfo:completion:)"
13+
static let connectAnon = "connectAnonymousUser(completion:)"
1314
static let refreshToken = "refreshToken(completion:)"
1415
static let clearTokenProvider = "clearTokenProvider()"
1516
static let logOut = "logOutUser()"
@@ -24,6 +25,7 @@ class AuthenticationRepository_Mock: AuthenticationRepository, Spy {
2425

2526
var connectUserResult: Result<Void, Error>?
2627
var connectGuestResult: Result<Void, Error>?
28+
var connectAnonResult: Result<Void, Error>?
2729
var refreshTokenResult: Result<Void, Error>?
2830
var completeWaitersToken: Token?
2931

@@ -73,6 +75,13 @@ class AuthenticationRepository_Mock: AuthenticationRepository, Spy {
7375
}
7476
}
7577

78+
override func connectAnonymousUser(completion: @escaping (Error?) -> Void) {
79+
record()
80+
if let result = connectAnonResult {
81+
completion(result.error)
82+
}
83+
}
84+
7685
override func setToken(token: Token, completeTokenWaiters: Bool) {
7786
record()
7887
setMockToken(token)

TestTools/StreamChatTestTools/SpyPattern/Spy/Logger_Spy.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Foundation
88
final class Logger_Spy: Logger, Spy {
99
var originalLogger: Logger?
1010
var recordedFunctions: [String] = []
11+
var failedAsserts: Int = 0
1112

1213
func injectMock() {
1314
let logger = LogConfig.logger
@@ -39,6 +40,7 @@ final class Logger_Spy: Logger, Spy {
3940
lineNumber: UInt = #line
4041
) {
4142
record()
43+
failedAsserts += condition() ? 0 : 1
4244
}
4345

4446
override func assertionFailure(
@@ -49,5 +51,6 @@ final class Logger_Spy: Logger, Spy {
4951
lineNumber: UInt = #line
5052
) {
5153
record()
54+
failedAsserts += 1
5255
}
5356
}

0 commit comments

Comments
 (0)