Skip to content

Commit

Permalink
Allow Rooms’s rooms to be mocks
Browse files Browse the repository at this point in the history
We want to be able to mock out the upcoming `Room.release()` method.
  • Loading branch information
lawrence-forooghian committed Nov 11, 2024
1 parent ac92127 commit c51fd92
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 26 deletions.
4 changes: 2 additions & 2 deletions Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public actor DefaultChatClient: ChatClient {
self.realtime = realtime
self.clientOptions = clientOptions ?? .init()
logger = DefaultInternalLogger(logHandler: self.clientOptions.logHandler, logLevel: self.clientOptions.logLevel)
let roomLifecycleManagerFactory = DefaultRoomLifecycleManagerFactory()
rooms = DefaultRooms(realtime: realtime, clientOptions: self.clientOptions, logger: logger, lifecycleManagerFactory: roomLifecycleManagerFactory)
let roomFactory = DefaultRoomFactory()
rooms = DefaultRooms(realtime: realtime, clientOptions: self.clientOptions, logger: logger, roomFactory: roomFactory)
}

public nonisolated var connection: any Connection {
Expand Down
27 changes: 21 additions & 6 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ public struct RoomStatusChange: Sendable, Equatable {
}
}

internal protocol RoomFactory: Sendable {
associatedtype Room: AblyChat.Room

func createRoom(realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: InternalLogger) async throws -> Room
}

internal final class DefaultRoomFactory: Sendable, RoomFactory {
private let lifecycleManagerFactory = DefaultRoomLifecycleManagerFactory()

internal func createRoom(realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: InternalLogger) async throws -> DefaultRoom<DefaultRoomLifecycleManagerFactory> {
try await DefaultRoom(
realtime: realtime,
chatAPI: chatAPI,
roomID: roomID,
options: options,
logger: logger,
lifecycleManagerFactory: lifecycleManagerFactory
)
}
}

internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>: Room where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor {
internal nonisolated let roomID: String
internal nonisolated let options: RoomOptions
Expand All @@ -41,12 +62,6 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>

private let lifecycleManager: any RoomLifecycleManager

#if DEBUG
internal nonisolated var testsOnly_realtime: RealtimeClient {
realtime
}
#endif

private let logger: InternalLogger

internal init(realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: InternalLogger, lifecycleManagerFactory: LifecycleManagerFactory) async throws {
Expand Down
12 changes: 6 additions & 6 deletions Sources/AblyChat/Rooms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public protocol Rooms: AnyObject, Sendable {
var clientOptions: ClientOptions { get }
}

internal actor DefaultRooms<LifecycleManagerFactory: RoomLifecycleManagerFactory>: Rooms where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor {
internal actor DefaultRooms<RoomFactory: AblyChat.RoomFactory>: Rooms {
private nonisolated let realtime: RealtimeClient
private let chatAPI: ChatAPI

Expand All @@ -19,16 +19,16 @@ internal actor DefaultRooms<LifecycleManagerFactory: RoomLifecycleManagerFactory
internal nonisolated let clientOptions: ClientOptions

private let logger: InternalLogger
private let lifecycleManagerFactory: LifecycleManagerFactory
private let roomFactory: RoomFactory

/// The set of rooms, keyed by room ID.
private var rooms: [String: DefaultRoom<LifecycleManagerFactory>] = [:]
private var rooms: [String: RoomFactory.Room] = [:]

internal init(realtime: RealtimeClient, clientOptions: ClientOptions, logger: InternalLogger, lifecycleManagerFactory: LifecycleManagerFactory) {
internal init(realtime: RealtimeClient, clientOptions: ClientOptions, logger: InternalLogger, roomFactory: RoomFactory) {
self.realtime = realtime
self.clientOptions = clientOptions
self.logger = logger
self.lifecycleManagerFactory = lifecycleManagerFactory
self.roomFactory = roomFactory
chatAPI = ChatAPI(realtime: realtime)
}

Expand All @@ -43,7 +43,7 @@ internal actor DefaultRooms<LifecycleManagerFactory: RoomLifecycleManagerFactory

return existingRoom
} else {
let room = try await DefaultRoom(realtime: realtime, chatAPI: chatAPI, roomID: roomID, options: options, logger: logger, lifecycleManagerFactory: lifecycleManagerFactory)
let room = try await roomFactory.createRoom(realtime: realtime, chatAPI: chatAPI, roomID: roomID, options: options, logger: logger)
rooms[roomID] = room
return room
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/AblyChatTests/DefaultChatClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct DefaultChatClientTests {
// Then: Its `rooms` property returns an instance of DefaultRooms with the same realtime client and client options
let rooms = client.rooms

let defaultRooms = try #require(rooms as? DefaultRooms<DefaultRoomLifecycleManagerFactory>)
let defaultRooms = try #require(rooms as? DefaultRooms<DefaultRoomFactory>)
#expect(defaultRooms.testsOnly_realtime === realtime)
#expect(defaultRooms.clientOptions.isEqualForTestPurposes(options))
}
Expand Down
29 changes: 18 additions & 11 deletions Tests/AblyChatTests/DefaultRoomsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ struct DefaultRoomsTests {
let realtime = MockRealtime.create(channels: .init(channels: [
.init(name: "basketball::$chat::$chatMessages"),
]))
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), lifecycleManagerFactory: MockRoomLifecycleManagerFactory())
let options = RoomOptions()
let roomToReturn = MockRoom(options: options)
let roomFactory = MockRoomFactory(room: roomToReturn)
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), roomFactory: roomFactory)

// When: get(roomID:options:) is called
let roomID = "basketball"
let options = RoomOptions()
let room = try await rooms.get(roomID: roomID, options: options)

// Then: It returns a DefaultRoom instance that uses the same Realtime instance, with the given ID and options
let defaultRoom = try #require(room as? DefaultRoom<MockRoomLifecycleManagerFactory>)
#expect(defaultRoom.testsOnly_realtime === realtime)
#expect(defaultRoom.roomID == roomID)
#expect(defaultRoom.options == options)
// Then: It returns a room that uses the same Realtime instance, with the given ID and options
let mockRoom = try #require(room as? MockRoom)
#expect(mockRoom === roomToReturn)

let createRoomArguments = try #require(await roomFactory.createRoomArguments)
#expect(createRoomArguments.realtime === realtime)
#expect(createRoomArguments.roomID == roomID)
#expect(createRoomArguments.options == options)
}

// @spec CHA-RC1b
Expand All @@ -31,10 +36,11 @@ struct DefaultRoomsTests {
let realtime = MockRealtime.create(channels: .init(channels: [
.init(name: "basketball::$chat::$chatMessages"),
]))
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), lifecycleManagerFactory: MockRoomLifecycleManagerFactory())
let options = RoomOptions()
let roomToReturn = MockRoom(options: options)
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), roomFactory: MockRoomFactory(room: roomToReturn))

