Skip to content

Commit

Permalink
refactor: simplify auth tests mocks (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev authored Feb 17, 2024
1 parent f6008bd commit 5e1dcca
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 239 deletions.
4 changes: 2 additions & 2 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,10 @@ public actor AuthClient {

self.init(
configuration: configuration,
sessionManager: .live,
sessionManager: DefaultSessionManager.shared,
codeVerifierStorage: .live,
api: api,
eventEmitter: EventEmitter(),
eventEmitter: DefaultEventEmitter.shared,
sessionStorage: .live,
logger: configuration.logger
)
Expand Down
33 changes: 29 additions & 4 deletions Sources/Auth/Internal/EventEmitter.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import ConcurrencyExtras
import Foundation

class EventEmitter: @unchecked Sendable {
protocol EventEmitter: Sendable {
func attachListener(
_ listener: @escaping AuthStateChangeListener
) -> AuthStateChangeListenerHandle

func emit(
_ event: AuthChangeEvent,
session: Session?,
handle: AuthStateChangeListenerHandle?
)
}

extension EventEmitter {
func emit(
_ event: AuthChangeEvent,
session: Session?
) {
emit(event, session: session, handle: nil)
}
}

final class DefaultEventEmitter: EventEmitter {
static let shared = DefaultEventEmitter()

private init() {}

let listeners = LockIsolated<[ObjectIdentifier: AuthStateChangeListener]>([:])

func attachListener(_ listener: @escaping AuthStateChangeListener)
-> AuthStateChangeListenerHandle
{
func attachListener(
_ listener: @escaping AuthStateChangeListener
) -> AuthStateChangeListenerHandle {
let handle = AuthStateChangeListenerHandle()
let key = ObjectIdentifier(handle)

Expand Down
29 changes: 12 additions & 17 deletions Sources/Auth/Internal/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,23 @@ struct SessionRefresher: Sendable {
var refreshSession: @Sendable (_ refreshToken: String) async throws -> Session
}

struct SessionManager: Sendable {
var session: @Sendable (_ shouldValidateExpiration: Bool) async throws -> Session
var update: @Sendable (_ session: Session) async throws -> Void
var remove: @Sendable () async -> Void

func session(shouldValidateExpiration: Bool = true) async throws -> Session {
try await session(shouldValidateExpiration)
}
protocol SessionManager: Sendable {
func session(shouldValidateExpiration: Bool) async throws -> Session
func update(_ session: Session) async throws -> Void
func remove() async
}

extension SessionManager {
static var live: Self = {
let manager = _LiveSessionManager()
return Self(
session: { try await manager.session(shouldValidateExpiration: $0) },
update: { try await manager.update($0) },
remove: { await manager.remove() }
)
}()
func session() async throws -> Session {
try await session(shouldValidateExpiration: true)
}
}

actor _LiveSessionManager {
actor DefaultSessionManager: SessionManager {
static let shared = DefaultSessionManager()

private init() {}

private var task: Task<Session, Error>?

private var storage: SessionStorage {
Expand Down
94 changes: 44 additions & 50 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import ConcurrencyExtras

final class AuthClientTests: XCTestCase {
var eventEmitter: MockEventEmitter!
var sessionManager: MockSessionManager!

var sut: AuthClient!

override func setUp() {
super.setUp()

eventEmitter = MockEventEmitter()
sessionManager = MockSessionManager()
sut = makeSUT()
}

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

sut = nil
eventEmitter = nil
sessionManager = nil
}

func testOnAuthStateChanges() async {
let session = Session.validSession
sessionManager.returnSession = .success(session)

let events = LockIsolated([AuthChangeEvent]())

await withDependencies {
$0.sessionManager.session = { @Sendable _ in session }
} operation: {
let handle = await sut.onAuthStateChange { event, _ in
events.withValue {
$0.append(event)
}
let handle = await sut.onAuthStateChange { event, _ in
events.withValue {
$0.append(event)
}
addTeardownBlock { [weak handle] in
XCTAssertNil(handle, "handle should be deallocated")
}

XCTAssertEqual(events.value, [.initialSession])
}
addTeardownBlock { [weak handle] in
XCTAssertNil(handle, "handle should be deallocated")
}

XCTAssertEqual(events.value, [.initialSession])
}

func testAuthStateChanges() async throws {
let session = Session.validSession
sessionManager.returnSession = .success(session)

let events = ActorIsolated([AuthChangeEvent]())

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

await withDependencies {
$0.sessionManager.session = { @Sendable _ in session }
} operation: {
let authStateStream = await sut.authStateChanges

let streamTask = Task {
for await (event, _) in authStateStream {
await events.withValue {
$0.append(event)
}
let authStateStream = await sut.authStateChanges

continuation.yield()
let streamTask = Task {
for await (event, _) in authStateStream {
await events.withValue {
$0.append(event)
}

continuation.yield()
}
}

_ = await stream.first { _ in true }
_ = await stream.first { _ in true }

let events = await events.value
XCTAssertEqual(events, [.initialSession])
let receivedEvents = await events.value
XCTAssertEqual(receivedEvents, [.initialSession])

streamTask.cancel()
}
streamTask.cancel()
}

func testSignOut() async throws {
sessionManager.returnSession = .success(.validSession)

try await withDependencies {
$0.api.execute = { _ in .stub() }
$0.sessionManager = .live
$0.sessionStorage = .inMemory
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
} operation: {
try await sut.signOut()

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

XCTAssertEqual(eventEmitter.emitReceivedParams.value.map(\.0), [.signedOut])
XCTAssertEqual(eventEmitter.emitReceivedParams.map(\.0), [.signedOut])
}
}

func testSignOutWithOthersScopeShouldNotRemoveLocalSession() async throws {
sessionManager.returnSession = .success(.validSession)

try await withDependencies {
$0.api.execute = { _ in .stub() }
$0.sessionManager = .live
$0.sessionStorage = .inMemory
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
} operation: {
try await sut.signOut(scope: .others)

// Session should still be valid.
_ = try await sut.session
XCTAssertFalse(sessionManager.removeCalled)
}
}

func testSignOutShouldRemoveSessionIfUserIsNotFound() async throws {
try await withDependencies {
sessionManager.returnSession = .success(.validSession)

await withDependencies {
$0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 404)) }
$0.sessionManager = .live
$0.sessionStorage = .inMemory
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
} operation: {
do {
try await sut.signOut()
Expand All @@ -140,23 +133,23 @@ final class AuthClientTests: XCTestCase {
XCTFail("Unexpected error: \(error)")
}

let emitedParams = eventEmitter.emitReceivedParams.value
let emitedParams = eventEmitter.emitReceivedParams
let emitedEvents = emitedParams.map(\.0)
let emitedSessions = emitedParams.map(\.1)

XCTAssertEqual(emitedEvents, [.signedOut])
XCTAssertEqual(emitedSessions.count, 1)
XCTAssertNil(emitedSessions[0])
XCTAssertNil(try Dependencies.current.value!.sessionStorage.getSession())

XCTAssertEqual(sessionManager.removeCallCount, 1)
}
}

func testSignOutShouldRemoveSessionIfJWTIsInvalid() async throws {
try await withDependencies {
sessionManager.returnSession = .success(.validSession)

await withDependencies {
$0.api.execute = { _ in throw AuthError.api(AuthError.APIError(code: 401)) }
$0.sessionManager = .live
$0.sessionStorage = .inMemory
try $0.sessionStorage.storeSession(StoredSession(session: .validSession))
} operation: {
do {
try await sut.signOut()
Expand All @@ -165,14 +158,15 @@ final class AuthClientTests: XCTestCase {
XCTFail("Unexpected error: \(error)")
}

let emitedParams = eventEmitter.emitReceivedParams.value
let emitedParams = eventEmitter.emitReceivedParams
let emitedEvents = emitedParams.map(\.0)
let emitedSessions = emitedParams.map(\.1)

XCTAssertEqual(emitedEvents, [.signedOut])
XCTAssertEqual(emitedSessions.count, 1)
XCTAssertNil(emitedSessions[0])
XCTAssertNil(try Dependencies.current.value!.sessionStorage.getSession())

XCTAssertEqual(sessionManager.removeCallCount, 1)
}
}

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

let sut = AuthClient(
configuration: configuration,
sessionManager: .mock,
sessionManager: sessionManager,
codeVerifierStorage: .mock,
api: .mock,
eventEmitter: eventEmitter,
Expand Down
20 changes: 16 additions & 4 deletions Tests/AuthTests/Mocks/MockEventEmitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,28 @@ import ConcurrencyExtras
import Foundation

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

override func emit(
func attachListener(_ listener: @escaping AuthStateChangeListener)
-> AuthStateChangeListenerHandle
{
emitter.attachListener(listener)
}

private let _emitReceivedParams: LockIsolated<[(AuthChangeEvent, Session?)]> = .init([])
var emitReceivedParams: [(AuthChangeEvent, Session?)] {
_emitReceivedParams.value
}

func emit(
_ event: AuthChangeEvent,
session: Session?,
handle: AuthStateChangeListenerHandle? = nil
) {
emitReceivedParams.withValue {
_emitReceivedParams.withValue {
$0.append((event, session))
}
super.emit(event, session: session, handle: handle)

emitter.emit(event, session: session, handle: handle)
}
}
35 changes: 35 additions & 0 deletions Tests/AuthTests/Mocks/MockSessionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// MockSessionManager.swift
//
//
// Created by Guilherme Souza on 16/02/24.
//

@testable import Auth
import ConcurrencyExtras
import Foundation

final class MockSessionManager: SessionManager {
private let _returnSession = LockIsolated(Result<Session, Error>?.none)
var returnSession: Result<Session, Error>? {
get { _returnSession.value }
set { _returnSession.setValue(newValue) }
}

func session(shouldValidateExpiration _: Bool) async throws -> Auth.Session {
try returnSession!.get()
}

func update(_: Auth.Session) async throws {}

private let _removeCallCount = LockIsolated(0)
var removeCallCount: Int {
get { _removeCallCount.value }
set { _removeCallCount.setValue(newValue) }
}

var removeCalled: Bool { removeCallCount > 0 }
func remove() async {
_removeCallCount.withValue { $0 += 1 }
}
}
12 changes: 2 additions & 10 deletions Tests/AuthTests/Mocks/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,6 @@ extension CodeVerifierStorage {
)
}

extension SessionManager {
static let mock = Self(
session: unimplemented("SessionManager.session"),
update: unimplemented("SessionManager.update"),
remove: unimplemented("SessionManager.remove")
)
}

extension SessionStorage {
static let mock = Self(
getSession: unimplemented("SessionStorage.getSession"),
Expand Down Expand Up @@ -101,9 +93,9 @@ extension Dependencies {
localStorage: Self.localStorage,
logger: nil
),
sessionManager: .mock,
sessionManager: MockSessionManager(),
api: .mock,
eventEmitter: EventEmitter(),
eventEmitter: MockEventEmitter(),
sessionStorage: .mock,
sessionRefresher: .mock,
codeVerifierStorage: .mock,
Expand Down
Loading

0 comments on commit 5e1dcca

Please sign in to comment.