Skip to content
Open
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
31 changes: 31 additions & 0 deletions wire-ios/Tests/Sourcery/generated/AutoMockable.generated.swift

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// 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 XCTest

@testable import Wire
@testable import WireRequestStrategy

@MainActor
final class E2EIEnrollmentFlowTests: XCTestCase {

private var oauthUseCase: MockOAuthUseCaseInterface!
private var targetVC: UIViewController!
private var window: UIWindow!

override func setUp() {
oauthUseCase = MockOAuthUseCaseInterface()
targetVC = UIViewController()
window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = targetVC
window.isHidden = false
}

override func tearDown() {
oauthUseCase = nil
targetVC = nil
window?.isHidden = true
window = nil
}

// MARK: - OAuth integration

func test_authenticate_callsOAuthUseCaseInvoke() async throws {
// Given
oauthUseCase.invokeParametersOnWebViewPresentingOnWebViewDismissed_MockValue = OAuthResponse(
idToken: "id",
refreshToken: "refresh"
)
let sut = makeSUT()

// When
_ = try await sut.authenticate(makeParameters())

// Then
XCTAssertEqual(oauthUseCase.invokeParametersOnWebViewPresentingOnWebViewDismissed_Invocations.count, 1)
}

func test_authenticate_returnsResponseFromUseCase() async throws {
// Given
let expectedResponse = OAuthResponse(idToken: "expected-id-token", refreshToken: "refresh")
oauthUseCase.invokeParametersOnWebViewPresentingOnWebViewDismissed_MockValue = expectedResponse
let sut = makeSUT()

// When
let result = try await sut.authenticate(makeParameters())

// Then
XCTAssertEqual(result.idToken, expectedResponse.idToken)
}

func test_authenticate_propagatesErrorFromUseCase() async {
// Given
oauthUseCase.invokeParametersOnWebViewPresentingOnWebViewDismissed_MockError = TestError.boom
let sut = makeSUT()

// When / Then
do {
_ = try await sut.authenticate(makeParameters())
XCTFail("Expected error to be thrown")
} catch TestError.boom {
// expected
} catch {
XCTFail("Unexpected error: \(error)")
}
}

// MARK: - Helpers

private func makeSUT() -> E2EIEnrollmentFlow {
E2EIEnrollmentFlow(
oauthUseCase: oauthUseCase,
targetVC: { [unowned self] in targetVC }
)
}

private func makeParameters() -> OAuthParameters {
OAuthParameters(
identityProvider: URL(string: "https://idp.example.com")!,

Check warning on line 103 in wire-ios/Wire-iOS Tests/UserInterface/E2EIdentity/E2EIEnrollmentFlowTests.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor your code to get this URI from a customizable parameter.

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-ios&issues=AZ6DI_trk4kmCzihCBPv&open=AZ6DI_trk4kmCzihCBPv&pullRequest=4792
clientID: "test-client",
keyauth: "test-keyauth",
acmeAudience: "test-audience"
)
}

private enum TestError: Error {
case boom
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,8 @@ extension AuthenticationCoordinator {

Task { @MainActor in
do {
let certificateChain = try await e2eiCertificateUseCase.invoke(authenticate: oauthUseCase.invoke)
let certificateChain = try await e2eiCertificateUseCase
.invoke(authenticate: { try await oauthUseCase.invoke(parameters: $0) })
executeActions([
.hideLoadingView,
.transition(.enrollE2EIdentitySuccess(certificateChain), mode: .reset)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,21 @@ struct DeveloperE2eiView: View {
}
}

Button(String("Enroll"), action: { viewModel.enrollCertificate() })
VStack(alignment: .leading) {
Button(String("Enroll"), action: { viewModel.enrollCertificate() })
footNote(
"Starts the enrollment flow with the selected expiration time."
)
}

VStack(alignment: .leading) {
Button(String("Show update certificate alert")) {
viewModel.showUpdateCertificateAlert(canRemindLater: false)
}
footNote(
"Manually triggers the \"Update Certificate\" popup. Tapping \"Update Certificate\" in the alert starts the enrollment flow with the selected expiration time."
)
}
}

Section("Certificate Revocation Lists") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ final class DeveloperE2eiViewModel: ObservableObject {

// MARK: - Actions

@MainActor
func enrollCertificate() {
guard
let session = userSession,
Expand All @@ -56,20 +57,68 @@ final class DeveloperE2eiViewModel: ObservableObject {

let e2eiCertificateUseCase = session.enrollE2EICertificate as? EnrollE2EICertificateUseCase
let oauthUseCase = OAuthUseCase(targetViewController: { topmostViewController })

Task {
let enrollmentFlow = E2EIEnrollmentFlow(
oauthUseCase: oauthUseCase,
targetVC: { topmostViewController }
)

Task { @MainActor in
enrollmentFlow.showActivityIndicator()
defer { enrollmentFlow.dismissActivityIndicator() }
do {
let expirySec = UInt32(certificateExpirationTime)
_ = try await e2eiCertificateUseCase?.invoke(
authenticate: oauthUseCase.invoke,
guard let certificateDetails = try await e2eiCertificateUseCase?.invoke(
authenticate: enrollmentFlow.authenticate,
expirySec: expirySec
)
) else { return }

enrollmentFlow.dismissActivityIndicator()

let successVC = SuccessfulCertificateEnrollmentViewController()
successVC.certificateDetails = certificateDetails
successVC.onOkTapped = { viewController in
viewController.dismiss(animated: true)
}
successVC.presentOverAll()
} catch {
WireLogger.e2ei.error("failed to enroll e2ei: \(error)")
}
}
}

@MainActor
func showUpdateCertificateAlert(canRemindLater: Bool) {
typealias E2EIUpdateStrings = L10n.Localizable.UpdateCertificate.Alert

guard let developerToolsViewController = UIApplication.shared.topmostViewController(onlyFullScreen: false)
else {
return
}

developerToolsViewController.dismiss(animated: true) {
guard let presentingViewController = UIApplication.shared.topmostViewController(onlyFullScreen: false)
else {
return
}

let alert = UIAlertController.alertForE2EIChangeWithActions(
title: E2EIUpdateStrings.title,
message: canRemindLater ? E2EIUpdateStrings.message : E2EIUpdateStrings.expiredMessage,
enrollButtonText: E2EIUpdateStrings.title,
canRemindLater: canRemindLater
) { action in
switch action {
case .getCertificate:
self.enrollCertificate()
case .remindLater, .learnMore:
break
}
}

presentingViewController.present(alert, animated: true)
}
}

func removeAllExpirationDates() {
guard let crlExpirationDatesRepository else { return }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,17 +353,30 @@ extension BlockerViewController {
}

let oauthUseCase = OAuthUseCase(targetViewController: { rootViewController })
let enrollmentFlow = E2EIEnrollmentFlow(
oauthUseCase: oauthUseCase,
targetVC: { rootViewController }
)

enrollmentFlow.showActivityIndicator()

do {
let certificateChain = try await activeUserSession
.enrollE2EICertificate
.invoke(authenticate: enrollmentFlow.authenticate)

let certificateChain = try await activeUserSession
.enrollE2EICertificate
.invoke(authenticate: oauthUseCase.invoke)
enrollmentFlow.dismissActivityIndicator()

let successEnrollmentViewController = SuccessfulCertificateEnrollmentViewController()
successEnrollmentViewController.certificateDetails = certificateChain
successEnrollmentViewController.onOkTapped = { viewController in
viewController.dismiss(animated: true)
let successEnrollmentViewController = SuccessfulCertificateEnrollmentViewController()
successEnrollmentViewController.certificateDetails = certificateChain
successEnrollmentViewController.onOkTapped = { viewController in
viewController.dismiss(animated: true)
}
successEnrollmentViewController.presentOverAll()
} catch {
enrollmentFlow.dismissActivityIndicator()
throw error
}
successEnrollmentViewController.presentOverAll()
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,16 @@ final class DeviceDetailsViewActionsHandler: DeviceDetailsViewActions, Observabl
throw DeviceDetailsActionsError.failedAction(errorDescription)
}
let oauthUseCase = OAuthUseCase(targetViewController: { topmostViewController })
let enrollmentFlow = E2EIEnrollmentFlow(
oauthUseCase: oauthUseCase,
targetVC: { topmostViewController }
)

enrollmentFlow.showActivityIndicator()
defer { enrollmentFlow.dismissActivityIndicator() }

return try await e2eiCertificateEnrollment.invoke(
authenticate: oauthUseCase.invoke
authenticate: enrollmentFlow.authenticate
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,12 @@ final class DeviceInfoViewModel: ObservableObject {

@MainActor
func enrollClient() async {
isActionInProgress = true
do {
let certificateChain = try await actionsHandler.enrollClient()
showCertificateUpdateSuccess?(certificateChain)
} catch {
showEnrollmentCertificateError = true
}
isActionInProgress = false
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// 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 UIKit
import WireRequestStrategy
import WireReusableUIComponents

@MainActor
final class E2EIEnrollmentFlow {

private let oauthUseCase: OAuthUseCaseInterface
private let targetVC: () -> UIViewController
private var activityIndicator: BlockingActivityIndicator?

init(
oauthUseCase: OAuthUseCaseInterface,
targetVC: @escaping () -> UIViewController
) {
self.oauthUseCase = oauthUseCase
self.targetVC = targetVC
}

func showActivityIndicator() {
guard
activityIndicator == nil,
let window = targetVC().view.window
else { return }

activityIndicator = BlockingActivityIndicator(view: window)
activityIndicator?.start()
}

func dismissActivityIndicator() {
activityIndicator?.stop()
activityIndicator = nil
}

func authenticate(_ parameters: OAuthParameters) async throws -> OAuthResponse {
try await oauthUseCase.invoke(
parameters: parameters,
onWebViewPresenting: { [weak self] in self?.dismissActivityIndicator() },
onWebViewDismissed: { [weak self] in self?.showActivityIndicator() }
)
}
}
Loading