Files
letro-ios/UnitTests/Sources/StartChatViewModelTests.swift
Skye Elliot 477bf859c5 Prompt user when inviting users with uncached identities (#5331)
If the user attempts to invite someone (to a room or creating a DM) whose identity is not cached, we prompt them to make sure this was their intention.
2026-04-14 14:51:58 +03:00

137 lines
5.4 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
@testable import ElementX
import Testing
@MainActor
struct StartChatScreenViewModelTests {
private var viewModel: StartChatScreenViewModelProtocol!
private var clientProxy: ClientProxyMock!
private var userDiscoveryService: UserDiscoveryServiceMock!
private var context: StartChatScreenViewModel.Context {
viewModel.context
}
init() {
clientProxy = .init(.init(userID: ""))
userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([])
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
viewModel = StartChatScreenViewModel(userSession: userSession,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
userDiscoveryService: userDiscoveryService,
appSettings: ServiceLocator.shared.settings)
}
@Test
mutating func queryShowingNoResults() async {
await search(query: "A")
#expect(context.viewState.usersSection.type == .suggestions)
await search(query: "AA")
#expect(context.viewState.usersSection.type == .suggestions)
#expect(!userDiscoveryService.searchProfilesWithCalled)
await search(query: "AAA")
assertSearchResults(toBe: 0)
#expect(userDiscoveryService.searchProfilesWithCalled)
}
@Test
func joinRoomByAddress() async throws {
clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "id", servers: []))
let deferredViewState = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.joinByAddressState == .addressFound(address: "#room:example.com", roomID: "id")
}
viewModel.context.roomAddress = "#room:example.com"
try await deferredViewState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
action == .showRoom(roomID: "id")
}
context.send(viewAction: .joinRoomByAddress)
try await deferredAction.fulfill()
}
@Test
func joinRoomByAddressFailsBecauseInvalid() async throws {
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.joinByAddressState == .invalidAddress
}
viewModel.context.roomAddress = ":"
context.send(viewAction: .joinRoomByAddress)
try await deferred.fulfill()
}
@Test
func joinRoomByAddressFailsBecauseNotFound() async throws {
clientProxy.resolveRoomAliasReturnValue = .failure(.failedResolvingRoomAlias)
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.joinByAddressState == .addressNotFound
}
viewModel.context.roomAddress = "#room:example.com"
context.send(viewAction: .joinRoomByAddress)
try await deferred.fulfill()
}
// MARK: - History Sharing
@Test
func inviteConfirmationFetchesIdentity() async throws {
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
clientProxy.directRoomForUserIDReturnValue = .success(nil)
clientProxy.userIdentityForFallBackToServerReturnValue = .success(UserIdentityProxyMock(configuration: .init(verificationState: .notVerified)))
// User identity becomes known, i.e. not unknown
let deferred = deferFulfillment(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite)) {
!$0.isUnknown
}
context.send(viewAction: .selectUser(.mockBob))
try await deferred.fulfill()
#expect(clientProxy.userIdentityForFallBackToServerCalled)
}
@Test
func inviteConfirmationFallsBackToUnknownIdentityOnFailure() async throws {
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
clientProxy.directRoomForUserIDReturnValue = .success(nil)
clientProxy.userIdentityForFallBackToServerReturnValue = .failure(.forbiddenAccess)
// User identity never becomes known, i.e. is never not unknown
let deferred = deferFailure(viewModel.context.$viewState.compactMap(\.bindings.selectedUserToInvite), timeout: .seconds(5)) {
!$0.isUnknown
}
context.send(viewAction: .selectUser(.mockBob))
try await deferred.fulfill()
#expect(clientProxy.userIdentityForFallBackToServerCalled)
}
// MARK: - Private
private func assertSearchResults(toBe count: Int) {
#expect(count >= 0)
#expect(context.viewState.usersSection.type == .searchResult)
#expect(context.viewState.usersSection.users.count == count)
#expect(context.viewState.hasEmptySearchResults == (count == 0))
}
@discardableResult
private mutating func search(query: String) async -> StartChatScreenViewState? {
viewModel.context.searchQuery = query
return await context.$viewState.nextValue
}
}