Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ extension ReloginViaSSOComponent: ReloginViaSSOViewModel.Factory {
)
}

@MainActor
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
ValidateSSOCodeUseCase()
}

@MainActor
func noHistoryView(result: AuthenticationResult) -> NoHistoryView {
let factory = noHistoryFactory(result: result)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

// sourcery: AutoMockable
public protocol ValidateSSOCodeUseCaseProtocol: Sendable {

func invoke(ssoCode: String) throws -> UUID

}

public enum ValidateSSOCodeFailure: Error {

case invalidCode

}

public protocol ValidateSSOCodeUseCaseFactory {

@MainActor
func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import WireAuthenticationAPI

package struct ValidateSSOCodeUseCase: ValidateSSOCodeUseCaseProtocol {

package init() {}

package func invoke(ssoCode: String) throws -> UUID {

if let uuid = SSOCodeValidator.validate(
ssoCode: ssoCode.trimmingCharacters(in: .whitespacesAndNewlines)
) {
return uuid
}

throw ValidateSSOCodeFailure.invalidCode
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import WireReusableUIComponents

struct FakeDetermineAuthMethodFactory: DetermineAuthMethodFactory,
DetermineAuthMethodUseCaseFactory,

Check warning on line 26 in WireAuthentication/Sources/WireAuthenticationUI/Mocks/FakeDetermineAuthMethodFactory.swift

View workflow job for this annotation

GitHub Actions / Test Results

Conformance of 'FakeDetermineAuthMethodFactory' to protocol 'FetchBackendConfigUseCaseFactory' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

Conformance of 'FakeDetermineAuthMethodFactory' to protocol 'FetchBackendConfigUseCaseFactory' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode
FetchBackendConfigUseCaseFactory,
LoginViaSSOUseCaseFactory,
ValidateEmailOrSSOCodeUseCaseFactory {
Expand Down Expand Up @@ -84,4 +84,5 @@
func validateEmailOrSSOCodeUseCase() -> any WireAuthenticationAPI.ValidateEmailOrSSOCodeUseCaseProtocol {
mockDependencies.validateEmailOrSSOCodeUseCase()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import WireAuthenticationAPI
import WireNetwork
import WireReusableUIComponents

struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory {
struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory, ValidateSSOCodeUseCaseFactory {

let environment: BackendEnvironment2
let existsAnotherAccount: Bool
Expand All @@ -45,4 +45,7 @@ struct FakeReloginViaSSOFactory: ReloginViaSSOFactory, LoginViaSSOUseCaseFactory
try await MockDependencies().loginViaSSOUseCase(environment: environment)
}

func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
MockDependencies().validateSSOCodeUseCase()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import WireAuthenticationAPI

extension MockDependencies: ValidateSSOCodeUseCaseFactory {

func validateSSOCodeUseCase() -> any ValidateSSOCodeUseCaseProtocol {
MockValidateSSOCodeUseCase()
}

}

struct MockValidateSSOCodeUseCase: ValidateSSOCodeUseCaseProtocol {

func invoke(ssoCode: String) throws -> UUID {
UUID()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ package final class ReloginViaSSOViewModel: ObservableObject {

package typealias Factory =
LoginViaSSOUseCaseFactory &
ReloginViaSSOFactory
ReloginViaSSOFactory &
ValidateSSOCodeUseCaseFactory

// MARK: - View state

Expand Down Expand Up @@ -65,24 +66,23 @@ package final class ReloginViaSSOViewModel: ObservableObject {
// MARK: - Actions

func login() async {
var code: UUID?
if isSSOCodeRequired {
guard let ssoCode = UUID(uuidString: rawSSOCode) else {
router.presentAlert(.incorrectSSOCode)
return
}
code = ssoCode
}

do {
var code: UUID?

if isSSOCodeRequired {
let validateSSOCode = factory.validateSSOCodeUseCase()
code = try validateSSOCode.invoke(ssoCode: rawSSOCode)
}

let loginViaSSO = try await factory.loginViaSSOUseCase(environment: environment)
let authResult = try await loginViaSSO.invoke(code: code)
router.navigate(to: ReloginViaSSODestination.noHistory(authResult))
} catch LoginViaSSOUseCaseError.userCancelled {
// No op
} catch LoginViaSSOUseCaseError.noDefaultCodeAvailable {
isSSOCodeRequired = true
} catch LoginViaSSOUseCaseError.invalidCode {
} catch LoginViaSSOUseCaseError.invalidCode, ValidateSSOCodeFailure.invalidCode {
router.presentAlert(.incorrectSSOCode)
} catch LoginViaSSOUseCaseError.invalidURL {
router.presentAlert(.invalidSSOLink)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// Wire
// Copyright (C) 2026 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
import Testing
import WireAuthenticationAPI

@testable import WireAuthenticationLogic

@Suite
struct ValidateSSOCodeUseCaseTests {

let sut: ValidateSSOCodeUseCase
let testUUIDString = "648e79cb-88b9-42a8-8ea7-dd93e97f4da1"

init() {
self.sut = ValidateSSOCodeUseCase()
}

// MARK: - Valid SSO Codes

@Test(
"Valid SSO codes with different cases",
arguments: [
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "lowercase"),
("WIRE-648E79CB-88B9-42A8-8EA7-DD93E97F4DA1", "uppercase"),
("Wire-648E79CB-88b9-42a8-8ea7-DD93E97F4DA1", "mixedCase")
]
)
func validSSOCodes(ssoCode: String, caseName: String) throws {
// when
let result = try sut.invoke(ssoCode: ssoCode)

// then
let expectedUUID = UUID(uuidString: testUUIDString)!
#expect(result == expectedUUID)
}

// MARK: - Valid SSO Codes with Whitespace

@Test(
"Valid SSO codes with various whitespace",
arguments: [
(" ", " ", "leading and trailing spaces"),
(" ", " ", "multiple leading and trailing spaces"),
("\n", "\n", "newlines"),
("\t", "\t", "tabs"),
(" \n\t ", " \r\n\t ", "mixed whitespace")
]
)
func validSSOCodesWithWhitespace(leading: String, trailing: String, description: String) throws {
// given
let ssoCode = "\(leading)wire-\(testUUIDString)\(trailing)"

// when
let result = try sut.invoke(ssoCode: ssoCode)

// then
let expectedUUID = UUID(uuidString: testUUIDString)!
#expect(result == expectedUUID)
}

// MARK: - Invalid SSO Codes

@Test(
"Invalid SSO codes throw error",
arguments: [
("648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "missing prefix"),
("fire-648e79cb-88b9-42a8-8ea7-dd93e97f4da1", "wrong prefix"),
("wire-invalid-uuid-format", "invalid UUID format"),
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da", "too short UUID"),
("wire-648e79cb-88b9-42a8-8ea7-dd93e97f4da12", "too long UUID"),
("", "empty string"),
(" \n\t ", "only whitespace"),
("wire-", "only prefix"),
(" wire- ", "whitespace with prefix")
]
)
func invalidSSOCodes(ssoCode: String, description: String) throws {
// when/then
#expect(throws: ValidateSSOCodeFailure.self) {
try sut.invoke(ssoCode: ssoCode)
}
}

// MARK: - Specific Edge Cases

@Test("Valid SSO code with only leading whitespace")
func validSSOCodeWithLeadingWhitespace() throws {
// given
let ssoCode = " wire-\(testUUIDString)"

// when
let result = try sut.invoke(ssoCode: ssoCode)

// then
let expectedUUID = UUID(uuidString: testUUIDString)!
#expect(result == expectedUUID)
}

@Test("Valid SSO code with only trailing whitespace")
func validSSOCodeWithTrailingWhitespace() throws {
// given
let ssoCode = "wire-\(testUUIDString) "

// when
let result = try sut.invoke(ssoCode: ssoCode)

// then
let expectedUUID = UUID(uuidString: testUUIDString)!
#expect(result == expectedUUID)
}
}
Loading