Skip to content

refactor: simplify auth tests mocks #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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