diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index fab5810dec..408fa1fabf 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -366,7 +366,6 @@ 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; - 53228D56C0C03711286A08F1 /* ResolveTimelineItemSendFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE7B6AE6EDC1BEAE30E5B38 /* ResolveTimelineItemSendFailureView.swift */; }; 5341D48F833E3E30F16FA2A3 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; @@ -755,6 +754,7 @@ A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A8FA7671948E3DF27F320026 /* BugReportFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */; }; + A9062C5F94DF651F0D21196F /* ResolveVerifiedUserSendFailureViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */; }; A93661C962B12942C08864B6 /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 2629CF48B33643CD5F69C612 /* Prefire */; }; A9482B967FC85DA611514D35 /* VoiceMessageRoomPlaybackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */; }; A969147E0EEE0E27EE226570 /* MediaUploadPreviewScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */; }; @@ -798,6 +798,7 @@ B31E5493C99381D4E204438B /* RoomTimelineControllerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */; }; B3D652AA1654270742072FB3 /* DeveloperOptionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */; }; B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; }; + B434548FA94576FA45D93E3C /* ResolveVerifiedUserSendFailureViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */; }; B444F9C184A377C1B481F07F /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; @@ -943,6 +944,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; }; D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; + D4AB1B84F8B4DE6C3EA7C5D1 /* ResolveVerifiedUserSendFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; @@ -1779,10 +1781,10 @@ 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; - 8BE7B6AE6EDC1BEAE30E5B38 /* ResolveTimelineItemSendFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveTimelineItemSendFailureView.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; + 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureView.swift; sourceTree = ""; }; 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyle.swift; sourceTree = ""; }; 8D1FA20DAB853C1156054912 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; @@ -1892,6 +1894,7 @@ A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = ""; }; + A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureViewStateTests.swift; sourceTree = ""; }; A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8DF55467ED4CE76B7AE9A33 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2075,6 +2078,7 @@ CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureViewState.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; @@ -2580,7 +2584,8 @@ isa = PBXGroup; children = ( 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */, - 8BE7B6AE6EDC1BEAE30E5B38 /* ResolveTimelineItemSendFailureView.swift */, + 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */, + CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */, 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */, 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */, 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */, @@ -3775,6 +3780,7 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, + A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */, A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, @@ -6037,6 +6043,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, + A9062C5F94DF651F0D21196F /* ResolveVerifiedUserSendFailureViewStateTests.swift in Sources */, C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, @@ -6578,7 +6585,8 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */, 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, - 53228D56C0C03711286A08F1 /* ResolveTimelineItemSendFailureView.swift in Sources */, + D4AB1B84F8B4DE6C3EA7C5D1 /* ResolveVerifiedUserSendFailureView.swift in Sources */, + B434548FA94576FA45D93E3C /* ResolveVerifiedUserSendFailureViewState.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */, 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */, diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 9f52b90c4d..81c05f27c9 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -103,6 +103,10 @@ extension JoinedRoomProxyMock { } return .success(member) } + + resendItemIDReturnValue = .success(()) + ignoreDeviceTrustAndResendDevicesItemIDReturnValue = .success(()) + withdrawVerificationAndResendUserIDsItemIDReturnValue = .success(()) flagAsUnreadReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(()) diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift new file mode 100644 index 0000000000..06d1ff2096 --- /dev/null +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift @@ -0,0 +1,125 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { + var viewModel: TimelineViewModel! + var context: TimelineViewModel.Context! + var viewState: ResolveVerifiedUserSendFailureViewState! + + override func setUp() async throws { + viewModel = .mock + context = viewModel.context + } + + func testUnsignedDevice() async throws { + // Given a failure where a single user has an unverified device + let userID = "@alice:matrix.org" + viewState = makeViewState(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]])) + XCTAssertNotNil(context.sendFailureInfo) + + try await verifyResolving(userIDs: [userID]) + } + + func testMultipleUnsignedDevices() async throws { + // Given a failure where a multiple users have unverified devices. + let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] + let devices = Dictionary(uniqueKeysWithValues: userIDs.map { (key: $0, value: ["DEVICE1, DEVICE2"]) }) + viewState = makeViewState(with: .hasUnsignedDevice(devices: devices)) + XCTAssertNotNil(context.sendFailureInfo) + + try await verifyResolving(userIDs: userIDs, assertStrings: false) + } + + func testChangedIdentity() async throws { + // Given a failure where a single user's identity has changed. + let userID = "@alice:matrix.org" + viewState = makeViewState(with: .changedIdentity(users: [userID])) + XCTAssertNotNil(context.sendFailureInfo) + + try await verifyResolving(userIDs: [userID]) + } + + func testMultipleChangedIdentities() async throws { + // Given a failure where a multiple users have unverified devices. + let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] + viewState = makeViewState(with: .changedIdentity(users: userIDs)) + XCTAssertNotNil(context.sendFailureInfo) + + try await verifyResolving(userIDs: userIDs) + } + + // MARK: Helpers + + private func makeViewState(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureViewState { + let sendFailureInfo = TimelineItemSendFailureInfo(id: .random, failure: failure) + context.sendFailureInfo = sendFailureInfo + return ResolveVerifiedUserSendFailureViewState(info: sendFailureInfo, context: context) + } + + private func verifyResolving(userIDs: [String], assertStrings: Bool = true) async throws { + var remainingUserIDs = userIDs + + while remainingUserIDs.count > 1 { + // Verify that the strings are being updated. + if assertStrings { + verifyDisplayName(from: remainingUserIDs) + } + + // When resolving the first failure. + let deferredFailure = deferFailure(context.$viewState, timeout: 1) { $0.bindings.sendFailureInfo == nil } + viewState.resolveAndSend() + try await deferredFailure.fulfill() + + // Then the sheet should remain open for the next failure. + XCTAssertNotNil(context.sendFailureInfo) + + remainingUserIDs.removeFirst() + } + + // Verify the final string. + if assertStrings { + verifyDisplayName(from: remainingUserIDs) + } + + // When resolving the final failure. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.sendFailureInfo == nil } + viewState.resolveAndSend() + try await deferred.fulfill() + + // Then the sheet should be dismissed. + XCTAssertNil(context.sendFailureInfo) + } + + private func verifyDisplayName(from remainingUserIDs: [String]) { + guard let userID = remainingUserIDs.first else { + XCTFail("There should be a user ID to check.") + return + } + + guard let displayName = context.viewState.members[userID]?.displayName else { + XCTFail("There should be a matching mock user") + return + } + + XCTAssertTrue(viewState.title.contains(displayName)) + XCTAssertTrue(viewState.subtitle.contains(displayName)) + } +}