Files
letro-ios/UnitTests/Sources/LoginScreenViewModelTests.swift
Mauro 173b39a07f Swift Testing for Unit Tests PART 1 (#5119)
* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment

more tests migration

Cleaned the code manually to establish some good patterns

more code improvements

some more code improvements

removed empty tests

update project

* more pr suggestions and cleanups

* removed the TestSetup pattern

* fixing claude not reusing tests

* pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
2026-02-19 16:20:47 +01:00

251 lines
10 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
@Suite
struct LoginScreenViewModelTests {
var viewModel: LoginScreenViewModelProtocol!
var context: LoginScreenViewModelType.Context {
viewModel.context
}
var clientFactory: AuthenticationClientFactoryMock!
var service: AuthenticationServiceProtocol!
@Test
mutating func basicServer() async {
// Given the view model configured for a basic server example.com that only supports password authentication.
await setupViewModel()
// Then the view state should be updated with the homeserver and show the login form.
#expect(context.viewState.homeserver == .mockBasicServer,
"The homeserver data should should match the new homeserver.")
#expect(context.viewState.loginMode == .password,
"The login form should be shown.")
}
@Test
mutating func usernameWithEmptyPassword() async {
// Given a form with an empty username and password.
await setupViewModel()
#expect(context.password.isEmpty,
"The initial value for the password should be empty.")
#expect(context.username.isEmpty,
"The initial value for the username should be empty.")
#expect(!context.viewState.hasValidCredentials,
"The credentials should be invalid.")
#expect(!context.viewState.canSubmit,
"The form should be blocked for submission.")
// When entering a username without a password.
context.username = "bob"
context.password = ""
// Then the credentials should be considered invalid.
#expect(!context.viewState.hasValidCredentials,
"The credentials should be invalid.")
#expect(!context.viewState.canSubmit,
"The form should be blocked for submission.")
}
@Test
mutating func emptyUsernameWithPassword() async {
// Given a form with an empty username and password.
await setupViewModel()
#expect(context.password.isEmpty,
"The initial value for the password should be empty.")
#expect(context.username.isEmpty,
"The initial value for the username should be empty.")
#expect(!context.viewState.hasValidCredentials,
"The credentials should be invalid.")
#expect(!context.viewState.canSubmit,
"The form should be blocked for submission.")
// When entering a password without a username.
context.username = ""
context.password = "12345678"
// Then the credentials should be considered invalid.
#expect(!context.viewState.hasValidCredentials,
"The credentials should be invalid.")
#expect(!context.viewState.canSubmit,
"The form should be blocked for submission.")
}
@Test
mutating func validCredentials() async {
// Given a form with an empty username and password.
await setupViewModel()
#expect(context.password.isEmpty,
"The initial value for the password should be empty.")
#expect(context.username.isEmpty,
"The initial value for the username should be empty.")
#expect(!context.viewState.hasValidCredentials,
"The credentials should be invalid.")
#expect(!context.viewState.canSubmit,
"The form should be blocked for submission.")
// When entering a username and an 8-character password.
context.username = "bob"
context.password = "12345678"
// Then the credentials should be considered valid.
#expect(context.viewState.hasValidCredentials,
"The credentials should be valid when the username and password are valid.")
#expect(context.viewState.canSubmit,
"The form should be ready to submit.")
}
@Test
mutating func loadingServerWithoutPassword() async throws {
// Given a form with valid credentials.
await setupViewModel()
context.username = "@bob:example.com"
#expect(!context.viewState.hasValidCredentials,
"The credentials should be not be valid without a password.")
#expect(!context.viewState.isLoading,
"The view shouldn't start in a loading state.")
#expect(!context.viewState.canSubmit,
"The form should not be submittable.")
// When updating the view model whilst loading a homeserver.
let deferred = deferFulfillment(context.observe(\.viewState.isLoading),
transitionValues: [true, false])
context.send(viewAction: .parseUsername)
// Then the view state should represent the loading but never allow submitting to occur.
try await deferred.fulfill()
#expect(!context.viewState.isLoading,
"The view should be back in a loaded state.")
#expect(!context.viewState.canSubmit,
"The form should still not be submittable.")
}
@Test
mutating func loadingServerWithPasswordEntered() async throws {
// Given a form with valid credentials.
await setupViewModel()
context.username = "@bob:example.com"
context.password = "12345678"
#expect(context.viewState.hasValidCredentials,
"The credentials should be valid.")
#expect(!context.viewState.isLoading,
"The view shouldn't start in a loading state.")
#expect(context.viewState.canSubmit,
"The form should be ready to submit.")
// When updating the view model whilst loading a homeserver.
let deferred = deferFulfillment(context.observe(\.viewState.canSubmit),
transitionValues: [false, true])
context.send(viewAction: .parseUsername)
// Then the view should be blocked from submitting while loading and then become unblocked again.
try await deferred.fulfill()
#expect(!context.viewState.isLoading,
"The view should be back in a loaded state.")
#expect(context.viewState.canSubmit,
"The form should be ready to submit.")
}
@Test
mutating func oidcServer() async throws {
// Given the screen configured for matrix.org
await setupViewModel()
// When entering a username for a user on a homeserver with OIDC.
let deferred = deferFulfillment(viewModel.actions) {
$0.isConfiguredForOIDC
}
context.username = "@bob:company.com"
context.send(viewAction: .parseUsername)
try await deferred.fulfill()
// Then the view state should be updated with the homeserver and show the OIDC button.
#expect(context.viewState.loginMode.supportsOIDCFlow,
"The OIDC button should be shown.")
}
@Test
mutating func unsupportedServer() async throws {
// Given the screen configured for matrix.org
await setupViewModel()
#expect(context.alertInfo == nil,
"There shouldn't be an alert when the screen loads.")
// When entering a username for an unsupported homeserver.
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) {
$0 != nil
}
context.username = "@bob:server.net"
context.send(viewAction: .parseUsername)
try await deferred.fulfill()
// Then the view state should be updated to show an alert.
#expect(context.alertInfo?.id == .unknown,
"An alert should be shown to the user.")
}
@Test
mutating func elementProRequired() async throws {
// Given the screen configured for matrix.org
await setupViewModel()
#expect(context.alertInfo == nil,
"There shouldn't be an alert when the screen loads.")
// When entering a username for an unsupported homeserver.
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) {
$0 != nil
}
context.username = "@bob:secure.gov"
context.send(viewAction: .parseUsername)
try await deferred.fulfill()
// Then the view state should be updated to show an alert.
#expect(context.alertInfo?.id == .elementProAlert,
"An alert should be shown to the user.")
}
@Test
mutating func loginHint() async {
await setupViewModel(loginHint: "")
#expect(context.username == "")
await setupViewModel(loginHint: "alice")
#expect(context.username == "alice")
await setupViewModel(loginHint: "mxid:@alice:example.com")
#expect(context.username == "@alice:example.com")
}
// MARK: - Helpers
private mutating func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
clientFactory: clientFactory,
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
guard case .success = await service
.configure(for: homeserverAddress, flow: .login) else {
Issue.record("A valid server should be configured for the test.")
return
}
viewModel = LoginScreenViewModel(authenticationService: service,
loginHint: loginHint,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics)
}
}