From c51fd92941e1089c236b625562eab34668fdf7bd Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 11 Nov 2024 15:15:20 -0300 Subject: [PATCH 1/2] =?UTF-8?q?Allow=20Rooms=E2=80=99s=20rooms=20to=20be?= =?UTF-8?q?=20mocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want to be able to mock out the upcoming `Room.release()` method. --- Sources/AblyChat/ChatClient.swift | 4 +- Sources/AblyChat/Room.swift | 27 +++++++--- Sources/AblyChat/Rooms.swift | 12 ++--- .../DefaultChatClientTests.swift | 2 +- Tests/AblyChatTests/DefaultRoomsTests.swift | 29 ++++++----- Tests/AblyChatTests/Mocks/MockRoom.swift | 49 +++++++++++++++++++ .../AblyChatTests/Mocks/MockRoomFactory.swift | 20 ++++++++ 7 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 Tests/AblyChatTests/Mocks/MockRoom.swift create mode 100644 Tests/AblyChatTests/Mocks/MockRoomFactory.swift diff --git a/Sources/AblyChat/ChatClient.swift b/Sources/AblyChat/ChatClient.swift index 2e657eee..3827deb6 100644 --- a/Sources/AblyChat/ChatClient.swift +++ b/Sources/AblyChat/ChatClient.swift @@ -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 { diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift index 6aa09379..7831af5f 100644 --- a/Sources/AblyChat/Room.swift +++ b/Sources/AblyChat/Room.swift @@ -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 { + try await DefaultRoom( + realtime: realtime, + chatAPI: chatAPI, + roomID: roomID, + options: options, + logger: logger, + lifecycleManagerFactory: lifecycleManagerFactory + ) + } +} + internal actor DefaultRoom: Room where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor { internal nonisolated let roomID: String internal nonisolated let options: RoomOptions @@ -41,12 +62,6 @@ internal actor DefaultRoom 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 { diff --git a/Sources/AblyChat/Rooms.swift b/Sources/AblyChat/Rooms.swift index 4a775923..1fc6179c 100644 --- a/Sources/AblyChat/Rooms.swift +++ b/Sources/AblyChat/Rooms.swift @@ -6,7 +6,7 @@ public protocol Rooms: AnyObject, Sendable { var clientOptions: ClientOptions { get } } -internal actor DefaultRooms: Rooms where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor { +internal actor DefaultRooms: Rooms { private nonisolated let realtime: RealtimeClient private let chatAPI: ChatAPI @@ -19,16 +19,16 @@ internal actor DefaultRooms] = [:] + 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) } @@ -43,7 +43,7 @@ internal actor DefaultRooms) + let defaultRooms = try #require(rooms as? DefaultRooms) #expect(defaultRooms.testsOnly_realtime === realtime) #expect(defaultRooms.clientOptions.isEqualForTestPurposes(options)) } diff --git a/Tests/AblyChatTests/DefaultRoomsTests.swift b/Tests/AblyChatTests/DefaultRoomsTests.swift index 69167d41..7ccec55b 100644 --- a/Tests/AblyChatTests/DefaultRoomsTests.swift +++ b/Tests/AblyChatTests/DefaultRoomsTests.swift @@ -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) - #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 @@ -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 @@ -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 diff --git a/Tests/AblyChatTests/Mocks/MockRoom.swift b/Tests/AblyChatTests/Mocks/MockRoom.swift new file mode 100644 index 00000000..c2b389a4 --- /dev/null +++ b/Tests/AblyChatTests/Mocks/MockRoom.swift @@ -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 { + fatalError("Not implemented") + } + + func attach() async throws { + fatalError("Not implemented") + } + + func detach() async throws { + fatalError("Not implemented") + } +} diff --git a/Tests/AblyChatTests/Mocks/MockRoomFactory.swift b/Tests/AblyChatTests/Mocks/MockRoomFactory.swift new file mode 100644 index 00000000..804fd605 --- /dev/null +++ b/Tests/AblyChatTests/Mocks/MockRoomFactory.swift @@ -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 + } +} From 8daa1910738bb1a2e3aaf861fbb8d17d9475fa7b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 7 Nov 2024 11:51:14 -0300 Subject: [PATCH 2/2] Implement room release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use the implementation of the RELEASE operation provided by the room lifecycle manager, and implement the spec points relating to room map bookkeeping and releasing the underlying realtime channels. Based on [1] at 6f0740a. I have not implemented the spec points that relate to making sure that a room fetch waits for any previous room with the same ID to finish releasing; this is a part of the spec which is in flux (currently implemented via the INITIALIZING status, which was added to the spec after we started implementing the room lifecycle manager and hasn’t been implemented in this SDK yet, and soon to be further changed in the spec by making room-getting async). We can look at the current state of things when we come to do #66. Part of #47. [1] https://github.com/ably/specification/pull/200 --- Sources/AblyChat/Room.swift | 21 ++++++++-- Sources/AblyChat/RoomLifecycleManager.swift | 11 ++++- Sources/AblyChat/Rooms.swift | 19 ++++++++- Tests/AblyChatTests/DefaultRoomTests.swift | 27 ++++++++++++ Tests/AblyChatTests/DefaultRoomsTests.swift | 41 +++++++++++++++++++ Tests/AblyChatTests/IntegrationTests.swift | 11 +++++ Tests/AblyChatTests/Mocks/MockChannels.swift | 19 +++++++-- Tests/AblyChatTests/Mocks/MockRoom.swift | 15 ++++++- .../AblyChatTests/Mocks/MockRoomFactory.swift | 6 ++- .../Mocks/MockRoomLifecycleManager.swift | 5 +++ 10 files changed, 163 insertions(+), 12 deletions(-) diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift index 7831af5f..9967dfad 100644 --- a/Sources/AblyChat/Room.swift +++ b/Sources/AblyChat/Room.swift @@ -19,6 +19,11 @@ public protocol Room: AnyObject, Sendable { var options: RoomOptions { get } } +/// A ``Room`` that exposes additional functionality for use within the SDK. +internal protocol InternalRoom: Room { + func release() async +} + public struct RoomStatusChange: Sendable, Equatable { public var current: RoomStatus public var previous: RoomStatus @@ -30,7 +35,7 @@ public struct RoomStatusChange: Sendable, Equatable { } internal protocol RoomFactory: Sendable { - associatedtype Room: AblyChat.Room + associatedtype Room: AblyChat.InternalRoom func createRoom(realtime: RealtimeClient, chatAPI: ChatAPI, roomID: String, options: RoomOptions, logger: InternalLogger) async throws -> Room } @@ -50,7 +55,7 @@ internal final class DefaultRoomFactory: Sendable, RoomFactory { } } -internal actor DefaultRoom: Room where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor { +internal actor DefaultRoom: InternalRoom where LifecycleManagerFactory.Contributor == DefaultRoomLifecycleContributor { internal nonisolated let roomID: String internal nonisolated let options: RoomOptions private let chatAPI: ChatAPI @@ -61,6 +66,7 @@ internal actor DefaultRoom private nonisolated let realtime: RealtimeClient private let lifecycleManager: any RoomLifecycleManager + private let channels: [RoomFeature: any RealtimeChannelProtocol] private let logger: InternalLogger @@ -75,7 +81,7 @@ internal actor DefaultRoom throw ARTErrorInfo.create(withCode: 40000, message: "Ensure your Realtime instance is initialized with a clientId.") } - let channels = Self.createChannels(roomID: roomID, realtime: realtime) + channels = Self.createChannels(roomID: roomID, realtime: realtime) let contributors = Self.createContributors(channels: channels) lifecycleManager = await lifecycleManagerFactory.createManager( @@ -130,6 +136,15 @@ internal actor DefaultRoom try await lifecycleManager.performDetachOperation() } + internal func release() async { + await lifecycleManager.performReleaseOperation() + + // CHA-RL3h + for channel in channels.values { + realtime.channels.release(channel.name) + } + } + // MARK: - Room status internal func onStatusChange(bufferingPolicy: BufferingPolicy) async -> Subscription { diff --git a/Sources/AblyChat/RoomLifecycleManager.swift b/Sources/AblyChat/RoomLifecycleManager.swift index 44aec8ee..a6dc72bc 100644 --- a/Sources/AblyChat/RoomLifecycleManager.swift +++ b/Sources/AblyChat/RoomLifecycleManager.swift @@ -43,6 +43,7 @@ internal protocol RoomLifecycleContributor: Identifiable, Sendable { internal protocol RoomLifecycleManager: Sendable { func performAttachOperation() async throws func performDetachOperation() async throws + func performReleaseOperation() async var roomStatus: RoomStatus { get async } func onChange(bufferingPolicy: BufferingPolicy) async -> Subscription } @@ -864,11 +865,19 @@ internal actor DefaultRoomLifecycleManager: Rooms { } } - internal func release(roomID _: String) async throws { - fatalError("Not yet implemented") + #if DEBUG + internal func testsOnly_hasExistingRoomWithID(_ roomID: String) -> Bool { + rooms[roomID] != nil + } + #endif + + internal func release(roomID: String) async throws { + guard let room = rooms[roomID] else { + // TODO: what to do here? (https://github.com/ably/specification/pull/200/files#r1837154563) — Andy replied that it’s a no-op but that this is going to be specified in an upcoming PR when we make room-getting async + return + } + + // CHA-RC1d + rooms.removeValue(forKey: roomID) + + // CHA-RL1e + await room.release() } } diff --git a/Tests/AblyChatTests/DefaultRoomTests.swift b/Tests/AblyChatTests/DefaultRoomTests.swift index 88135491..eec8e94b 100644 --- a/Tests/AblyChatTests/DefaultRoomTests.swift +++ b/Tests/AblyChatTests/DefaultRoomTests.swift @@ -92,6 +92,33 @@ struct DefaultRoomTests { #expect(await lifecycleManager.detachCallCount == 1) } + // MARK: - Release + + // @spec CHA-RL3h - I haven’t explicitly tested that `performReleaseOperation()` happens _before_ releasing the channels (i.e. the “upon operation completion” part of the spec point), because it would require me to spend extra time on mock-writing which I can’t really afford to spend right now. I think we can live with it at least for the time being; I’m pretty sure there are other tests where the spec mentions or requires an order where I also haven’t tested the order. + @Test + func release() async throws { + // Given: a DefaultRoom instance + let channelsList = [ + MockRealtimeChannel(name: "basketball::$chat::$chatMessages"), + ] + let channels = MockChannels(channels: channelsList) + let realtime = MockRealtime.create(channels: channels) + + let lifecycleManager = MockRoomLifecycleManager() + let lifecycleManagerFactory = MockRoomLifecycleManagerFactory(manager: lifecycleManager) + + let room = try await DefaultRoom(realtime: realtime, chatAPI: ChatAPI(realtime: realtime), roomID: "basketball", options: .init(), logger: TestLogger(), lifecycleManagerFactory: lifecycleManagerFactory) + + // When: `release()` is called on the room + await room.release() + + // Then: It: + // 1. calls `performReleaseOperation()` on the room lifecycle manager + // 2. calls `channels.release()` with the name of each of the features’ channels + #expect(await lifecycleManager.releaseCallCount == 1) + #expect(Set(channels.releaseArguments) == Set(channelsList.map(\.name))) + } + // MARK: - Room status @Test diff --git a/Tests/AblyChatTests/DefaultRoomsTests.swift b/Tests/AblyChatTests/DefaultRoomsTests.swift index 7ccec55b..4cebc330 100644 --- a/Tests/AblyChatTests/DefaultRoomsTests.swift +++ b/Tests/AblyChatTests/DefaultRoomsTests.swift @@ -3,6 +3,8 @@ import Testing // The channel name of basketball::$chat::$chatMessages is passed in to these tests due to `DefaultRoom` kicking off the `DefaultMessages` initialization. This in turn needs a valid `roomId` or else the `MockChannels` class will throw an error as it would be expecting a channel with the name \(roomID)::$chat::$chatMessages to exist (where `roomId` is the property passed into `rooms.get`). struct DefaultRoomsTests { + // MARK: - Get a room + // @spec CHA-RC1a @Test func get_returnsRoomWithGivenID() async throws { @@ -78,4 +80,43 @@ struct DefaultRoomsTests { // Then: It throws an inconsistentRoomOptions error #expect(isChatError(caughtError, withCode: .inconsistentRoomOptions)) } + + // MARK: - Release a room + + // @spec CHA-RC1d + // @spec CHA-RC1e + @Test + func release() async throws { + // Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID + let realtime = MockRealtime.create(channels: .init(channels: [ + .init(name: "basketball::$chat::$chatMessages"), + ])) + let options = RoomOptions() + let hasExistingRoomAtMomentRoomReleaseCalledStreamComponents = AsyncStream.makeStream(of: Bool.self) + let roomFactory = MockRoomFactory() + let rooms = DefaultRooms(realtime: realtime, clientOptions: .init(), logger: TestLogger(), roomFactory: roomFactory) + + let roomID = "basketball" + + let roomToReturn = MockRoom(options: options) { + await hasExistingRoomAtMomentRoomReleaseCalledStreamComponents.continuation.yield(rooms.testsOnly_hasExistingRoomWithID(roomID)) + } + await roomFactory.setRoom(roomToReturn) + + _ = try await rooms.get(roomID: roomID, options: .init()) + try #require(await rooms.testsOnly_hasExistingRoomWithID(roomID)) + + // When: `release(roomID:)` is called with this room ID + _ = try await rooms.release(roomID: roomID) + + // Then: + // 1. first, the room is removed from the room map + // 2. next, `release` is called on the room + + // These two lines are convoluted because the #require macro has a hard time with stuff of type Bool? and emits warnings about ambiguity unless you jump through the hoops it tells you to + let hasExistingRoomAtMomentRoomReleaseCalled = await hasExistingRoomAtMomentRoomReleaseCalledStreamComponents.stream.first { _ in true } + #expect(try !#require(hasExistingRoomAtMomentRoomReleaseCalled as Bool?)) + + #expect(await roomToReturn.releaseCallCount == 1) + } } diff --git a/Tests/AblyChatTests/IntegrationTests.swift b/Tests/AblyChatTests/IntegrationTests.swift index 8712fe14..fa975d70 100644 --- a/Tests/AblyChatTests/IntegrationTests.swift +++ b/Tests/AblyChatTests/IntegrationTests.swift @@ -74,5 +74,16 @@ struct IntegrationTests { // (11) Check that we received a DETACHED status change as a result of detaching the room _ = try #require(await rxRoomStatusSubscription.first { $0.current == .detached }) #expect(await rxRoom.status == .detached) + + // (12) Release the room + try await rxClient.rooms.release(roomID: roomID) + + // (13) Check that we received a RELEASED status change as a result of releasing the room + _ = try #require(await rxRoomStatusSubscription.first { $0.current == .released }) + #expect(await rxRoom.status == .released) + + // (14) Fetch the room we just released and check it’s a new object + let postReleaseRxRoom = try await rxClient.rooms.get(roomID: roomID, options: .init()) + #expect(postReleaseRxRoom !== rxRoom) } } diff --git a/Tests/AblyChatTests/Mocks/MockChannels.swift b/Tests/AblyChatTests/Mocks/MockChannels.swift index d7398051..8afb6f00 100644 --- a/Tests/AblyChatTests/Mocks/MockChannels.swift +++ b/Tests/AblyChatTests/Mocks/MockChannels.swift @@ -1,8 +1,11 @@ import Ably import AblyChat -final class MockChannels: RealtimeChannelsProtocol, Sendable { +final class MockChannels: RealtimeChannelsProtocol, @unchecked Sendable { private let channels: [MockRealtimeChannel] + private let mutex = NSLock() + /// Access must be synchronized via ``mutex``. + private(set) var _releaseArguments: [String] = [] init(channels: [MockRealtimeChannel]) { self.channels = channels @@ -24,7 +27,17 @@ final class MockChannels: RealtimeChannelsProtocol, Sendable { fatalError("Not implemented") } - func release(_: String) { - fatalError("Not implemented") + func release(_ name: String) { + mutex.lock() + defer { mutex.unlock() } + _releaseArguments.append(name) + } + + var releaseArguments: [String] { + let result: [String] + mutex.lock() + result = _releaseArguments + mutex.unlock() + return result } } diff --git a/Tests/AblyChatTests/Mocks/MockRoom.swift b/Tests/AblyChatTests/Mocks/MockRoom.swift index c2b389a4..7039e218 100644 --- a/Tests/AblyChatTests/Mocks/MockRoom.swift +++ b/Tests/AblyChatTests/Mocks/MockRoom.swift @@ -1,10 +1,13 @@ @testable import AblyChat -actor MockRoom: Room { +actor MockRoom: InternalRoom { let options: RoomOptions + private(set) var releaseCallCount = 0 + let releaseImplementation: (@Sendable () async -> Void)? - init(options: RoomOptions) { + init(options: RoomOptions, releaseImplementation: (@Sendable () async -> Void)? = nil) { self.options = options + self.releaseImplementation = releaseImplementation } nonisolated var roomID: String { @@ -46,4 +49,12 @@ actor MockRoom: Room { func detach() async throws { fatalError("Not implemented") } + + func release() async { + releaseCallCount += 1 + guard let releaseImplementation else { + fatalError("releaseImplementation must be set before calling `release`") + } + await releaseImplementation() + } } diff --git a/Tests/AblyChatTests/Mocks/MockRoomFactory.swift b/Tests/AblyChatTests/Mocks/MockRoomFactory.swift index 804fd605..f24ad3d1 100644 --- a/Tests/AblyChatTests/Mocks/MockRoomFactory.swift +++ b/Tests/AblyChatTests/Mocks/MockRoomFactory.swift @@ -1,13 +1,17 @@ @testable import AblyChat actor MockRoomFactory: RoomFactory { - private let room: MockRoom? + private var 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 setRoom(_ room: MockRoom) { + 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) diff --git a/Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift b/Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift index 429e0d78..ad1e004f 100644 --- a/Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift +++ b/Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift @@ -6,6 +6,7 @@ actor MockRoomLifecycleManager: RoomLifecycleManager { private(set) var attachCallCount = 0 private let detachResult: Result? private(set) var detachCallCount = 0 + private(set) var releaseCallCount = 0 private let _roomStatus: RoomStatus? // TODO: clean up old subscriptions (https://github.com/ably-labs/ably-chat-swift/issues/36) private var subscriptions: [Subscription] = [] @@ -32,6 +33,10 @@ actor MockRoomLifecycleManager: RoomLifecycleManager { try detachResult.get() } + func performReleaseOperation() async { + releaseCallCount += 1 + } + var roomStatus: RoomStatus { guard let roomStatus = _roomStatus else { fatalError("In order to call roomStatus, roomStatus must be passed to the initializer")