From 88e1c7ad13ae7c96ed8f0456f8b69a06a3ae4bd5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 14 Mar 2024 14:05:28 +0000 Subject: [PATCH] Add mute expiration support when muting a channel (#3083) --- CHANGELOG.md | 4 +- .../DemoChatChannelListRouter.swift | 13 +++ .../Endpoints/ChannelEndpoints.swift | 14 +++- .../Payloads/MutedChannelPayload.swift | 9 +- .../ChannelController/ChannelController.swift | 10 ++- .../StreamChat/Database/DTOs/ChannelDTO.swift | 5 +- .../Database/DTOs/ChannelMuteDTO.swift | 2 + .../StreamChatModel.xcdatamodel/contents | 3 +- Sources/StreamChat/Models/MuteDetails.swift | 2 + .../ChannelReadUpdaterMiddleware.swift | 6 +- .../StreamChat/Workers/ChannelUpdater.swift | 5 +- .../Workers/ChannelUpdater_Mock.swift | 4 +- .../Endpoints/ChannelEndpoints_Tests.swift | 29 +++++++ .../ChannelController_Tests.swift | 84 +++++++++++++++++++ .../Database/DTOs/ChannelMuteDTO_Tests.swift | 18 ++-- .../Workers/ChannelUpdater_Tests.swift | 52 +++++++++++- 16 files changed, 234 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06dc538e47b..1c83f7980ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +## StreamChat +### ✅ Added +- Add mute expiration support when muting a channel [#3083](https://github.com/GetStream/stream-chat-swift/pull/3083) # [4.50.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.50.0) _March 11, 2024_ diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift index cff434a5a16..ee0468b0f1a 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -298,6 +298,19 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { } } }), + .init(title: "Mute channel with expiration", isEnabled: canMuteChannel, handler: { [unowned self] _ in + self.rootViewController.presentAlert(title: "Enter expiration", textFieldPlaceholder: "Seconds") { expiration in + guard let expiration = Int(expiration ?? ""), expiration != 0 else { + self.rootViewController.presentAlert(title: "Expiration is not valid") + return + } + channelController.muteChannel(expiration: expiration * 1000) { error in + if let error = error { + self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)") + } + } + } + }), .init(title: "Cool channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in channelController.partialChannelUpdate(extraData: ["is_cool": true]) { error in if let error = error { diff --git a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift index 135fc7c9d87..6894a13c5f9 100644 --- a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift +++ b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift @@ -58,14 +58,20 @@ extension Endpoint { body: body ) } - - static func muteChannel(cid: ChannelId, mute: Bool) -> Endpoint { - .init( + + static func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil) -> Endpoint { + var body: [String: AnyEncodable] = ["channel_cid": AnyEncodable(cid)] + + if let expiration = expiration { + body["expiration"] = AnyEncodable(expiration) + } + + return .init( path: .muteChannel(mute), method: .post, queryItems: nil, requiresConnectionId: true, - body: ["channel_cid": cid] + body: body ) } diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MutedChannelPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MutedChannelPayload.swift index e5e8ffdc9a5..d8f73bee74b 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MutedChannelPayload.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MutedChannelPayload.swift @@ -4,30 +4,34 @@ import Foundation -/// An object describing the incoming muted-user JSON payload. +/// An object describing the incoming muted-channel JSON payload. struct MutedChannelPayload: Decodable { private enum CodingKeys: String, CodingKey { case mutedChannel = "channel" case user case createdAt = "created_at" case updatedAt = "updated_at" + case expiresAt = "expires" } let mutedChannel: ChannelDetailPayload let user: UserPayload let createdAt: Date let updatedAt: Date + let expiresAt: Date? init( mutedChannel: ChannelDetailPayload, user: UserPayload, createdAt: Date, - updatedAt: Date + updatedAt: Date, + expiresAt: Date? = nil ) { self.mutedChannel = mutedChannel self.user = user self.createdAt = createdAt self.updatedAt = updatedAt + self.expiresAt = expiresAt } init(from decoder: Decoder) throws { @@ -36,5 +40,6 @@ struct MutedChannelPayload: Decodable { user = try container.decode(UserPayload.self, forKey: .user) createdAt = try container.decode(Date.self, forKey: .createdAt) updatedAt = try container.decode(Date.self, forKey: .createdAt) + expiresAt = try container.decodeIfPresent(Date.self, forKey: .expiresAt) } } diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift index 9e39d5b2d90..c2e2be08e18 100644 --- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift +++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift @@ -306,17 +306,19 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP /// Mutes the channel this controller manages. /// - /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. - /// If request fails, the completion will be called with an error. + /// - Parameters: + /// - expiration: Duration of mute in milliseconds. + /// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished. + /// If request fails, the completion will be called with an error. /// - public func muteChannel(completion: ((Error?) -> Void)? = nil) { + public func muteChannel(expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) return } - updater.muteChannel(cid: cid, mute: true) { error in + updater.muteChannel(cid: cid, mute: true, expiration: expiration) { error in self.callback { completion?(error) } diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift index 77f2dd83e48..7b1318821db 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift @@ -451,7 +451,7 @@ extension ChatChannel { return .noUnread } } - + let fetchMessages: () -> [ChatMessage] = { guard dto.isValid else { return [] } return MessageDTO @@ -494,7 +494,8 @@ extension ChatChannel { return .init( createdAt: mute.createdAt.bridgeDate, - updatedAt: mute.updatedAt.bridgeDate + updatedAt: mute.updatedAt.bridgeDate, + expiresAt: mute.expiresAt?.bridgeDate ) } diff --git a/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift index 30ae6e522f6..7422422fe69 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift @@ -9,6 +9,7 @@ import Foundation final class ChannelMuteDTO: NSManagedObject { @NSManaged var createdAt: DBDate @NSManaged var updatedAt: DBDate + @NSManaged var expiresAt: DBDate? @NSManaged var channel: ChannelDTO @NSManaged var currentUser: CurrentUserDTO @@ -49,6 +50,7 @@ extension NSManagedObjectContext { dto.currentUser = currentUser dto.createdAt = payload.createdAt.bridgeDate dto.updatedAt = payload.updatedAt.bridgeDate + dto.expiresAt = payload.expiresAt?.bridgeDate return dto } diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents index 52c655a932b..58b0aea7b9f 100644 --- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents +++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -110,6 +110,7 @@ + diff --git a/Sources/StreamChat/Models/MuteDetails.swift b/Sources/StreamChat/Models/MuteDetails.swift index cde7b118431..aa30222e277 100644 --- a/Sources/StreamChat/Models/MuteDetails.swift +++ b/Sources/StreamChat/Models/MuteDetails.swift @@ -10,4 +10,6 @@ public struct MuteDetails: Equatable { public let createdAt: Date /// The time when the mute was updated. public let updatedAt: Date? + /// The expiration date of the pinning. Infinite expiration in case it is `nil`. + public let expiresAt: Date? } diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift index 81c576e8900..fa23c774f2c 100644 --- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift +++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift @@ -196,8 +196,10 @@ struct ChannelReadUpdaterMiddleware: EventMiddleware { channelRead: ChannelReadDTO, message: MessagePayload ) -> UnreadSkippingReason? { - if channelRead.channel.mute != nil { - return .channelIsMuted + if let mute = channelRead.channel.mute { + guard let expiredDate = mute.expiresAt, expiredDate.bridgeDate <= Date() else { + return .channelIsMuted + } } if message.user.id == currentUser.user.id { diff --git a/Sources/StreamChat/Workers/ChannelUpdater.swift b/Sources/StreamChat/Workers/ChannelUpdater.swift index 39b1b020039..481ff4cad69 100644 --- a/Sources/StreamChat/Workers/ChannelUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelUpdater.swift @@ -125,9 +125,10 @@ class ChannelUpdater: Worker { /// - Parameters: /// - cid: The channel identifier. /// - mute: Defines if the channel with the specified **cid** should be muted. + /// - expiration: Duration of mute in milliseconds. /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. - func muteChannel(cid: ChannelId, mute: Bool, completion: ((Error?) -> Void)? = nil) { - apiClient.request(endpoint: .muteChannel(cid: cid, mute: mute)) { + func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) { + apiClient.request(endpoint: .muteChannel(cid: cid, mute: mute, expiration: expiration)) { completion?($0.error) } } diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift index f013a814c31..9e91c07bb60 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift @@ -21,6 +21,7 @@ final class ChannelUpdater_Mock: ChannelUpdater { @Atomic var muteChannel_cid: ChannelId? @Atomic var muteChannel_mute: Bool? + @Atomic var muteChannel_expiration: Int? @Atomic var muteChannel_completion: ((Error?) -> Void)? @Atomic var deleteChannel_cid: ChannelId? @@ -239,9 +240,10 @@ final class ChannelUpdater_Mock: ChannelUpdater { partialChannelUpdate_completion = completion } - override func muteChannel(cid: ChannelId, mute: Bool, completion: ((Error?) -> Void)? = nil) { + override func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) { muteChannel_cid = cid muteChannel_mute = mute + muteChannel_expiration = expiration muteChannel_completion = completion } diff --git a/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift index 01f4b8bce04..6a0c58e1e7b 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift @@ -212,6 +212,35 @@ final class ChannelEndpoints_Tests: XCTestCase { XCTAssertEqual(mute ? "moderation/mute/channel" : "moderation/unmute/channel", endpoint.path.value) } } + + func test_muteChannelWithExpiration_buildsCorrectly() { + let testCases = [true, false] + let expiration = 1_000_000 + + for mute in testCases { + let channelID = ChannelId.unique + + let body: [String: AnyEncodable] = [ + "channel_cid": AnyEncodable(channelID), + "expiration": AnyEncodable(expiration) + ] + + let expectedEndpoint = Endpoint( + path: .muteChannel(mute), + method: .post, + queryItems: nil, + requiresConnectionId: true, + body: body + ) + + // Build endpoint + let endpoint: Endpoint = .muteChannel(cid: channelID, mute: mute, expiration: expiration) + + // Assert endpoint is built correctly + XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) + XCTAssertEqual(mute ? "moderation/mute/channel" : "moderation/unmute/channel", endpoint.path.value) + } + } func test_showChannel_buildsCorrectly() { let cid = ChannelId.unique diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift index 79070630792..35a76d27c2d 100644 --- a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift @@ -1926,6 +1926,37 @@ final class ChannelController_Tests: XCTestCase { XCTAssertNil(error) } + + func test_muteChannelWithExpiration_failsForNewChannels() throws { + let expiration = 1_000_000 + + // Create `ChannelController` for new channel + let query = ChannelQuery(channelPayload: .unique) + setupControllerForNewChannel(query: query) + + // Simulate `muteChannel` call and assert error is returned + var error: Error? = try waitFor { [callbackQueueID] completion in + controller.muteChannel(expiration: expiration) { error in + AssertTestQueue(withId: callbackQueueID) + completion(error) + } + } + XCTAssert(error is ClientError.ChannelNotCreatedYet) + + // Simulate successful backend channel creation + env.channelUpdater!.update_onChannelCreated?(query.cid!) + + // Simulate `muteChannel` call and assert no error is returned + error = try waitFor { [callbackQueueID] completion in + controller.muteChannel(expiration: expiration) { error in + AssertTestQueue(withId: callbackQueueID) + completion(error) + } + env.channelUpdater!.muteChannel_completion?(nil) + } + + XCTAssertNil(error) + } func test_muteChannel_callsChannelUpdater() { // Simulate `muteChannel` call and catch the completion @@ -1958,6 +1989,41 @@ final class ChannelController_Tests: XCTestCase { // `weakController` should be deallocated too AssertAsync.canBeReleased(&weakController) } + + func test_muteChannelWithExpiration_callsChannelUpdater() { + let expiration = 1_000_000 + + // Simulate `muteChannel` call and catch the completion + var completionCalled = false + controller.muteChannel(expiration: expiration) { [callbackQueueID] error in + AssertTestQueue(withId: callbackQueueID) + XCTAssertNil(error) + completionCalled = true + } + + // Keep a weak ref so we can check if it's actually deallocated + weak var weakController = controller + + // (Try to) deallocate the controller + // by not keeping any references to it + controller = nil + + // Assert cid, muted state and expiration are passed to `channelUpdater`, completion is not called yet + XCTAssertEqual(env.channelUpdater!.muteChannel_cid, channelId) + XCTAssertEqual(env.channelUpdater!.muteChannel_mute, true) + XCTAssertEqual(env.channelUpdater!.muteChannel_expiration, expiration) + XCTAssertFalse(completionCalled) + + // Simulate successful update + env.channelUpdater!.muteChannel_completion?(nil) + // Release reference of completion so we can deallocate stuff + env.channelUpdater!.muteChannel_completion = nil + + // Assert completion is called + AssertAsync.willBeTrue(completionCalled) + // `weakController` should be deallocated too + AssertAsync.canBeReleased(&weakController) + } func test_muteChannel_propagatesErrorFromUpdater() { // Simulate `muteChannel` call and catch the completion @@ -1974,6 +2040,24 @@ final class ChannelController_Tests: XCTestCase { // Completion should be called with the error AssertAsync.willBeEqual(completionCalledError as? TestError, testError) } + + func test_muteChannelWithExpiration_propagatesErrorFromUpdater() { + let expiration = 1_000_000 + + // Simulate `muteChannel` call and catch the completion + var completionCalledError: Error? + controller.muteChannel(expiration: expiration) { [callbackQueueID] in + AssertTestQueue(withId: callbackQueueID) + completionCalledError = $0 + } + + // Simulate failed update + let testError = TestError() + env.channelUpdater!.muteChannel_completion?(testError) + + // Completion should be called with the error + AssertAsync.willBeEqual(completionCalledError as? TestError, testError) + } // MARK: - Unmuting channel diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelMuteDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelMuteDTO_Tests.swift index 1964a6b12a0..d77abb3a629 100644 --- a/Tests/StreamChatTests/Database/DTOs/ChannelMuteDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/ChannelMuteDTO_Tests.swift @@ -31,7 +31,8 @@ final class ChannelMuteDTO_Tests: XCTestCase { mutedChannel: .dummy(cid: .unique), user: currentUserPayload, createdAt: .unique, - updatedAt: .unique + updatedAt: .unique, + expiresAt: .unique ) try database.writeSynchronously { session in @@ -42,6 +43,7 @@ final class ChannelMuteDTO_Tests: XCTestCase { let channel: ChatChannel = try XCTUnwrap(database.viewContext.channel(cid: mutePayload.mutedChannel.cid)?.asModel()) XCTAssertEqual(channel.muteDetails?.createdAt, mutePayload.createdAt) XCTAssertEqual(channel.muteDetails?.updatedAt, mutePayload.updatedAt) + XCTAssertEqual(channel.muteDetails?.expiresAt, mutePayload.expiresAt) let currentUser: CurrentChatUser = try XCTUnwrap(database.viewContext.currentUser?.asModel()) XCTAssertEqual(currentUser.mutedChannels, [channel]) @@ -53,7 +55,8 @@ final class ChannelMuteDTO_Tests: XCTestCase { mutedChannel: .dummy(cid: .unique), user: .dummy(userId: .unique), createdAt: .unique, - updatedAt: .unique + updatedAt: .unique, + expiresAt: .unique ) // WHEN @@ -71,7 +74,8 @@ final class ChannelMuteDTO_Tests: XCTestCase { mutedChannel: channel, user: currentUser, createdAt: .unique, - updatedAt: .unique + updatedAt: .unique, + expiresAt: .unique ) var loadedMuteDTO: ChannelMuteDTO? { @@ -89,6 +93,7 @@ final class ChannelMuteDTO_Tests: XCTestCase { let muteDTO = try XCTUnwrap(loadedMuteDTO) XCTAssertEqual(muteDTO.createdAt.bridgeDate, mute.createdAt) XCTAssertEqual(muteDTO.updatedAt.bridgeDate, mute.updatedAt) + XCTAssertEqual(muteDTO.expiresAt?.bridgeDate, mute.expiresAt) XCTAssertEqual(muteDTO.currentUser.user.id, currentUser.id) XCTAssertEqual(muteDTO.channel.cid, channel.cid.rawValue) } @@ -101,7 +106,8 @@ final class ChannelMuteDTO_Tests: XCTestCase { mutedChannel: channel, user: currentUser, createdAt: .unique, - updatedAt: .unique + updatedAt: .unique, + expiresAt: .unique ) try database.writeSynchronously { session in @@ -114,7 +120,8 @@ final class ChannelMuteDTO_Tests: XCTestCase { mutedChannel: channel, user: currentUser, createdAt: .unique, - updatedAt: .unique + updatedAt: .unique, + expiresAt: .unique ) try database.writeSynchronously { session in try session.saveChannelMute(payload: updatedMute) @@ -126,6 +133,7 @@ final class ChannelMuteDTO_Tests: XCTestCase { ) XCTAssertEqual(muteDTO.createdAt.bridgeDate, updatedMute.createdAt) XCTAssertEqual(muteDTO.updatedAt.bridgeDate, updatedMute.updatedAt) + XCTAssertEqual(muteDTO.expiresAt?.bridgeDate, updatedMute.expiresAt) XCTAssertEqual(muteDTO.currentUser.user.id, currentUser.id) XCTAssertEqual(muteDTO.channel.cid, channel.cid.rawValue) } diff --git a/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift index 4f7a339c0ff..6d1b72e9381 100644 --- a/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift @@ -771,6 +771,19 @@ final class ChannelUpdater_Tests: XCTestCase { let referenceEndpoint: Endpoint = .muteChannel(cid: channelID, mute: mute) XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint)) } + + func test_muteChannelWithExpiration_makesCorrectAPICall() { + let channelID = ChannelId.unique + let mute = true + let expiration = 1_000_000 + + // Simulate `muteChannel(cid:, mute:, completion:)` call + channelUpdater.muteChannel(cid: channelID, mute: mute, expiration: expiration) + + // Assert correct endpoint is called + let referenceEndpoint: Endpoint = .muteChannel(cid: channelID, mute: mute, expiration: expiration) + XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint)) + } func test_muteChannel_successfulResponse_isPropagatedToCompletion() { // Simulate `muteChannel(cid:, mute:, completion:)` call @@ -789,6 +802,26 @@ final class ChannelUpdater_Tests: XCTestCase { // Assert completion is called AssertAsync.willBeTrue(completionCalled) } + + func test_muteChannelWithExpiration_successfulResponse_isPropagatedToCompletion() { + let expiration = 1_000_000 + + // Simulate `muteChannel(cid:, mute:, completion:, expiration:)` call + var completionCalled = false + channelUpdater.muteChannel(cid: .unique, mute: true, expiration: expiration) { error in + XCTAssertNil(error) + completionCalled = true + } + + // Assert completion is not called yet + XCTAssertFalse(completionCalled) + + // Simulate API response with success + apiClient.test_simulateResponse(Result.success(.init())) + + // Assert completion is called + AssertAsync.willBeTrue(completionCalled) + } func test_muteChannel_errorResponse_isPropagatedToCompletion() { // Simulate `muteChannel(cid:, mute:, completion:)` call @@ -802,6 +835,21 @@ final class ChannelUpdater_Tests: XCTestCase { // Assert the completion is called with the error AssertAsync.willBeEqual(completionCalledError as? TestError, error) } + + func test_muteChannelWithExpiration_errorResponse_isPropagatedToCompletion() { + let expiration = 1_000_000 + + // Simulate `muteChannel(cid:, mute:, completion:, expiration:)` call + var completionCalledError: Error? + channelUpdater.muteChannel(cid: .unique, mute: true, expiration: expiration) { completionCalledError = $0 } + + // Simulate API response with failure + let error = TestError() + apiClient.test_simulateResponse(Result.failure(error)) + + // Assert the completion is called with the error + AssertAsync.willBeEqual(completionCalledError as? TestError, error) + } // MARK: - Delete channel @@ -1174,7 +1222,7 @@ final class ChannelUpdater_Tests: XCTestCase { let channelID = ChannelId.unique let userIds: Set = Set([UserId.unique]) - // Simulate `muteChannel(cid:, mute:, completion:)` call + // Simulate `addMembers(cid:, userIds:, hideHistory:)` call var completionCalledError: Error? channelUpdater.addMembers(cid: channelID, userIds: userIds, hideHistory: false) { completionCalledError = $0 } @@ -1225,7 +1273,7 @@ final class ChannelUpdater_Tests: XCTestCase { let channelID = ChannelId.unique let userIds: Set = Set([UserId.unique]) - // Simulate `muteChannel(cid:, mute:, completion:)` call + // Simulate `inviteMembers(cid:, channelID:, userIds:)` call var completionCalledError: Error? channelUpdater.inviteMembers(cid: channelID, userIds: userIds) { completionCalledError = $0 }