From 548b5a15d1b0e9855f83d8e53a96622fb83e00df Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Tue, 12 Nov 2024 17:27:25 +0000 Subject: [PATCH] Add test coverage --- .../Fixtures/JSONs/Member.json | 1 + .../Workers/ChannelMemberUpdater_Mock.swift | 29 +++++++ .../Endpoints/ChannelEndpoints_Tests.swift | 2 +- .../Endpoints/EndpointPath_Tests.swift | 11 +++ .../Endpoints/MemberEndpoints_Tests.swift | 30 +++++++ .../Payloads/MemberPayload_Tests.swift | 1 + .../ChannelController_Tests.swift | 16 ++-- .../MemberController_Tests.swift | 61 ++++++++++++++ .../Database/DTOs/MemberModelDTO_Tests.swift | 4 +- .../StateLayer/Chat_Tests.swift | 6 +- .../Workers/ChannelMemberUpdater_Tests.swift | 79 +++++++++++++++++++ 11 files changed, 227 insertions(+), 13 deletions(-) diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Member.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Member.json index 3e1d0b6cdad..6bcd7af38b1 100644 --- a/TestTools/StreamChatTestTools/Fixtures/JSONs/Member.json +++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Member.json @@ -7,6 +7,7 @@ "ban_expires" : "2021-03-08T15:42:31.355923Z", "user_id" : "broken-waterfall-5", "channel_role" : "owner", + "is_premium": true, "user" : { "id" : "broken-waterfall-5", "banned" : false, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift index e0b7a67311e..24ee5c13813 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelMemberUpdater_Mock.swift @@ -20,6 +20,13 @@ final class ChannelMemberUpdater_Mock: ChannelMemberUpdater { @Atomic var unbanMember_completion: ((Error?) -> Void)? @Atomic var unbanMember_completion_result: Result? + @Atomic var partialUpdate_userId: UserId? + @Atomic var partialUpdate_cid: ChannelId? + @Atomic var partialUpdate_extraData: [String: RawJSON]? + @Atomic var partialUpdate_unset: [String]? + @Atomic var partialUpdate_completion: ((Result) -> Void)? + @Atomic var partialUpdate_completion_result: Result? + func cleanUp() { banMember_userId = nil banMember_cid = nil @@ -33,6 +40,13 @@ final class ChannelMemberUpdater_Mock: ChannelMemberUpdater { unbanMember_cid = nil unbanMember_completion = nil unbanMember_completion_result = nil + + partialUpdate_userId = nil + partialUpdate_cid = nil + partialUpdate_extraData = nil + partialUpdate_unset = nil + partialUpdate_completion = nil + partialUpdate_completion_result = nil } override func banMember( @@ -61,4 +75,19 @@ final class ChannelMemberUpdater_Mock: ChannelMemberUpdater { unbanMember_completion = completion unbanMember_completion_result?.invoke(with: completion) } + + override func partialUpdate( + userId: UserId, + in cid: ChannelId, + extraData: [String: RawJSON]?, + unset: [String]?, + completion: @escaping ((Result) -> Void) + ) { + partialUpdate_userId = userId + partialUpdate_cid = cid + partialUpdate_extraData = extraData + partialUpdate_unset = unset + partialUpdate_completion = completion + partialUpdate_completion_result?.invoke(with: completion) + } } diff --git a/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift index edf4ee930d4..b9a84400fa7 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift @@ -299,7 +299,7 @@ final class ChannelEndpoints_Tests: XCTestCase { func test_addMembers_buildsCorrectly() { let cid = ChannelId.unique let userIds: Set = Set([UserId.unique]) - let members = userIds.map { MemberInfoRequest(userId: $0, extraData: nil) } + let members = userIds.map { MemberInfoRequest(userId: $0, extraData: ["is_premium": true]) } let expectedEndpoint = Endpoint( path: .channelUpdate(cid.apiPath), diff --git a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift index c6e55ed6c04..4cd58e00010 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift @@ -70,6 +70,16 @@ final class EndpointPathTests: XCTestCase { XCTAssertFalse(EndpointPath.unread.shouldBeQueuedOffline) } + func test_partialMemberUpdate_shouldNOTBeQueuedOffline() { + XCTAssertFalse(EndpointPath.partialMemberUpdate(userId: "1", cid: .unique).shouldBeQueuedOffline) + } + + func test_partialMemberUpdate_value() { + let cid = ChannelId.unique + let path = EndpointPath.partialMemberUpdate(userId: "1", cid: cid).value + XCTAssertEqual(path, "channels/\(cid.apiPath)/member/1") + } + // MARK: - Codable func test_isProperlyEncodedAndDecoded() throws { @@ -78,6 +88,7 @@ final class EndpointPathTests: XCTestCase { assertResultEncodingAndDecoding(.users) assertResultEncodingAndDecoding(.guest) assertResultEncodingAndDecoding(.members) + assertResultEncodingAndDecoding(.partialMemberUpdate(userId: "1", cid: .init(type: .messaging, id: "2"))) assertResultEncodingAndDecoding(.search) assertResultEncodingAndDecoding(.devices) assertResultEncodingAndDecoding(.threads) diff --git a/Tests/StreamChatTests/APIClient/Endpoints/MemberEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/MemberEndpoints_Tests.swift index f6a0585d63a..bc8f5abe5ec 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/MemberEndpoints_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/MemberEndpoints_Tests.swift @@ -29,4 +29,34 @@ final class MemberEndpoints_Tests: XCTestCase { XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) XCTAssertEqual("members", endpoint.path.value) } + + func test_partialMemberUpdate_buildsCorrectly() { + let userId: UserId = "test-user" + let cid: ChannelId = .unique + let extraData: [String: RawJSON] = ["is_premium": .bool(true)] + let unset: [String] = ["is_cool"] + + let body: [String: AnyEncodable] = [ + "set": AnyEncodable(["is_premium": true]), + "unset": AnyEncodable(["is_cool"]) + ] + let expectedEndpoint = Endpoint( + path: .partialMemberUpdate(userId: userId, cid: cid), + method: .patch, + queryItems: nil, + requiresConnectionId: false, + body: body + ) + + // Build endpoint. + let endpoint: Endpoint = .partialMemberUpdate( + userId: userId, + cid: cid, + extraData: extraData, + unset: unset + ) + + // Assert endpoint is built correctly. + XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) + } } diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MemberPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MemberPayload_Tests.swift index d9a2a49ba84..c3d5838b513 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MemberPayload_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MemberPayload_Tests.swift @@ -20,6 +20,7 @@ final class MemberPayload_Tests: XCTestCase { XCTAssertEqual(payload.isBanned, true) XCTAssertEqual(payload.isShadowBanned, true) XCTAssertEqual(payload.notificationsMuted, true) + XCTAssertEqual(payload.extraData?["is_premium"], true) XCTAssertNotNil(payload.user) XCTAssertEqual(payload.user!.id, "broken-waterfall-5") diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift index 492e6d16fab..03edaeb3051 100644 --- a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift @@ -3473,11 +3473,11 @@ final class ChannelController_Tests: XCTestCase { // Create `ChannelController` for new channel let query = ChannelQuery(channelPayload: .unique) setupControllerForNewChannel(query: query) - let members: Set = [.unique] + let members: [MemberInfo] = [.init(userId: .unique, extraData: nil)] // Simulate `addMembers` call and assert error is returned var error: Error? = try waitFor { [callbackQueueID] completion in - controller.addMembers(userIds: members) { error in + controller.addMembers(members) { error in AssertTestQueue(withId: callbackQueueID) completion(error) } @@ -3489,7 +3489,7 @@ final class ChannelController_Tests: XCTestCase { // Simulate `addMembers` call and assert no error is returned error = try waitFor { [callbackQueueID] completion in - controller.addMembers(userIds: members) { error in + controller.addMembers(members) { error in AssertTestQueue(withId: callbackQueueID) completion(error) } @@ -3500,11 +3500,11 @@ final class ChannelController_Tests: XCTestCase { } func test_addMembers_callsChannelUpdater() { - let members: Set = [.unique] + let members: [MemberInfo] = [.init(userId: .unique, extraData: ["is_premium": true])] // Simulate `addMembers` call and catch the completion var completionCalled = false - controller.addMembers(userIds: members) { [callbackQueueID] error in + controller.addMembers(members) { [callbackQueueID] error in AssertTestQueue(withId: callbackQueueID) XCTAssertNil(error) completionCalled = true @@ -3519,7 +3519,7 @@ final class ChannelController_Tests: XCTestCase { // Assert cid and members state are passed to `channelUpdater`, completion is not called yet XCTAssertEqual(env.channelUpdater!.addMembers_cid, channelId) - XCTAssertEqual(env.channelUpdater!.addMembers_userIds, members) + XCTAssertEqual(env.channelUpdater!.addMembers_memberInfos?.map(\.userId), members.map(\.userId)) XCTAssertFalse(completionCalled) // Simulate successful update @@ -3534,11 +3534,11 @@ final class ChannelController_Tests: XCTestCase { } func test_addMembers_propagatesErrorFromUpdater() { - let members: Set = [.unique] + let members: [MemberInfo] = [.init(userId: .unique, extraData: nil)] // Simulate `addMembers` call and catch the completion var completionCalledError: Error? - controller.addMembers(userIds: members) { [callbackQueueID] in + controller.addMembers(members) { [callbackQueueID] in AssertTestQueue(withId: callbackQueueID) completionCalledError = $0 } diff --git a/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift b/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift index 2aea5d0c54a..d3ef0d8d692 100644 --- a/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/MemberController/MemberController_Tests.swift @@ -483,6 +483,67 @@ final class MemberController_Tests: XCTestCase { XCTAssertEqual(env.memberUpdater!.unbanMember_userId, controller.userId) XCTAssertEqual(env.memberUpdater!.unbanMember_cid, controller.cid) } + + // MARK: - Partial Update + + func test_partialUpdate_propagatesError() { + let expectedError = TestError() + + // Simulate `partialUpdate` call and catch the completion + var receivedResult: Result? + controller.partialUpdate(extraData: ["key": .string("value")], unsetProperties: ["field"]) { [callbackQueueID] result in + AssertTestQueue(withId: callbackQueueID) + receivedResult = result + } + + // Simulate network response with error + env.memberUpdater!.partialUpdate_completion?(.failure(expectedError)) + + // Assert error is propagated + AssertAsync.willBeEqual(receivedResult?.error as? TestError, expectedError) + } + + func test_partialUpdate_propagatesSuccess() { + let expectedMember: ChatChannelMember = .mock(id: .unique) + + // Simulate `partialUpdate` call and catch the completion + var receivedResult: Result? + controller.partialUpdate(extraData: ["key": .string("value")], unsetProperties: ["field"]) { [callbackQueueID] result in + AssertTestQueue(withId: callbackQueueID) + receivedResult = result + } + + // 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 + + // Simulate successful network response + env.memberUpdater!.partialUpdate_completion?(.success(expectedMember)) + // Release reference of completion so we can deallocate stuff + env.memberUpdater!.partialUpdate_completion = nil + + // Assert success is propagated + AssertAsync.willBeEqual(receivedResult?.value?.id, expectedMember.id) + // `weakController` should be deallocated too + AssertAsync.canBeReleased(&weakController) + } + + func test_partialUpdate_callsMemberUpdater_withCorrectValues() { + let extraData: [String: RawJSON] = ["key": .string("value")] + let unsetProperties = ["field1", "field2"] + + // Simulate `partialUpdate` call + controller.partialUpdate(extraData: extraData, unsetProperties: unsetProperties) + + // Assert updater is called with correct values + XCTAssertEqual(env.memberUpdater!.partialUpdate_userId, controller.userId) + XCTAssertEqual(env.memberUpdater!.partialUpdate_cid, controller.cid) + XCTAssertEqual(env.memberUpdater!.partialUpdate_extraData, extraData) + XCTAssertEqual(env.memberUpdater!.partialUpdate_unset, unsetProperties) + } } private class TestEnvironment { diff --git a/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift index d737c8671c9..69952adc0cc 100644 --- a/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/MemberModelDTO_Tests.swift @@ -50,7 +50,8 @@ final class MemberModelDTO_Tests: XCTestCase { banExpiresAt: .unique, isBanned: true, isShadowBanned: true, - notificationsMuted: true + notificationsMuted: true, + extraData: ["is_premium": .bool(true)] ) // Asynchronously save the payload to the db @@ -82,6 +83,7 @@ final class MemberModelDTO_Tests: XCTestCase { Assert.willBeEqual(payload.user!.extraData, loadedMember?.extraData) Assert.willBeEqual(Set(payload.user!.teams), loadedMember?.teams) Assert.willBeEqual(payload.user!.language!, loadedMember?.language?.languageCode) + Assert.willBeEqual(true, loadedMember?.memberExtraData["is_premium"]?.boolValue) } } diff --git a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift index 768511f8ede..498fc3b91ba 100644 --- a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift @@ -209,10 +209,10 @@ final class Chat_Tests: XCTestCase { func test_addMembers_whenChannelUpdaterSucceeds_thenAddMembersSucceeds() async throws { for hideHistory in [true, false] { env.channelUpdaterMock.addMembers_completion_result = .success(()) - let memberIds: [UserId] = [.unique, .unique] - try await chat.addMembers(memberIds, systemMessage: "My system message", hideHistory: hideHistory) + let members: [MemberInfo] = [.init(userId: .unique, extraData: nil), .init(userId: .unique, extraData: nil)] + try await chat.addMembers(members, systemMessage: "My system message", hideHistory: hideHistory) XCTAssertEqual(channelId, env.channelUpdaterMock.addMembers_cid) - XCTAssertEqual(memberIds.sorted(), env.channelUpdaterMock.addMembers_userIds?.sorted()) + XCTAssertEqual(members.map(\.userId).sorted(), env.channelUpdaterMock.addMembers_userIds?.sorted()) XCTAssertEqual("My system message", env.channelUpdaterMock.addMembers_message) XCTAssertEqual(hideHistory, env.channelUpdaterMock.addMembers_hideHistory) XCTAssertEqual(currentUserId, env.channelUpdaterMock.addMembers_currentUserId) diff --git a/Tests/StreamChatTests/Workers/ChannelMemberUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelMemberUpdater_Tests.swift index e47d624aa8e..f26dfb5fbf0 100644 --- a/Tests/StreamChatTests/Workers/ChannelMemberUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/ChannelMemberUpdater_Tests.swift @@ -135,4 +135,83 @@ final class ChannelMemberUpdater_Tests: XCTestCase { // Assert the completion is called with the error AssertAsync.willBeEqual(completionCalledError as? TestError, error) } + + // MARK: - Partial Update + + func test_partialUpdate_makesCorrectAPICall() { + let userId: UserId = .unique + let cid: ChannelId = .unique + let extraData: [String: RawJSON] = ["key": .string("value")] + let unset: [String] = ["field1"] + + // Simulate `partialUpdate` call + updater.partialUpdate( + userId: userId, + in: cid, + extraData: extraData, + unset: unset, + completion: { _ in } + ) + + // Assert correct endpoint is called + XCTAssertEqual( + apiClient.request_endpoint, + AnyEndpoint( + .partialMemberUpdate( + userId: userId, + cid: cid, + extraData: extraData, + unset: unset + ) + ) + ) + } + + func test_partialUpdate_propagatesSuccessfulResponse() { + let cid: ChannelId = .unique + let memberPayload: MemberPayload = .dummy() + + // Simulate `partialUpdate` call + var completionResult: Result? + updater.partialUpdate( + userId: .unique, + in: cid, + extraData: nil, + unset: nil + ) { result in + completionResult = result + } + + // Simulate API response with success + let response = PartialMemberUpdateResponse(channelMember: memberPayload) + apiClient.test_simulateResponse(Result.success(response)) + + // Assert completion is called with the member + AssertAsync { + Assert.willBeTrue(completionResult?.value?.id == memberPayload.userId) + } + } + + func test_partialUpdate_propagatesError() { + // Simulate `partialUpdate` call + var completionResult: Result? + updater.partialUpdate( + userId: .unique, + in: .unique, + extraData: nil, + unset: nil + ) { result in + completionResult = result + } + + // Simulate API response with failure + let error = TestError() + apiClient.test_simulateResponse(Result.failure(error)) + + // Assert the completion is called with the error + AssertAsync { + Assert.willBeTrue(completionResult?.isError == true) + Assert.willBeEqual(completionResult?.error as? TestError, error) + } + } }