let roomID = "basketball"
let options = RoomOptions()
let firstRoom = try await rooms.get(roomID: roomID, options: options)

// When: get(roomID:options:) is called with the same room ID
Expand All @@ -51,10 +57,11 @@ struct DefaultRoomsTests {
let realtime = MockRealtime.create(channels: .init(channels: [
.init(name: "basketball::$chat::$chatMessages"),
]))
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), lifecycleManagerFactory: MockRoomLifecycleManagerFactory())
let options = RoomOptions()
let roomToReturn = MockRoom(options: options)
let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), roomFactory: MockRoomFactory(room: roomToReturn))

let roomID = "basketball"
let options = RoomOptions()
_ = try await rooms.get(roomID: roomID, options: options)

// When: get(roomID:options:) is called with the same ID but different options
Expand Down
49 changes: 49 additions & 0 deletions Tests/AblyChatTests/Mocks/MockRoom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@testable import AblyChat

actor MockRoom: Room {
let options: RoomOptions

init(options: RoomOptions) {
self.options = options
}

nonisolated var roomID: String {
fatalError("Not implemented")
}

nonisolated var messages: any Messages {
fatalError("Not implemented")
}

nonisolated var presence: any Presence {
fatalError("Not implemented")
}

nonisolated var reactions: any RoomReactions {
fatalError("Not implemented")
}

nonisolated var typing: any Typing {
fatalError("Not implemented")
}

nonisolated var occupancy: any Occupancy {
fatalError("Not implemented")
}

var status: AblyChat.RoomStatus {
fatalError("Not implemented")
}

func onStatusChange(bufferingPolicy _: BufferingPolicy) async -> Subscription<RoomStatusChange> {
fatalError("Not implemented")
}

func attach() async throws {
fatalError("Not implemented")
}

func detach() async throws {
fatalError("Not implemented")
}
}
20 changes: 20 additions & 0 deletions Tests/AblyChatTests/Mocks/MockRoomFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@testable import AblyChat

actor MockRoomFactory: RoomFactory {
private let room: MockRoom?
private(set) var createRoomArguments: (realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: any InternalLogger)?

init(room: MockRoom? = nil) {
self.room = room
}

func createRoom(realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: any InternalLogger) async throws -> MockRoom {
createRoomArguments = (realtime: realtime, chatAPI: chatAPI, roomID: roomID, options: options, logger: logger)

guard let room else {
fatalError("MockRoomFactory.createRoom called, but the mock factory has not been set up with a room to return")
}

return room
}
}

0 comments on commit c51fd92

Please sign in to comment.