Skip to content

Commit 5e1dcca

Browse files
authored
refactor: simplify auth tests mocks (#243)
1 parent f6008bd commit 5e1dcca

File tree

9 files changed

+251
-239
lines changed

9 files changed

+251
-239
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,10 @@ public actor AuthClient {
166166

167167
self.init(
168168
configuration: configuration,
169-
sessionManager: .live,
169+
sessionManager: DefaultSessionManager.shared,
170170
codeVerifierStorage: .live,
171171
api: api,
172-
eventEmitter: EventEmitter(),
172+
eventEmitter: DefaultEventEmitter.shared,
173173
sessionStorage: .live,
174174
logger: configuration.logger
175175
)

Sources/Auth/Internal/EventEmitter.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import ConcurrencyExtras
22
import Foundation
33

4-
class EventEmitter: @unchecked Sendable {
4+
protocol EventEmitter: Sendable {
5+
func attachListener(
6+
_ listener: @escaping AuthStateChangeListener
7+
) -> AuthStateChangeListenerHandle
8+
9+
func emit(
10+
_ event: AuthChangeEvent,
11+
session: Session?,
12+
handle: AuthStateChangeListenerHandle?
13+
)
14+
}
15+
16+
extension EventEmitter {
17+
func emit(
18+
_ event: AuthChangeEvent,
19+
session: Session?
20+
) {
21+
emit(event, session: session, handle: nil)
22+
}
23+
}
24+
25+
final class DefaultEventEmitter: EventEmitter {
26+
static let shared = DefaultEventEmitter()
27+
28+
private init() {}
29+
530
let listeners = LockIsolated<[ObjectIdentifier: AuthStateChangeListener]>([:])
631

7-
func attachListener(_ listener: @escaping AuthStateChangeListener)
8-
-> AuthStateChangeListenerHandle
9-
{
32+
func attachListener(
33+
_ listener: @escaping AuthStateChangeListener
34+
) -> AuthStateChangeListenerHandle {
1035
let handle = AuthStateChangeListenerHandle()
1136
let key = ObjectIdentifier(handle)
1237

Sources/Auth/Internal/SessionManager.swift

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,23 @@ struct SessionRefresher: Sendable {
55
var refreshSession: @Sendable (_ refreshToken: String) async throws -> Session
66
}
77

8-
struct SessionManager: Sendable {
9-
var session: @Sendable (_ shouldValidateExpiration: Bool) async throws -> Session
10-
var update: @Sendable (_ session: Session) async throws -> Void
11-
var remove: @Sendable () async -> Void
12-
13-
func session(shouldValidateExpiration: Bool = true) async throws -> Session {
14-
try await session(shouldValidateExpiration)
15-
}
8+
protocol SessionManager: Sendable {
9+
func session(shouldValidateExpiration: Bool) async throws -> Session
10+
func update(_ session: Session) async throws -> Void
11+
func remove() async
1612
}
1713

1814
extension SessionManager {
19-
static var live: Self = {
20-
let manager = _LiveSessionManager()
21-
return Self(
22-
session: { try await manager.session(shouldValidateExpiration: $0) },
23-
update: { try await manager.update($0) },
24-
remove: { await manager.remove() }
25-
)
26-
}()
15+
func session() async throws -> Session {
16+
try await session(shouldValidateExpiration: true)
17+
}
2718
}
2819

29-
actor _LiveSessionManager {
20+
actor DefaultSessionManager: SessionManager {
21+
static let shared = DefaultSessionManager()
22+
23+
private init() {}
24+
3025
private var task: Task<Session, Error>?
3126

3227
private var storage: SessionStorage {

Tests/AuthTests/AuthClientTests.swift

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import ConcurrencyExtras
1717

1818
final class AuthClientTests: XCTestCase {
1919
var eventEmitter: MockEventEmitter!
20+
var sessionManager: MockSessionManager!
2021

2122
var sut: AuthClient!
2223

2324
override func setUp() {
2425
super.setUp()
2526

2627
eventEmitter = MockEventEmitter()
28+
sessionManager = MockSessionManager()
2729
sut = makeSUT()
2830
}
2931

@@ -38,66 +40,60 @@ final class AuthClientTests: XCTestCase {
3840

3941
sut = nil
4042
eventEmitter = nil
43+
sessionManager = nil
4144
}
4245

4346
func testOnAuthStateChanges() async {
4447
let session = Session.validSession
48+
sessionManager.returnSession = .success(session)
4549

4650
let events = LockIsolated([AuthChangeEvent]())
4751

48-
await withDependencies {
49-
$0.sessionManager.session = { @Sendable _ in session }
50-
} operation: {
51-
let handle = await sut.onAuthStateChange { event, _ in
52-
events.withValue {
53-
$0.append(event)
54-
}
52+
let handle = await sut.onAuthStateChange { event, _ in
53+
events.withValue {
54+
$0.append(event)
5555
}
56-
addTeardownBlock { [weak handle] in
57-
XCTAssertNil(handle, "handle should be deallocated")
58-
}
59-
60-
XCTAssertEqual(events.value, [.initialSession])
6156
}
57+
addTeardownBlock { [weak handle] in
58+
XCTAssertNil(handle, "handle should be deallocated")
59+
}
60+
61+
XCTAssertEqual(events.value, [.initialSession])
6262
}
6363

6464
func testAuthStateChanges() async throws {
6565
let session = Session.validSession
66+
sessionManager.returnSession = .success(session)
6667

6768
let events = ActorIsolated([AuthChangeEvent]())
6869

6970
let (stream, continuation) = AsyncStream<Void>.makeStream()
7071

71-
await withDependencies {
72-
$0.sessionManager.session = { @Sendable _ in session }
73-
} operation: {
74-
let authStateStream = await sut.authStateChanges
75-
76-
let streamTask = Task {
77-
for await (event, _) in authStateStream {
78-
await events.withValue {
79-
$0.append(event)
80-
}
72+
let authStateStream = await sut.authStateChanges
8173

82-
continuation.yield()
74+
let streamTask = Task {
75+
for await (event, _) in authStateStream {
76+
await events.withValue {
77+
$0.append(event)
8378
}
79+
80+
continuation.yield()
8481
}
82+
}
8583

86-
_ = await stream.first { _ in true }
84+
_ = await stream.first { _ in true }
8785

88-
let events = await events.value
89-
XCTAssertEqual(events, [.initialSession])
86+
let receivedEvents = await events.value
87+
XCTAssertEqual(receivedEvents, [.initialSession])
9088

91-
streamTask.cancel()
92-
}
89+
streamTask.cancel()
9390
}
9491

9592
func testSignOut() async throws {
93+
sessionManager.returnSession = .success(.validSession)
94+
9695
try await withDependencies {
9796
$0.api.execute = { _ in .stub() }
98-
$0.sessionManager = .live
99-
$0.sessionStorage = .inMemory
100-
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
10197
} operation: {
10298
try await sut.signOut()
10399

@@ -108,30 +104,27 @@ final class AuthClientTests: XCTestCase {
108104
XCTFail("Unexpected error.")
109105
}
110106

111-
XCTAssertEqual(eventEmitter.emitReceivedParams.value.map(\.0), [.signedOut])
107+
XCTAssertEqual(eventEmitter.emitReceivedParams.map(\.0), [.signedOut])
112108
}
113109
}
114110

115111
func testSignOutWithOthersScopeShouldNotRemoveLocalSession() async throws {
112+
sessionManager.returnSession = .success(.validSession)
113+
116114
try await withDependencies {
117115
$0.api.execute = { _ in .stub() }
118-
$0.sessionManager = .live
119-
$0.sessionStorage = .inMemory
120-
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
121116
} operation: {
122117
try await sut.signOut(scope: .others)
123118

124-
// Session should still be valid.
125-
_ = try await sut.session
119+
XCTAssertFalse(sessionManager.removeCalled)
126120
}
127121
}
128122

129123
func testSignOutShouldRemoveSessionIfUserIsNotFound() async throws {
130-
try await withDependencies {
124+
sessionManager.returnSession = .success(.validSession)
125+
126+
await withDependencies {
131127
$0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 404)) }
132-
$0.sessionManager = .live
133-
$0.sessionStorage = .inMemory
134-
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
135128
} operation: {
136129
do {
137130
try await sut.signOut()
@@ -140,23 +133,23 @@ final class AuthClientTests: XCTestCase {
140133
XCTFail("Unexpected error: \(error)")
141134
}
142135

143-
let emitedParams = eventEmitter.emitReceivedParams.value
136+
let emitedParams = eventEmitter.emitReceivedParams
144137
let emitedEvents = emitedParams.map(\.0)
145138
let emitedSessions = emitedParams.map(\.1)
146139

147140
XCTAssertEqual(emitedEvents, [.signedOut])
148141
XCTAssertEqual(emitedSessions.count, 1)
149142
XCTAssertNil(emitedSessions[0])
150-
XCTAssertNil(try Dependencies.current.value!.sessionStorage.getSession())
143+
144+
XCTAssertEqual(sessionManager.removeCallCount, 1)
151145
}
152146
}
153147

154148
func testSignOutShouldRemoveSessionIfJWTIsInvalid() async throws {
155-
try await withDependencies {
149+
sessionManager.returnSession = .success(.validSession)
150+
151+
await withDependencies {
156152
$0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 401)) }
157-
$0.sessionManager = .live
158-
$0.sessionStorage = .inMemory
159-
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
160153
} operation: {
161154
do {
162155
try await sut.signOut()
@@ -165,14 +158,15 @@ final class AuthClientTests: XCTestCase {
165158
XCTFail("Unexpected error: \(error)")
166159
}
167160

168-
let emitedParams = eventEmitter.emitReceivedParams.value
161+
let emitedParams = eventEmitter.emitReceivedParams
169162
let emitedEvents = emitedParams.map(\.0)
170163
let emitedSessions = emitedParams.map(\.1)
171164

172165
XCTAssertEqual(emitedEvents, [.signedOut])
173166
XCTAssertEqual(emitedSessions.count, 1)
174167
XCTAssertNil(emitedSessions[0])
175-
XCTAssertNil(try Dependencies.current.value!.sessionStorage.getSession())
168+
169+
XCTAssertEqual(sessionManager.removeCallCount, 1)
176170
}
177171
}
178172

@@ -186,7 +180,7 @@ final class AuthClientTests: XCTestCase {
186180

187181
let sut = AuthClient(
188182
configuration: configuration,
189-
sessionManager: .mock,
183+
sessionManager: sessionManager,
190184
codeVerifierStorage: .mock,
191185
api: .mock,
192186
eventEmitter: eventEmitter,

Tests/AuthTests/Mocks/MockEventEmitter.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,28 @@ import ConcurrencyExtras
1010
import Foundation
1111

1212
final class MockEventEmitter: EventEmitter {
13-
let emitReceivedParams: LockIsolated<[(AuthChangeEvent, Session?)]> = .init([])
13+
private let emitter = DefaultEventEmitter.shared
1414

15-
override func emit(
15+
func attachListener(_ listener: @escaping AuthStateChangeListener)
16+
-> AuthStateChangeListenerHandle
17+
{
18+
emitter.attachListener(listener)
19+
}
20+
21+
private let _emitReceivedParams: LockIsolated<[(AuthChangeEvent, Session?)]> = .init([])
22+
var emitReceivedParams: [(AuthChangeEvent, Session?)] {
23+
_emitReceivedParams.value
24+
}
25+
26+
func emit(
1627
_ event: AuthChangeEvent,
1728
session: Session?,
1829
handle: AuthStateChangeListenerHandle? = nil
1930
) {
20-
emitReceivedParams.withValue {
31+
_emitReceivedParams.withValue {
2132
$0.append((event, session))
2233
}
23-
super.emit(event, session: session, handle: handle)
34+
35+
emitter.emit(event, session: session, handle: handle)
2436
}
2537
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// MockSessionManager.swift
3+
//
4+
//
5+
// Created by Guilherme Souza on 16/02/24.
6+
//
7+
8+
@testable import Auth
9+
import ConcurrencyExtras
10+
import Foundation
11+
12+
final class MockSessionManager: SessionManager {
13+
private let _returnSession = LockIsolated(Result<Session, Error>?.none)
14+
var returnSession: Result<Session, Error>? {
15+
get { _returnSession.value }
16+
set { _returnSession.setValue(newValue) }
17+
}
18+
19+
func session(shouldValidateExpiration _: Bool) async throws -> Auth.Session {
20+
try returnSession!.get()
21+
}
22+
23+
func update(_: Auth.Session) async throws {}
24+
25+
private let _removeCallCount = LockIsolated(0)
26+
var removeCallCount: Int {
27+
get { _removeCallCount.value }
28+
set { _removeCallCount.setValue(newValue) }
29+
}
30+
31+
var removeCalled: Bool { removeCallCount > 0 }
32+
func remove() async {
33+
_removeCallCount.withValue { $0 += 1 }
34+
}
35+
}

Tests/AuthTests/Mocks/Mocks.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,6 @@ extension CodeVerifierStorage {
2222
)
2323
}
2424

25-
extension SessionManager {
26-
static let mock = Self(
27-
session: unimplemented("SessionManager.session"),
28-
update: unimplemented("SessionManager.update"),
29-
remove: unimplemented("SessionManager.remove")
30-
)
31-
}
32-
3325
extension SessionStorage {
3426
static let mock = Self(
3527
getSession: unimplemented("SessionStorage.getSession"),
@@ -101,9 +93,9 @@ extension Dependencies {
10193
localStorage: Self.localStorage,
10294
logger: nil
10395
),
104-
sessionManager: .mock,
96+
sessionManager: MockSessionManager(),
10597
api: .mock,
106-
eventEmitter: EventEmitter(),
98+
eventEmitter: MockEventEmitter(),
10799
sessionStorage: .mock,
108100
sessionRefresher: .mock,
109101
codeVerifierStorage: .mock,

0 commit comments

Comments
 (0)