diff --git a/.github/workflows/run_all_tests.yml b/.github/workflows/run_all_tests.yml index 4237fdbfdd7..40f4826c17d 100644 --- a/.github/workflows/run_all_tests.yml +++ b/.github/workflows/run_all_tests.yml @@ -2,6 +2,12 @@ name: Run all tests on: workflow_dispatch: + inputs: + run_critical_flows: + description: 'Run critical flows' + type: boolean + default: false + # This is what will cancel the workflow concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -13,6 +19,6 @@ jobs: with: all: true notify_secret: WIRE_IOS_CI_WEBHOOK - run_critical_flows: true + run_critical_flows: ${{ inputs.run_critical_flows }} notify: always secrets: inherit diff --git a/.github/workflows/test_develop.yml b/.github/workflows/test_develop.yml index f64e56a2989..41b74a52b9d 100644 --- a/.github/workflows/test_develop.yml +++ b/.github/workflows/test_develop.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false # prevents a branch to cancel other branches on failure matrix: - branch: [develop, release/cycle-4.16, release/cycle-4.17] # List other branches here + branch: [develop, release/cycle-4.16, release/cycle-4.18] # List other branches here uses: ./.github/workflows/_reusable_run_tests.yml with: all: true diff --git a/WireAVS/Package.swift b/WireAVS/Package.swift index ce04c2191b6..e415d79b3c2 100644 --- a/WireAVS/Package.swift +++ b/WireAVS/Package.swift @@ -17,8 +17,8 @@ let package = Package( targets: [ .binaryTarget( name: "WireAVS", - url: "https://github.com/wireapp/wire-avs/releases/download/10.1.57/avs.xcframework.zip", - checksum: "2231a0582a2f7217c2a7a3d9884dbe314e1dcc04ea3ee78c3e78937f9b78b039" + url: "https://github.com/wireapp/wire-avs/releases/download/10.1.65/avs.xcframework.zip", + checksum: "5553b0132cef04bde820d6d60d54ec1e0bd026866be937dbfb888ff1b0f4ddc7" ) ] ) diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift index 7e7e3baa248..0690f2a5046 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift @@ -21,7 +21,6 @@ import NeedleFoundation import WireDataModel import WireLogging import WireNetwork -import WireUtilitiesPackage protocol NSEClientScopeDependency: Dependency { diff --git a/WireDomain/Sources/WireDomain/Synchronization/IncrementalSync.swift b/WireDomain/Sources/WireDomain/Synchronization/IncrementalSync.swift index 54f912a675d..2ab2a17df4c 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/IncrementalSync.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/IncrementalSync.swift @@ -23,7 +23,6 @@ import WireLogging import WireNetwork import WireSystem import WireUtilities -import WireUtilitiesPackage public struct IncrementalSync: IncrementalSyncProtocol { @@ -166,14 +165,23 @@ public struct IncrementalSync: IncrementalSyncProtocol { logger.info("handling live event stream", attributes: .incrementalSyncV2, .safePublic) syncStateSubject.send(.liveSyncing(.ongoing)) - // because we might be interrupted when in background, we wrap the sync in an expiringActivity that - // will cancel the task - not keeping any db operation (sqlite file opened) in suspend mode - await withExpiringActivity(reason: "processLiveStream IncrementalSync") { - await processLiveEvents( - liveEventStream: liveEventStream, - processedEnvelopeIDs: processedEnvelopeIDs, - publicKeys: publicKeys + do { + // because we might be interrupted when in background, we wrap the sync in an expiringActivity that + // will cancel the task - not keeping any db operation (sqlite file opened) in suspend mode + try await withExpiringActivity(reason: "processLiveStream IncrementalSync") { + await processLiveEvents( + liveEventStream: liveEventStream, + processedEnvelopeIDs: processedEnvelopeIDs, + publicKeys: publicKeys + ) + } + } catch { + // if we expire, close everything + WireLogger.sync.debug( + "Error while processing live stream, close push channel", + attributes: .incrementalSyncV2 ) + await pushChannel.close() } logger.debug("live event stream did finish", attributes: .incrementalSyncV2) @@ -193,14 +201,6 @@ public struct IncrementalSync: IncrementalSyncProtocol { ) async { do { for try await var envelope in liveEventStream { - - guard !Task.isCancelled else { - return logger.debug( - "processLiveEvents has been cancelled, returning", - attributes: .incrementalSyncV2 + [.eventEnvelopeID: envelope.id] - ) - } - logger.debug( "received live event envelope", attributes: .incrementalSyncV2 + [.eventEnvelopeID: envelope.id] diff --git a/WireDomain/Sources/WireDomain/Synchronization/IncrementalSyncV2.swift b/WireDomain/Sources/WireDomain/Synchronization/IncrementalSyncV2.swift index 82a4600a2a4..0ffeb8784b3 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/IncrementalSyncV2.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/IncrementalSyncV2.swift @@ -22,7 +22,6 @@ import WireCoreCrypto import WireDataModel import WireLogging import WireNetwork -import WireUtilitiesPackage public typealias CreatePushChannelStateClosure = () -> PushChannelStateProtocol @@ -157,16 +156,26 @@ public struct IncrementalSyncV2: LiveSyncProtocol { logger.debug("handling live event stream", attributes: logAttributes) syncStateSubject.send(.liveSyncing(.ongoing)) - // because we might be interrupted when in background, we wrap the sync in an expiringActivity that will - // cancel the task (not keeping any file lock in suspend mode) - await withExpiringActivity(reason: "processLiveStream IncrementalSyncV2") { - await processLiveStream( - liveEventStream, - pushChannel: pushChannel, - syncMarker: syncMarker - ) + do { + // because we might be interrupted when in background, we wrap the sync in an expiringActivity that will + // cancel the task (not keeping any file lock in suspend mode) + try await withExpiringActivity(reason: "processLiveStream IncrementalSyncV2") { + await processLiveStream( + liveEventStream, + pushChannel: pushChannel, + syncMarker: syncMarker + ) - WireLogger.sync.debug("Live stream ended, close push channel", attributes: logAttributes) + WireLogger.sync.debug("Live stream ended, close push channel", attributes: logAttributes) + await pushChannel.close() + await pushChannelState.markAsClosed() + } + } catch { + // if we expire, close everything + WireLogger.sync.debug( + "Error while processing live stream, close push channel", + attributes: logAttributes + ) await pushChannel.close() await pushChannelState.markAsClosed() } @@ -251,14 +260,6 @@ public struct IncrementalSyncV2: LiveSyncProtocol { do { for try await element in liveEventStream { - - guard !Task.isCancelled else { - return logger.debug( - "returning from processLiveStream early, task cancelled", - attributes: logAttributes - ) - } - switch element { case let .syncMarker(id, deliveryTag): @@ -328,7 +329,7 @@ public struct IncrementalSyncV2: LiveSyncProtocol { var storedEnvelopes: [(UpdateEventEnvelope, Int64)] = [] // decrypt - try await coreCryptoProvider.coreCrypto().extendedTransaction { coreCryptoContext in + try await coreCryptoProvider.coreCrypto().transaction { coreCryptoContext in for envelope in envelopes { if DeveloperFlag.ignoreIncomingEvents.isOn { diff --git a/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsSync.swift b/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsSync.swift index 4f82138881c..248fbdad560 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsSync.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsSync.swift @@ -94,7 +94,7 @@ public struct PullPendingUpdateEventsSync: PullPendingUpdateEventsSyncProtocol { var lastEnvelopeID: UUID? // We are decrypting the batch within one core crypto transaction - try await coreCryptoProvider.coreCrypto().extendedTransaction { context in + try await coreCryptoProvider.coreCrypto().transaction { context in WireLogger.sync.debug( "decrypting batch of \(envelopes.count) envelopes", attributes: .safePublic diff --git a/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsV2Sync.swift b/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsV2Sync.swift index 16063430b98..0eae74dc9fe 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsV2Sync.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/PullPendingUpdateEventsV2Sync.swift @@ -134,7 +134,7 @@ public struct PullPendingUpdateEventsSyncV2: PullPendingUpdateEventsSyncV2Protoc var storedEnvelopes: [(UpdateEventEnvelope, Int64)] = [] // decrypt - try await coreCryptoProvider.coreCrypto().extendedTransaction { coreCryptoContext in + try await coreCryptoProvider.coreCrypto().transaction { coreCryptoContext in for envelope in envelopes { var envelope = envelope envelope.events = await decryptEnvelope(envelope, in: coreCryptoContext) diff --git a/WireDomain/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/WireDomain Project.xcodeproj/project.pbxproj index e2ca50ff414..bebbe71fefb 100644 --- a/WireDomain/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/WireDomain Project.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 591B6E452C8B09BA009F8A7B /* WireDataModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCC32C1C8CC20076CB1C /* WireDataModel.framework */; }; 591B6E472C8B09BD009F8A7B /* WireDataModelSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01BDA5442C20762200636E50 /* WireDataModelSupport.framework */; }; 59202AD22D54D3D500143413 /* WireDomainPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59202AD12D54D3D500143413 /* WireDomainPackage */; }; - 595B05F02F8900D5009973C7 /* WireUtilitiesPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 595B05EF2F8900D5009973C7 /* WireUtilitiesPackage */; }; 598D042D2C89C63100B64D71 /* WireFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 598D042C2C89C63100B64D71 /* WireFoundation */; }; 59DBDE982D395BB50069C64C /* WireDomainPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 59DBDE972D395BB50069C64C /* WireDomainPackage */; }; 59EA774F2D00CE0C002CA0B8 /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 59EA774E2D00CE0C002CA0B8 /* WireLogging */; }; @@ -116,7 +115,6 @@ buildActionMask = 2147483647; files = ( 598D042D2C89C63100B64D71 /* WireFoundation in Frameworks */, - 595B05F02F8900D5009973C7 /* WireUtilitiesPackage in Frameworks */, 590386C32F6BE9960089C9E4 /* WireData in Frameworks */, 34DC44B32E01C206004D5DD5 /* WireNetwork in Frameworks */, CB6343F02DB7EF3D00A1C892 /* WireUpdateEventCoding in Frameworks */, @@ -258,7 +256,6 @@ C91D188D2D7212FC00B63B66 /* NeedleFoundation */, CB6343EF2DB7EF3D00A1C892 /* WireUpdateEventCoding */, 34DC44B22E01C206004D5DD5 /* WireNetwork */, - 595B05EF2F8900D5009973C7 /* WireUtilitiesPackage */, 590386C22F6BE9960089C9E4 /* WireData */, ); productName = WireDomain; @@ -976,10 +973,6 @@ isa = XCSwiftPackageProductDependency; productName = WireDomainPackage; }; - 595B05EF2F8900D5009973C7 /* WireUtilitiesPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = WireUtilitiesPackage; - }; 598D042C2C89C63100B64D71 /* WireFoundation */ = { isa = XCSwiftPackageProductDependency; productName = WireFoundation; diff --git a/WireFoundation/Package.swift b/WireFoundation/Package.swift index b029cc82045..5b414dde0e6 100644 --- a/WireFoundation/Package.swift +++ b/WireFoundation/Package.swift @@ -19,7 +19,6 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", exact: "1.18.3"), .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"), - .package(path: "../WireLogging"), .package(path: "../WirePlugins") ], targets: [ @@ -58,7 +57,6 @@ let package = Package( .target( name: "WireUtilitiesPackage", dependencies: [ - "WireLogging", .product(name: "ZIPFoundation", package: "ZIPFoundation") ] ), diff --git a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityPerformerProtocol.swift b/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityPerformerProtocol.swift deleted file mode 100644 index 5de6755017f..00000000000 --- a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityPerformerProtocol.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// 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 WireLogging - -// sourcery: AutoMockable -protocol ExpiringActivityPerformerProtocol: Sendable { - - func performExpiringActivity( - reason: String, - using block: @escaping @Sendable (_ isExpiring: Bool) -> Void - ) - -} - -extension ExpiringActivityPerformerProtocol { - - /// Registers an expiring activity that keeps the system from suspending the app - /// while the given task is running. If the system revokes background time, the task - /// is cancelled. - /// - /// This method returns immediately. The expiring activity's callback is kept blocked - /// (via a semaphore) until either the task completes or the system signals expiration. - /// - /// - Parameters: - /// - reason: A human-readable reason for the activity, used for debugging. - /// - task: The task to protect. It will be cancelled if the system reclaims background time. - func performTaskCancellationAsExpiringActivity( - reason: String, - task: Task - ) { - let logger = WireLogger.backgroundActivity - logger.debug("Setting up expiring activity for task cancellation [reason: \(reason)]") - - let semaphore = DispatchSemaphore(value: 0) - performExpiringActivity(reason: reason) { isExpiring in - - if isExpiring { - - logger.debug("Activity is expiring, cancelling task and signaling semaphore … [reason: \(reason)]") - - // System is revoking background time — cancel the task. - task.cancel() - - // Calling signal() here in order to unblock the first invocation of this closure and prevent the system - // from killing the app. The execution of the task however, might be suspended. - semaphore.signal() - - } else { - - logger.debug("Starting expiring activity, waiting on semaphore … [reason: \(reason)]") - - // System granted time. Block this callback until the task finishes, - // so the system knows we're still doing work. - Task.detached { - - logger.debug("Awaiting task … [reason: \(reason)]") - - // We only need to wait for the task to complete; the result is irrelevant. - _ = try? await task.value - - logger.debug("… task ended, signaling semaphore … [reason: \(reason)]") - - semaphore.signal() - - } - - semaphore.wait() - - logger.debug("Waiting on semaphore finished [reason: \(reason)]") - - } - } - - } - -} diff --git a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityProcessInfoWrapper.swift b/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityProcessInfoWrapper.swift deleted file mode 100644 index 797c2c91709..00000000000 --- a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/ExpiringActivityProcessInfoWrapper.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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 - -struct ExpiringActivityProcessInfoWrapper: ExpiringActivityPerformerProtocol { - - var processInfo: ProcessInfo - - init(processInfo: ProcessInfo = .processInfo) { - self.processInfo = processInfo - } - - func performExpiringActivity(reason: String, using block: @escaping @Sendable (Bool) -> Void) { - processInfo.performExpiringActivity(withReason: reason, using: block) - } - -} diff --git a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/withExpiringActivity.swift b/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/withExpiringActivity.swift deleted file mode 100644 index 7b3096a6c64..00000000000 --- a/WireFoundation/Sources/WireUtilitiesPackage/ExpiringActivityRegistration/withExpiringActivity.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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/. -// - -// MARK: - Non-throwing - -public func withExpiringActivity( - reason: String, - block: @escaping @Sendable () async -> Void -) async { - await withExpiringActivity( - performer: ExpiringActivityProcessInfoWrapper(), - reason: reason, - block: block - ) -} - -func withExpiringActivity( - performer: some ExpiringActivityPerformerProtocol, - reason: String, - block: @escaping @Sendable () async -> Void -) async { - let task = Task(operation: block) - performer.performTaskCancellationAsExpiringActivity(reason: reason, task: task) - await withTaskCancellationHandler { - await task.value - } onCancel: { - task.cancel() - } -} - -// MARK: - Throwing - -public func withExpiringActivity( - reason: String, - block: @escaping @Sendable () async throws -> T -) async throws -> T where T: Sendable { - try await withExpiringActivity( - performer: ExpiringActivityProcessInfoWrapper(), - reason: reason, - block: block - ) -} - -func withExpiringActivity( - performer: some ExpiringActivityPerformerProtocol, - reason: String, - block: @escaping @Sendable () async throws -> T -) async throws -> T where T: Sendable { - let task = Task(operation: block) - performer.performTaskCancellationAsExpiringActivity(reason: reason, task: task) - return try await withTaskCancellationHandler { - try await task.value - } onCancel: { - task.cancel() - } -} diff --git a/WireFoundation/Sources/WireUtilitiesPackageSupport/Sourcery/sourcery.yml b/WireFoundation/Sources/WireUtilitiesPackageSupport/Sourcery/sourcery.yml index 76faa2718bf..fd9ca804bb9 100644 --- a/WireFoundation/Sources/WireUtilitiesPackageSupport/Sourcery/sourcery.yml +++ b/WireFoundation/Sources/WireUtilitiesPackageSupport/Sourcery/sourcery.yml @@ -5,5 +5,4 @@ templates: output: ${DERIVED_SOURCES_DIR} args: - autoMockableTestableImports: ["WireUtilitiesPackage"] autoMockablePublicImports: ["Foundation", "WireUtilitiesPackage"] diff --git a/WireFoundation/Tests/WireUtilitiesPackageTests/ExpiringActivityRegistration/ExpiringActivityPerformerTests.swift b/WireFoundation/Tests/WireUtilitiesPackageTests/ExpiringActivityRegistration/ExpiringActivityPerformerTests.swift deleted file mode 100644 index e8af7c09f52..00000000000 --- a/WireFoundation/Tests/WireUtilitiesPackageTests/ExpiringActivityRegistration/ExpiringActivityPerformerTests.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// 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 - -@testable import WireUtilitiesPackage -@testable import WireUtilitiesPackageSupport - -struct ExpiringActivityPerformerTests { - - let performerMock = ExpiringActivityPerformerProtocolMock() - - @Test - func testReasonIsForwardedToPerformer() { - // Given - var receivedReason: String? - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = - { reason, block in - receivedReason = reason - block(false) - } - let task = Task {} - - // When - performerMock.performTaskCancellationAsExpiringActivity(reason: "sync messages", task: task) - - // Then - #expect(receivedReason == "sync messages") - } - - @Test - func testTaskIsCancelledWhenExpiring() { - // Given - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = { _, block in - block(true) - } - let task = Task { - try? await Task.sleep(for: .seconds(60)) - } - - // When - performerMock.performTaskCancellationAsExpiringActivity(reason: "sync", task: task) - - // Then - #expect(task.isCancelled) - } - - @Test - func testBlockWaitsForTaskToFinishWhenNotExpiring() async throws { - // Given - let (stream, continuation) = AsyncStream.makeStream() - let flag = Flag() - - let task = Task { - for await _ in stream {} - } - - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = { _, block in - DispatchQueue.global().async { - block(false) - Task { await flag.set() } - } - } - - // When - performerMock.performTaskCancellationAsExpiringActivity(reason: "sync", task: task) - - // Then — the block should still be waiting because the task hasn't finished. - try await Task.sleep(for: .milliseconds(200)) - var didReturn = await flag.value - #expect(!didReturn) - - // Allow the task to complete. - continuation.finish() - - // The block should return shortly after. - try await Task.sleep(for: .milliseconds(200)) - didReturn = await flag.value - #expect(didReturn) - } - - @Test - func testBlockUnblocksWhenSystemRevokesTime() async throws { - // Given - let (stream, _) = AsyncStream.makeStream() - let flag = Flag() - var capturedBlock: (@Sendable (Bool) -> Void)? - - let task = Task { - for await _ in stream {} - } - - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = { _, block in - capturedBlock = block - DispatchQueue.global().async { - block(false) - Task { await flag.set() } - } - } - - // When — register the activity, which grants time and blocks. - performerMock.performTaskCancellationAsExpiringActivity(reason: "sync", task: task) - - // The block should still be waiting because the task hasn't finished. - try await Task.sleep(for: .milliseconds(200)) - var didReturn = await flag.value - #expect(!didReturn) - - // Simulate the system revoking background time. - capturedBlock?(true) - - // Then — the block should unblock and the task should be cancelled. - try await Task.sleep(for: .milliseconds(200)) - didReturn = await flag.value - #expect(didReturn) - #expect(task.isCancelled) - } - - @Test - func testBlockReturnsEvenWhenTaskThrows() async throws { - // Given - struct TestError: Error {} - let flag = Flag() - - let task = Task { - throw TestError() - } - - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = { _, block in - DispatchQueue.global().async { - block(false) - Task { await flag.set() } - } - } - - // When - performerMock.performTaskCancellationAsExpiringActivity(reason: "sync", task: task) - - // Then — block should return despite the task throwing. - try await Task.sleep(for: .milliseconds(200)) - let didReturn = await flag.value - #expect(didReturn) - } - - @Test - func testCancellingOuterTaskCancelsBlock() async throws { - // Given - let blockStarted = Flag() - let blockCancelled = Flag() - - performerMock - .performExpiringActivityReasonStringUsingBlockSendableEscapingIsExpiringBoolVoidVoidClosure = { _, block in - DispatchQueue.global().async { - block(false) - } - } - - let outerTask = Task { - await withExpiringActivity(performer: performerMock, reason: "sync") { - await blockStarted.set() - while !Task.isCancelled { - try? await Task.sleep(for: .milliseconds(10)) - } - await blockCancelled.set() - } - } - - // Wait for the block to start running. - while !(await blockStarted.value) { - try await Task.sleep(for: .milliseconds(10)) - } - - // When - outerTask.cancel() - - // Then — the block should see cancellation. - try await Task.sleep(for: .milliseconds(200)) - let wasCancelled = await blockCancelled.value - #expect(wasCancelled) - } - -} - -// MARK: - - -private actor Flag { - var value = false - func set() { value = true } -} diff --git a/WireMessaging/Sources/WireMessagingUI/WireDrive/Components/Previews/WireDriveDocumentHeaderView.swift b/WireMessaging/Sources/WireMessagingUI/WireDrive/Components/Previews/WireDriveDocumentHeaderView.swift index 3fc93b8ccd5..8505922caf5 100644 --- a/WireMessaging/Sources/WireMessagingUI/WireDrive/Components/Previews/WireDriveDocumentHeaderView.swift +++ b/WireMessaging/Sources/WireMessagingUI/WireDrive/Components/Previews/WireDriveDocumentHeaderView.swift @@ -59,6 +59,15 @@ struct WireDriveDocumentHeaderView: View { .font(for: .subline1) .lineLimit(1) .layoutPriority(1) + + Spacer() + + if !isDraftPreview { + stateTextView() + .foregroundStyle(isError ? ColorTheme.Base.error.color : ColorTheme.Base.secondaryText.color) + .font(for: .subline1) + .lineLimit(1) + } } .padding(.horizontal, 8) .padding(.top, 8) diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-dark.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-dark.png index f9615e547a7..e20418805ad 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-dark.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f907ac356670620837738258d7d8802c45d865028db280dfed216fb245eb1e4 -size 17348 +oid sha256:526daf3c87e75b8ccae2fa027159c10d6786966b12330a40b273ed19d9dc8d27 +size 20199 diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-light.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-light.png index 4abf172d9dd..dca7dbbc34b 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-light.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.1-light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ba1c8a81f3bffc79513ea8e63bfb31a1bd6b0eb77ad41b0c058aae4655d4909 -size 17514 +oid sha256:f8e6f9457db47c076e75e862fe748f9d1aaf6623094b0a6db58728abb9e894e2 +size 20304 diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-dark.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-dark.png index 93d9385026f..71faeb76b19 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-dark.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d94087bd3b824973b54a377f3bb526379cc8ba08cb420865be06f296b94ffa92 -size 15892 +oid sha256:1ba997e45292361a5a8c870812d1aad6e47a122cd3ac2df7b6268b5219f2d2b7 +size 18756 diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-light.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-light.png index 02f46417ce1..8ce8f3065be 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-light.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.3-light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b881f5dd4f71baf01006be19343f8b1a159f1eb9dd1aad5d8d91eea94c4c276 -size 15974 +oid sha256:20b786ab2960e4eeae048d29c79366df3c3c82d6fa23b4320fdc71b12915353c +size 18762 diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-dark.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-dark.png index 669bf374dd8..e44ac96bc08 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-dark.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61ff07a9451a8e4b016882c545346434c35e186c094d930268425e3655069db4 -size 15593 +oid sha256:c1a2b326fbe7d41c662f9da672d13c2fdb3c13d2883a45fca6f607acd71a6b0a +size 18880 diff --git a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-light.png b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-light.png index 73d2022289f..11ba877375f 100644 --- a/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-light.png +++ b/WireMessaging/Tests/WireMessagingTests/Resources/ReferenceImages/WireDriveDocumentAttachmentPreviewTests/testConfigurationVariations.4-light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3045ff1c5326dcd96c8d113bf199c4c2e61e7ba61b18070089e6fb75bc55bdf5 -size 15720 +oid sha256:91fbc4e21ff7519adef389a514180ef421313907583bd1a349d5fea9c4a7e47e +size 19127 diff --git a/WireNetwork/Sources/WireNetwork/Models/API/APIVersion.swift b/WireNetwork/Sources/WireNetwork/Models/API/APIVersion.swift index 2c185f650ac..854ff9dd86d 100644 --- a/WireNetwork/Sources/WireNetwork/Models/API/APIVersion.swift +++ b/WireNetwork/Sources/WireNetwork/Models/API/APIVersion.swift @@ -52,7 +52,7 @@ public enum APIVersion: UInt, CaseIterable, Comparable, Sendable { /// as production ready. public static let productionVersions: Set = [ - .v0, .v1, .v2, .v3, .v4, .v5, .v6, .v7, .v8, .v9, .v10, .v11, .v12, .v13, .v14 + .v0, .v1, .v2, .v3, .v4, .v5, .v6, .v7, .v8, .v9, .v10, .v11, .v12, .v13, .v14, .v15 ] /// API versions currently under development and not suitable for production diff --git a/WireUI/Sources/WireLocators/Locators.swift b/WireUI/Sources/WireLocators/Locators.swift index ec89a5cd47b..f06d2c12a94 100644 --- a/WireUI/Sources/WireLocators/Locators.swift +++ b/WireUI/Sources/WireLocators/Locators.swift @@ -387,6 +387,7 @@ public enum Locators { public enum FileMenu: String { case deleteToRecycleBin + case restore public var identifier: String { "fileMenu.\(rawValue)" diff --git a/wire-ios-data-model/Source/Core Crypto/CoreCryptoProvider.swift b/wire-ios-data-model/Source/Core Crypto/CoreCryptoProvider.swift index 2676d8ec2a9..909a6b91d0f 100644 --- a/wire-ios-data-model/Source/Core Crypto/CoreCryptoProvider.swift +++ b/wire-ios-data-model/Source/Core Crypto/CoreCryptoProvider.swift @@ -20,17 +20,6 @@ import Foundation import WireCoreCrypto import WireFoundation import WireLogging -import WireUtilitiesPackage - -public extension CoreCryptoProtocol { - - func extendedTransaction(block: @escaping (any CoreCryptoContextProtocol) async throws -> T) async throws -> T { - try await withExpiringActivity(reason: "cc transation") { - try await self.transaction(block) - } - } - -} // sourcery: AutoMockable public protocol CoreCryptoProviderProtocol { @@ -117,7 +106,7 @@ public actor CoreCryptoProvider: CoreCryptoProviderProtocol { WireLogger.mls.info("Initialising MLS client with basic credentials") let defaultCiphersuite = await featureRespository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite let coreCrypto = try await coreCrypto() - _ = try await coreCrypto.extendedTransaction { context in + _ = try await coreCrypto.transaction { context in try await context.mlsInit( clientId: .init(bytes: mlsClientID.data), ciphersuites: [defaultCiphersuite], @@ -133,7 +122,7 @@ public actor CoreCryptoProvider: CoreCryptoProviderProtocol { ) async throws -> CRLsDistributionPoints? { WireLogger.mls.info("Initialising MLS client from end-to-end identity enrollment") let coreCrypto = try await coreCrypto() - return try await coreCrypto.extendedTransaction { context in + return try await coreCrypto.transaction { context in let crlsDistributionPoints = try await context.e2eiMlsInitOnly( enrollment: enrollment, certificateChain: certificateChain, @@ -265,7 +254,7 @@ public actor CoreCryptoProvider: CoreCryptoProviderProtocol { attributes: .safePublic ) - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { WireLogger.coreCrypto.debug( "proteus init", attributes: .safePublic @@ -308,7 +297,7 @@ public actor CoreCryptoProvider: CoreCryptoProviderProtocol { "core crypto transaction...", attributes: .safePublic ) - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { WireLogger.coreCrypto.debug( "mls init", attributes: .safePublic diff --git a/wire-ios-data-model/Source/E2EIdentity/E2EISetupService.swift b/wire-ios-data-model/Source/E2EIdentity/E2EISetupService.swift index 4e2be4910ad..a3a843a8a95 100644 --- a/wire-ios-data-model/Source/E2EIdentity/E2EISetupService.swift +++ b/wire-ios-data-model/Source/E2EIdentity/E2EISetupService.swift @@ -71,19 +71,19 @@ public final class E2EISetupService: E2EISetupServiceInterface { // MARK: - Public interface public func isTrustAnchorRegistered() async throws -> Bool { - try await coreCryptoProvider.coreCrypto().extendedTransaction { context in + try await coreCryptoProvider.coreCrypto().transaction { context in try await context.e2eiIsPkiEnvSetup() } } public func registerTrustAnchor(_ trustAnchor: String) async throws { - try await coreCryptoProvider.coreCrypto().extendedTransaction { context in + try await coreCryptoProvider.coreCrypto().transaction { context in try await context.e2eiRegisterAcmeCa(trustAnchorPem: trustAnchor) } } public func registerFederationCertificate(_ certificate: String) async throws { - _ = try await coreCryptoProvider.coreCrypto().extendedTransaction { context in + try await coreCryptoProvider.coreCrypto().transaction { context in try await context.e2eiRegisterIntermediateCa(certPem: certificate) } } @@ -121,7 +121,7 @@ public final class E2EISetupService: E2EISetupServiceInterface { let ciphersuite = await featureRepository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite let expirySec = expirySec ?? UInt32(TimeInterval.oneDay * 90) - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { if isUpgradingClient { let e2eiIsEnabled = try await $0.e2eiIsEnabled(ciphersuite: ciphersuite) if e2eiIsEnabled { diff --git a/wire-ios-data-model/Source/E2EIdentity/E2EIVerificationStatusService.swift b/wire-ios-data-model/Source/E2EIdentity/E2EIVerificationStatusService.swift index c36c1a3d2cc..cd5ee36971c 100644 --- a/wire-ios-data-model/Source/E2EIdentity/E2EIVerificationStatusService.swift +++ b/wire-ios-data-model/Source/E2EIdentity/E2EIVerificationStatusService.swift @@ -69,7 +69,7 @@ public final class E2EIVerificationStatusService: E2EIVerificationStatusServiceI public func getConversationStatus(groupID: MLSGroupID) async throws -> MLSVerificationStatus { do { - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.e2eiConversationState(conversationId: groupID.conversationId).toMLSVerificationStatus() } } catch { diff --git a/wire-ios-data-model/Source/MLS/CreateMLSGroupUseCase.swift b/wire-ios-data-model/Source/MLS/CreateMLSGroupUseCase.swift index 6281f862e98..0589b415f71 100644 --- a/wire-ios-data-model/Source/MLS/CreateMLSGroupUseCase.swift +++ b/wire-ios-data-model/Source/MLS/CreateMLSGroupUseCase.swift @@ -88,7 +88,7 @@ extension CreateMLSGroupUseCase { // the external senders is the same as the parent, otherwise we // won't be able to decrypt external remove proposals from the // owning domain. - let externalSenders = try await coreCrypto.extendedTransaction { + let externalSenders = try await coreCrypto.transaction { [try await $0.getExternalSender(conversationId: parentGroupID.conversationId)] } @@ -136,7 +136,7 @@ extension CreateMLSGroupUseCase { custom: .init(keyRotationSpan: nil, wirePolicy: nil) ) - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { let e2eiIsEnabled = try await $0.e2eiIsEnabled(ciphersuite: ciphersuite.coreCryptoCipherSuite) try await $0.createConversation( conversationId: groupID.conversationId, diff --git a/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift b/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift index de8a70193d3..bf5bf42a538 100644 --- a/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift +++ b/wire-ios-data-model/Source/MLS/MLSActionExecutor.swift @@ -211,7 +211,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { if let context { try await processWelcomeMessageInternal(message, context: context) } else { - try await coreCrypto.extendedTransaction { context in + try await coreCrypto.transaction { context in try await self.processWelcomeMessageInternal(message, context: context) } } @@ -240,7 +240,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { do { WireLogger.mls.info("adding members to group...", attributes: groupID.safeAttributes) - let crlNewDistributionPoints = try await coreCrypto.extendedTransaction { + let crlNewDistributionPoints = try await coreCrypto.transaction { try await $0.addClientsToConversation( conversationId: groupID.conversationId, keyPackages: invitees.compactMap(\.coreCryptoKeyPackage) @@ -269,7 +269,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { try await performNonReentrant(groupID: groupID) { do { WireLogger.mls.info("removing clients from group...", attributes: groupID.safeAttributes) - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.removeClientsFromConversation( conversationId: groupID.conversationId, clients: clients @@ -290,7 +290,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { try await performNonReentrant(groupID: groupID) { do { WireLogger.mls.info("updating key material for group...", attributes: groupID.safeAttributes) - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.updateKeyingMaterial(conversationId: groupID.conversationId) } } catch { @@ -308,7 +308,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { try await performNonReentrant(groupID: groupID) { do { WireLogger.mls.info("committing pending proposals for group", attributes: groupID.safeAttributes) - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { try await $0.commitPendingProposals(conversationId: groupID.conversationId) } WireLogger.mls @@ -329,7 +329,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { do { WireLogger.mls.info("joining group via external commit", attributes: groupID.safeAttributes) let ciphersuite = await featureRepository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite - let conversationInitBundle = try await coreCrypto.extendedTransaction { + let conversationInitBundle = try await coreCrypto.transaction { let e2eiIsEnabled = try await $0.e2eiIsEnabled(ciphersuite: ciphersuite) return try await $0.joinByExternalCommit( groupInfo: GroupInfo(bytes: groupInfo), @@ -365,7 +365,7 @@ public actor MLSActionExecutor: MLSActionExecutorProtocol { try await decryptMessageInternal(message, in: groupID, context: context) } else { try await performNonReentrant(groupID: groupID) { - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { try await self.decryptMessageInternal(message, in: groupID, context: $0) } } diff --git a/wire-ios-data-model/Source/MLS/MLSEncryptionService.swift b/wire-ios-data-model/Source/MLS/MLSEncryptionService.swift index d19499cabda..a5d6abe83e6 100644 --- a/wire-ios-data-model/Source/MLS/MLSEncryptionService.swift +++ b/wire-ios-data-model/Source/MLS/MLSEncryptionService.swift @@ -75,7 +75,7 @@ public final class MLSEncryptionService: MLSEncryptionServiceInterface { ) async throws -> Data { do { WireLogger.mls.debug("encrypting message (\(message.count) bytes) for group (\(groupID))") - return try await coreCrypto.extendedTransaction { try await $0.encryptMessage( + return try await coreCrypto.transaction { try await $0.encryptMessage( conversationId: groupID.conversationId, message: message ) } diff --git a/wire-ios-data-model/Source/MLS/MLSService.swift b/wire-ios-data-model/Source/MLS/MLSService.swift index 8b57f9b0b9d..3e74677fe7a 100644 --- a/wire-ios-data-model/Source/MLS/MLSService.swift +++ b/wire-ios-data-model/Source/MLS/MLSService.swift @@ -180,7 +180,7 @@ public final class MLSService: MLSServiceInterface { } public func epoch(for groupID: MLSGroupID) async throws -> UInt64 { - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { let exists = try? await $0.conversationExists(conversationId: groupID.conversationId) if exists == true { return try await $0.conversationEpoch(conversationId: groupID.conversationId) @@ -201,7 +201,7 @@ public final class MLSService: MLSServiceInterface { let keyLength: UInt32 = 32 - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { let epoch = try await $0.conversationEpoch(conversationId: subconversationGroupID.conversationId) let secretKey = try await $0.exportSecretKey( @@ -237,7 +237,7 @@ public final class MLSService: MLSServiceInterface { public func subconversationMembers(for subconversationGroupID: MLSGroupID) async throws -> [MLSClientID] { do { - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.getClientIds(conversationId: subconversationGroupID.conversationId).compactMap { MLSClientID(data: $0.copyBytes()) } @@ -568,7 +568,7 @@ public final class MLSService: MLSServiceInterface { logger.info("wiping group", attributes: groupID.safeAttributes) do { - try await coreCrypto.extendedTransaction { [self] in + try await coreCrypto.transaction { [self] in guard try await $0.conversationExists( conversationId: groupID.conversationId ) else { @@ -654,7 +654,7 @@ public final class MLSService: MLSServiceInterface { } let ciphersuite = await featureRepository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite - let estimatedLocalKeyPackageCount = try await coreCrypto.extendedTransaction { + let estimatedLocalKeyPackageCount = try await coreCrypto.transaction { try await $0.clientValidKeypackagesCount(ciphersuite: ciphersuite, credentialType: .basic) } let shouldCountRemainingKeyPackages = estimatedLocalKeyPackageCount < halfOfTargetUnclaimedKeyPackageCount @@ -711,7 +711,7 @@ public final class MLSService: MLSServiceInterface { do { let ciphersuite = await featureRepository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite - keyPackages = try await coreCrypto.extendedTransaction { + keyPackages = try await coreCrypto.transaction { let e2eiIsEnabled = try await $0.e2eiIsEnabled(ciphersuite: ciphersuite) return try await $0.clientKeypackages( ciphersuite: ciphersuite, @@ -762,7 +762,7 @@ public final class MLSService: MLSServiceInterface { } public func externalSenderKey(groupID: MLSGroupID) async throws -> Data { - try await coreCrypto.extendedTransaction { coreCrypto in + try await coreCrypto.transaction { coreCrypto in try await coreCrypto.getExternalSender(conversationId: groupID.conversationId) }.copyBytes() } @@ -770,7 +770,7 @@ public final class MLSService: MLSServiceInterface { public func conversationExists(groupID: MLSGroupID) async throws -> Bool { logger.info("checking if group (\(groupID)) exists...") - let result = try await coreCrypto.extendedTransaction { coreCrypto in + let result = try await coreCrypto.transaction { coreCrypto in try await coreCrypto.conversationExists(conversationId: groupID.conversationId) } logger.info("... group (\(groupID)) " + (result ? "exists!" : "does not exist!")) @@ -1161,7 +1161,7 @@ public final class MLSService: MLSServiceInterface { private func outOfSyncConversations(in context: NSManagedObjectContext) async throws -> [OutOfSyncConversationInfo] { - let conversations = try await coreCrypto.extendedTransaction { coreCrypto in + let conversations = try await coreCrypto.transaction { coreCrypto in let allMLSConversations = await context.perform { ZMConversation.fetchMLSConversations(in: context) } @@ -1232,7 +1232,7 @@ public final class MLSService: MLSServiceInterface { subgroup: MLSSubgroup?, context: NSManagedObjectContext ) async throws -> Bool { - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { await self.isConversationOutOfSync( conversation, subgroup: subgroup, @@ -1823,7 +1823,7 @@ public final class MLSService: MLSServiceInterface { parentGroupID: parentGroupID ) - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { try await $0.wipeConversation(conversationId: subconversationGroupID.conversationId) } } catch { diff --git a/wire-ios-data-model/Source/Model/Conversation/ZMConversation+ExternalParticipant.swift b/wire-ios-data-model/Source/Model/Conversation/ZMConversation+ExternalParticipant.swift index 274480e4c70..7ef5d662455 100644 --- a/wire-ios-data-model/Source/Model/Conversation/ZMConversation+ExternalParticipant.swift +++ b/wire-ios-data-model/Source/Model/Conversation/ZMConversation+ExternalParticipant.swift @@ -56,18 +56,13 @@ public extension ZMConversation { /// The state of external participants in the conversation. var externalParticipantsState: ExternalParticipantsState { - // Exception 1) We don't consider guests/apps as external participants in 1:1 conversations + // External-participant states are only reported for group conversations. guard conversationType == .group else { return [] } - // Exception 2) If there is only one user in the group and it's an app, we don't consider it as external let participants = Set(localParticipants) let selfUser = ZMUser.selfUser(in: managedObjectContext!) let otherUsers = participants.subtracting([selfUser]) - if otherUsers.count == 1, otherUsers.first!.isAppOrBot { - return [] - } - // Calculate the external participants state let canDisplayGuests = selfUser.isTeamMember let canDisplayExternals = selfUser.teamRole != .partner diff --git a/wire-ios-data-model/Source/Proteus/ProteusService.swift b/wire-ios-data-model/Source/Proteus/ProteusService.swift index ed76d07c149..9bd7d905da3 100644 --- a/wire-ios-data-model/Source/Proteus/ProteusService.swift +++ b/wire-ios-data-model/Source/Proteus/ProteusService.swift @@ -60,7 +60,7 @@ public final class ProteusService: ProteusServiceInterface { } do { - try await coreCrypto.extendedTransaction { try await $0.proteusSessionFromPrekey( + try await coreCrypto.transaction { try await $0.proteusSessionFromPrekey( sessionId: id.rawValue, prekey: prekeyData ) } @@ -80,7 +80,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("deleting session") do { - try await coreCrypto.extendedTransaction { try await $0.proteusSessionDelete(sessionId: id.rawValue) } + try await coreCrypto.transaction { try await $0.proteusSessionDelete(sessionId: id.rawValue) } } catch { logger.error("failed to delete session: \(String(describing: error))") throw DeleteSessionError.failedToDeleteSession @@ -98,7 +98,7 @@ public final class ProteusService: ProteusServiceInterface { func saveSession(id: ProteusSessionID) async throws { do { - try await coreCrypto.extendedTransaction { try await $0.proteusSessionSave(sessionId: id.rawValue) } + try await coreCrypto.transaction { try await $0.proteusSessionSave(sessionId: id.rawValue) } } catch { // swiftlint:disable:next todo_requires_jira_link // TODO: Log error @@ -112,8 +112,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("checking if session exists") do { - return try await coreCrypto - .extendedTransaction { try await $0.proteusSessionExists(sessionId: id.rawValue) } + return try await coreCrypto.transaction { try await $0.proteusSessionExists(sessionId: id.rawValue) } } catch { logger.error("failed to check if session exists \(String(describing: error))") return false @@ -156,7 +155,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("encrypting data") do { - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.proteusEncrypt( sessionId: id.rawValue, plaintext: data @@ -176,7 +175,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("encrypting data batch") do { - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.proteusEncryptBatched( sessions: sessions.map(\.rawValue), plaintext: data @@ -223,7 +222,7 @@ public final class ProteusService: ProteusServiceInterface { if let context { try await decryptInternal(data: data, forSession: id, context: context) } else { - try await coreCrypto.extendedTransaction { context in + try await coreCrypto.transaction { context in try await self.decryptInternal(data: data, forSession: id, context: context) } } @@ -284,7 +283,7 @@ public final class ProteusService: ProteusServiceInterface { public func generatePrekey(id: UInt16) async throws -> String { do { return try await coreCrypto - .extendedTransaction { try await $0.proteusNewPrekey(prekeyId: id).base64EncodedString() } + .transaction { try await $0.proteusNewPrekey(prekeyId: id).base64EncodedString() } } catch { throw PrekeyError.failedToGeneratePrekey } @@ -293,8 +292,7 @@ public final class ProteusService: ProteusServiceInterface { public func lastPrekey() async throws -> String { logger.info("getting last resort prekey") do { - return try await coreCrypto - .extendedTransaction { try await $0.proteusLastResortPrekey().base64EncodedString() } + return try await coreCrypto.transaction { try await $0.proteusLastResortPrekey().base64EncodedString() } } catch { logger.error("failed to get last resort prekey: \(String(describing: error))") throw PrekeyError.failedToGetLastPrekey @@ -303,7 +301,7 @@ public final class ProteusService: ProteusServiceInterface { public var lastPrekeyID: UInt16 { get async { - let lastPrekeyID = try? await coreCrypto.extendedTransaction { try $0.proteusLastResortPrekeyId() } + let lastPrekeyID = try? await coreCrypto.transaction { try $0.proteusLastResortPrekeyId() } return lastPrekeyID ?? UInt16.max } } @@ -354,7 +352,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("fetching local fingerprint") do { - return try await coreCrypto.extendedTransaction { try await $0.proteusFingerprint() } + return try await coreCrypto.transaction { try await $0.proteusFingerprint() } } catch { logger.error("failed to fetch local fingerprint: \(String(describing: error))") throw FingerprintError.failedToGetLocalFingerprint @@ -365,8 +363,7 @@ public final class ProteusService: ProteusServiceInterface { logger.info("fetching remote fingerprint") do { - return try await coreCrypto - .extendedTransaction { try await $0.proteusFingerprintRemote(sessionId: id.rawValue) } + return try await coreCrypto.transaction { try await $0.proteusFingerprintRemote(sessionId: id.rawValue) } } catch { logger.error("failed to fetch remote fingerprint: \(String(describing: error))") throw FingerprintError.failedToGetRemoteFingerprint @@ -381,8 +378,7 @@ public final class ProteusService: ProteusServiceInterface { } do { - return try await coreCrypto - .extendedTransaction { try $0.proteusFingerprintPrekeybundle(prekey: prekeyData) } + return try await coreCrypto.transaction { try $0.proteusFingerprintPrekeybundle(prekey: prekeyData) } } catch { logger.error("failed to get fingerprint from prekey: \(String(describing: error))") throw FingerprintError.failedToGetFingerprintFromPrekey diff --git a/wire-ios-data-model/Source/UseCases/IsUserE2EICertifiedUseCase.swift b/wire-ios-data-model/Source/UseCases/IsUserE2EICertifiedUseCase.swift index 36792ae9f25..5bbd01e0a3c 100644 --- a/wire-ios-data-model/Source/UseCases/IsUserE2EICertifiedUseCase.swift +++ b/wire-ios-data-model/Source/UseCases/IsUserE2EICertifiedUseCase.swift @@ -70,7 +70,7 @@ public struct IsUserE2EICertifiedUseCase: IsUserE2EICertifiedUseCaseProtocol { // make the call to Core Crypto let coreCrypto = try await coreCryptoProvider.coreCrypto() - let userIdentities = try await coreCrypto.extendedTransaction { context in + let userIdentities = try await coreCrypto.transaction { context in // get MLS group members let allUserIdentities = try await context.getUserIdentities( conversationId: mlsGroupID.conversationId, diff --git a/wire-ios-data-model/Tests/Model/Conversation/ZMConversationExternalParticipantsStateTests.swift b/wire-ios-data-model/Tests/Model/Conversation/ZMConversationExternalParticipantsStateTests.swift index 2855a69c265..534b0d719c7 100644 --- a/wire-ios-data-model/Tests/Model/Conversation/ZMConversationExternalParticipantsStateTests.swift +++ b/wire-ios-data-model/Tests/Model/Conversation/ZMConversationExternalParticipantsStateTests.swift @@ -96,12 +96,9 @@ class ZMConversationExternalParticipantsStateTests: ZMConversationTestsBase { // None assertMatrixRow(.group, selfUser: .personal, otherUsers: [.personal], expectedResult: []) assertMatrixRow(.group, selfUser: .personal, otherUsers: [.memberOfHostingTeam], expectedResult: []) - assertMatrixRow(.group, selfUser: .personal, otherUsers: [.service], expectedResult: []) assertMatrixRow(.group, selfUser: .memberOfHostingTeam, otherUsers: [.memberOfHostingTeam], expectedResult: []) - assertMatrixRow(.group, selfUser: .memberOfHostingTeam, otherUsers: [.service], expectedResult: []) assertMatrixRow(.group, selfUser: .external, otherUsers: [.external], expectedResult: []) assertMatrixRow(.group, selfUser: .external, otherUsers: [.memberOfHostingTeam], expectedResult: []) - assertMatrixRow(.group, selfUser: .external, otherUsers: [.service], expectedResult: []) // Only Remotes assertMatrixRow( @@ -122,6 +119,9 @@ class ZMConversationExternalParticipantsStateTests: ZMConversationTestsBase { assertMatrixRow(.group, selfUser: .external, otherUsers: [.personal], expectedResult: [.visibleGuests]) // Only Services + assertMatrixRow(.group, selfUser: .personal, otherUsers: [.service], expectedResult: [.visibleApps]) + assertMatrixRow(.group, selfUser: .memberOfHostingTeam, otherUsers: [.service], expectedResult: [.visibleApps]) + assertMatrixRow(.group, selfUser: .external, otherUsers: [.service], expectedResult: [.visibleApps]) assertMatrixRow( .group, selfUser: .memberOfHostingTeam, diff --git a/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj b/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj index 5e524d074b0..39d2880b090 100644 --- a/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj +++ b/wire-ios-data-model/WireDataModel.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 0176B8812E7AE25B005D448B /* WireLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 0176B8802E7AE25B005D448B /* WireLogging */; }; 01D2B7A32D64A8F50030D28D /* WireCoreCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = 01D2B7A22D64A8F50030D28D /* WireCoreCrypto */; }; 1657FA9A2D9C2EB800A7B337 /* WireCoreCryptoUniffi in Frameworks */ = {isa = PBXBuildFile; productRef = 1657FA992D9C2EB800A7B337 /* WireCoreCryptoUniffi */; }; - 34A4F28F2F8FBC4B006FA043 /* WireUtilitiesPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 34A4F28E2F8FBC4B006FA043 /* WireUtilitiesPackage */; }; 34DC44B52E01C210004D5DD5 /* WireNetwork in Frameworks */ = {isa = PBXBuildFile; productRef = 34DC44B42E01C210004D5DD5 /* WireNetwork */; }; 5902AC752DA92365000A8F7F /* WireFoundationSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 5902AC742DA92365000A8F7F /* WireFoundationSupport */; }; 591B6E4C2C8B09C6009F8A7B /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C9A66E1CAD77930039E10C /* CoreData.framework */; }; @@ -190,7 +189,6 @@ E6707E902BBD683F00469D57 /* PINCache in Frameworks */, 59B48C5C2C89CBBD00EA7999 /* WireFoundation in Frameworks */, EE8DA9702954A03E00F58B79 /* WireImages.framework in Frameworks */, - 34A4F28F2F8FBC4B006FA043 /* WireUtilitiesPackage in Frameworks */, 59537CDB2CFF8F2B00920B59 /* WireLogging in Frameworks */, EE8DA96A2954A03100F58B79 /* WireTransport.framework in Frameworks */, 01D2B7A32D64A8F50030D28D /* WireCoreCrypto in Frameworks */, @@ -349,7 +347,6 @@ 59506A872E2E61BD0051B07A /* SwiftProtobuf */, 5950B0342E2EA1070051B07A /* GenericMessageProtocol */, CBB648A42E33D18900FA52AA /* WireData */, - 34A4F28E2F8FBC4B006FA043 /* WireUtilitiesPackage */, ); productName = WireDataModel; productReference = F9C9A4FC1CAD5DF10039E10C /* WireDataModel.framework */; @@ -922,10 +919,6 @@ isa = XCSwiftPackageProductDependency; productName = WireCoreCryptoUniffi; }; - 34A4F28E2F8FBC4B006FA043 /* WireUtilitiesPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = WireUtilitiesPackage; - }; 34DC44B42E01C210004D5DD5 /* WireNetwork */ = { isa = XCSwiftPackageProductDependency; productName = WireNetwork; diff --git a/wire-ios-request-strategy/Sources/E2EIdentity/E2EIKeyPackageRotator.swift b/wire-ios-request-strategy/Sources/E2EIdentity/E2EIKeyPackageRotator.swift index 65cac383f1f..4580ffff680 100644 --- a/wire-ios-request-strategy/Sources/E2EIdentity/E2EIKeyPackageRotator.swift +++ b/wire-ios-request-strategy/Sources/E2EIdentity/E2EIKeyPackageRotator.swift @@ -85,7 +85,7 @@ public class E2EIKeyPackageRotator: E2EIKeyPackageRotating { throw Error.invalidIdentity } - let crlNewDistributionPoints = try await coreCrypto.extendedTransaction { context in + let crlNewDistributionPoints = try await coreCrypto.transaction { context in try await context.saveX509Credential( enrollment: enrollment, certificateChain: certificateChain @@ -117,7 +117,7 @@ public class E2EIKeyPackageRotator: E2EIKeyPackageRotating { return mlsGroupIDs } - try await coreCrypto.extendedTransaction { context in + try await coreCrypto.transaction { context in for groupID in mlsConversationsToMigrate { do { try await context.e2eiRotate(conversationId: groupID.conversationId) @@ -144,7 +144,7 @@ public class E2EIKeyPackageRotator: E2EIKeyPackageRotating { throw Error.invalidCiphersuite } - try await coreCrypto.extendedTransaction { coreCryptoContext in + try await coreCrypto.transaction { coreCryptoContext in try await coreCryptoContext.deleteStaleKeyPackages( ciphersuite: ciphersuite.coreCryptoCipherSuite ) diff --git a/wire-ios-sync-engine/Source/E2EI/CRL/CertificateRevocationListsChecker.swift b/wire-ios-sync-engine/Source/E2EI/CRL/CertificateRevocationListsChecker.swift index b998bf53072..5809bca5440 100644 --- a/wire-ios-sync-engine/Source/E2EI/CRL/CertificateRevocationListsChecker.swift +++ b/wire-ios-sync-engine/Source/E2EI/CRL/CertificateRevocationListsChecker.swift @@ -131,7 +131,7 @@ public class CertificateRevocationListsChecker: CertificateRevocationListsChecki let crlData = try await crlAPI.getRevocationList(from: crlURL) // register the CRL with core crypto - let registration = try await coreCrypto.extendedTransaction { + let registration = try await coreCrypto.transaction { try await $0.e2eiRegisterCrl(crlDp: distributionPoint.absoluteString, crlDer: crlData) } diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift index 48419ba32a1..44000207bb3 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager+CallKitManagerDelegate.swift @@ -99,7 +99,14 @@ extension SessionManager: CallKitManagerDelegate { } func didEndAllCalls() { - WireLogger.calling.info("all calls ended, suspending background tasks", attributes: .safePublic) + guard UIApplication.shared.applicationState == .background else { + return + } + + WireLogger.calling.info( + "all calls ended in background, suspending all syncs", + attributes: .safePublic + ) Task { for userSession in backgroundUserSessions.values { await userSession.syncAgent?.suspend() diff --git a/wire-ios-sync-engine/Source/Synchronization/SyncAgent.swift b/wire-ios-sync-engine/Source/Synchronization/SyncAgent.swift index 155e49fecea..02928458875 100644 --- a/wire-ios-sync-engine/Source/Synchronization/SyncAgent.swift +++ b/wire-ios-sync-engine/Source/Synchronization/SyncAgent.swift @@ -23,7 +23,6 @@ import WireDomain import WireFoundation import WireLogging import WireUtilities -import WireUtilitiesPackage // sourcery: AutoMockable protocol SyncAgentProtocol { @@ -137,6 +136,9 @@ final class SyncAgent: NSObject, SyncAgentProtocol { } } catch is CancellationError { // ignore error + } catch URLError.cancelled { + // ignore error, this is a result of cancelling the sync while a `URLSessionDataTask` is in progress, + // we treat it the same as a `CancellationError` } catch { delegate?.syncAgentDidFailSyncing( self, diff --git a/wire-ios-sync-engine/Source/Use cases/CheckOneOnOneConversationIsReadyUseCase.swift b/wire-ios-sync-engine/Source/Use cases/CheckOneOnOneConversationIsReadyUseCase.swift index 8bfae6cf58e..62145c151de 100644 --- a/wire-ios-sync-engine/Source/Use cases/CheckOneOnOneConversationIsReadyUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/CheckOneOnOneConversationIsReadyUseCase.swift @@ -88,7 +88,7 @@ struct CheckOneOnOneConversationIsReadyUseCase: CheckOneOnOneConversationIsReady // MARK: - Helpers private func isMLSConversationEstablished(groupID: MLSGroupID) async throws -> Bool { - try await coreCryptoProvider.coreCrypto().extendedTransaction { + try await coreCryptoProvider.coreCrypto().transaction { try await $0.conversationExists(conversationId: groupID.conversationId) } } diff --git a/wire-ios-sync-engine/Source/Use cases/GetE2eIdentityCertificatesUseCase.swift b/wire-ios-sync-engine/Source/Use cases/GetE2eIdentityCertificatesUseCase.swift index af0004a45f3..f9cf2f2bb80 100644 --- a/wire-ios-sync-engine/Source/Use cases/GetE2eIdentityCertificatesUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/GetE2eIdentityCertificatesUseCase.swift @@ -127,7 +127,7 @@ public final class GetE2eIdentityCertificatesUseCase: GetE2eIdentityCertificates conversationId: WireCoreCryptoUniffi.ConversationId, clientIDs: [WireCoreCryptoUniffi.ClientId] ) async throws -> [WireIdentity] { - try await coreCrypto.extendedTransaction { + try await coreCrypto.transaction { try await $0.getDeviceIdentities( conversationId: conversationId, deviceIds: clientIDs diff --git a/wire-ios-sync-engine/Source/Use cases/GetIsE2EIdentityEnabledUseCase.swift b/wire-ios-sync-engine/Source/Use cases/GetIsE2EIdentityEnabledUseCase.swift index dbb4594f8fa..5ba54e17aa3 100644 --- a/wire-ios-sync-engine/Source/Use cases/GetIsE2EIdentityEnabledUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/GetIsE2EIdentityEnabledUseCase.swift @@ -39,7 +39,7 @@ public final class GetIsE2EIdentityEnabledUseCase: GetIsE2EIdentityEnabledUseCas public func invoke() async throws -> Bool { let ciphersuite = await featureRepository.fetchMLS().config.defaultCipherSuite.coreCryptoCipherSuite let coreCrypto = try await coreCryptoProvider.coreCrypto() - return try await coreCrypto.extendedTransaction { + return try await coreCrypto.transaction { try await $0.e2eiIsEnabled(ciphersuite: ciphersuite) } } diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/AppVersionMigrations/AppVersionMigration_4_12_2.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/AppVersionMigrations/AppVersionMigration_4_12_2.swift index b9e274f3643..aa4fdc2a543 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/AppVersionMigrations/AppVersionMigration_4_12_2.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/AppVersionMigrations/AppVersionMigration_4_12_2.swift @@ -42,7 +42,7 @@ struct AppVersionMigration_4_12_2: AppVersionMigration { for mlsGroupID in mlsGroupIDs { - try await coreCrypto.extendedTransaction { ccContext in + try await coreCrypto.transaction { ccContext in let epoch: UInt64 = if try await ccContext .conversationExists(conversationId: mlsGroupID.conversationId) { UInt64(try await ccContext.conversationEpoch(conversationId: mlsGroupID.conversationId)) diff --git a/wire-ios-system/Source/ExpiringActivity.swift b/wire-ios-system/Source/ExpiringActivity.swift new file mode 100644 index 00000000000..c93843c6796 --- /dev/null +++ b/wire-ios-system/Source/ExpiringActivity.swift @@ -0,0 +1,115 @@ +// +// 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 WireLogging + +protocol ExpiringActivityInterface { + + func performExpiringActivity(withReason reason: String, using block: @escaping @Sendable (Bool) -> Void) + +} + +extension ProcessInfo: ExpiringActivityInterface {} + +/// The expiring activity is not allowed to run possibly because the background execution time has already expired. + +public struct ExpiringActivityNotAllowedToRun: Error {} + +/// Execute an async function inside an [performExpiringActivity](https://developer.apple.com/documentation/foundation/processinfo/1617030-performexpiringactivity) +/// which cancels the task when the activity expires. It's up to the async function to handle the cancellation by for +/// example +/// calling [Task.checkCancellation](https://developer.apple.com/documentation/swift/task/checkcancellation()) at the +/// appropriate time. +/// +/// - Parameters: +/// - reason: Description of what the activity does, helpful for debugging purposes. +/// - block: async operation which supports cancellation. + +public func withExpiringActivity(reason: String, block: @escaping () async throws -> Void) async throws { + let manager = ExpiringActivityManager() + try await manager.withExpiringActivity(reason: reason, block: block) +} + +actor ExpiringActivityManager { + + let api: any ExpiringActivityInterface + var task: Task? + + init() { + self.init(api: ProcessInfo.processInfo) + } + + init(api: any ExpiringActivityInterface) { + self.api = api + } + + func withExpiringActivity(reason: String, block: @escaping () async throws -> Void) async throws { + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + api.performExpiringActivity(withReason: reason) { expiring in + if !expiring { + let semaphore = DispatchSemaphore(value: 0) + Task { + do { + WireLogger.backgroundActivity.debug("Start of activity: \(reason)") + try await self.startWork(block: block, semaphore: semaphore).value + WireLogger.backgroundActivity.debug("Expiring activity completed: \(reason)") + continuation.resume() + } catch { + WireLogger.backgroundActivity.warn("Expiring activity ended with an error: \(error)") + continuation.resume(throwing: error) + } + + } + semaphore.wait() + } else { + WireLogger.backgroundActivity.warn("Background activity is expiring: \(reason)") + Task { + do { + try await self.stopWork() + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + } onCancel: { + Task { try? await self.stopWork() } + } + } + + func startWork(block: @escaping () async throws -> Void, semaphore: DispatchSemaphore) -> Task { + let task = Task { + defer { + WireLogger.backgroundActivity.debug("Releasing semaphore") + semaphore.signal() + } + try await block() + } + self.task = task + return task + } + + func stopWork() throws { + guard let task else { throw ExpiringActivityNotAllowedToRun() } + task.cancel() + self.task = nil + } +} diff --git a/wire-ios-system/Tests/ExpiringActivityTests.swift b/wire-ios-system/Tests/ExpiringActivityTests.swift new file mode 100644 index 00000000000..5972aa7a283 --- /dev/null +++ b/wire-ios-system/Tests/ExpiringActivityTests.swift @@ -0,0 +1,151 @@ +// +// 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 XCTest +@testable import WireSystem + +class ExpiringActivityTests: XCTestCase { + + let concurrentQueue = DispatchQueue(label: "activity queue", attributes: [.concurrent]) + + func testThatTaskIsCancelled_WhenActivityExpires() async throws { + + // given + let api = MockExpiringActivityAPI() + let sut = ExpiringActivityManager(api: api) + + api.method = { _, block in + self.concurrentQueue.async { + block(false) + } + self.concurrentQueue.async { + block(true) + } + } + + // when + do { + try await sut.withExpiringActivity(reason: "Expiring test activity") { + while true { + await Task.yield() + try Task.checkCancellation() + } + } + XCTFail("Expected a cancellation error to be thrown") + } catch {} + } + + func testThatTaskIsCancelled_WhenActivityIsNotAllowedToBegin() async throws { + + // given + let api = MockExpiringActivityAPI() + let sut = ExpiringActivityManager(api: api) + + api.method = { _, block in + self.concurrentQueue.async { + block(true) + } + } + + // when + do { + try await sut.withExpiringActivity(reason: "Expiring test activity") { + while true { + await Task.yield() + try Task.checkCancellation() + } + } + XCTFail("Expected an expiring activity not allowed to run error to be thrown") + } catch {} + } + + func testThatWorkIsCancelled_WhenOuterTaskIsCancelled() async throws { + + // given + let api = MockExpiringActivityAPI() + let sut = ExpiringActivityManager(api: api) + + api.method = { _, block in + self.concurrentQueue.async { + block(false) + } + } + + let workStarted = expectation(description: "work started") + + // when + let outerTask = Task { + try await sut.withExpiringActivity(reason: "test activity") { + workStarted.fulfill() + while true { + await Task.yield() + try Task.checkCancellation() + } + } + } + + await fulfillment(of: [workStarted], timeout: 1) + outerTask.cancel() + + // then + do { + try await outerTask.value + XCTFail("Expected a cancellation error to be thrown") + } catch {} + } + + func testThatTaskEndsWithoutError_WhenActivityCompletes() async throws { + + // given + let api = MockExpiringActivityAPI() + let sut = ExpiringActivityManager(api: api) + + api.method = { _, block in + self.concurrentQueue.async { + block(false) + } + } + + // when + do { + try await sut.withExpiringActivity(reason: "Expiring test activity") { + try Task.checkCancellation() + } + } catch { + XCTFail("Expected the activity to end without any error thrown") + } + } + +} + +private class MockExpiringActivityAPI: ExpiringActivityInterface { + + typealias MethodCall = (_ reason: String, _ block: @escaping @Sendable (Bool) -> Void) -> Void + + var method: MethodCall? + + func performExpiringActivity(withReason reason: String, using block: @escaping @Sendable (Bool) -> Void) { + if let method { + method(reason, block) + } else { + fatalError("no mock for `performExpiringActivity(withReason:using:)`") + } + } + +} diff --git a/wire-ios/Wire-iOS/Sources/AppDelegate.swift b/wire-ios/Wire-iOS/Sources/AppDelegate.swift index c3400b78629..8fc40946d59 100644 --- a/wire-ios/Wire-iOS/Sources/AppDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/AppDelegate.swift @@ -515,7 +515,7 @@ private extension AppDelegate { let data = try Data(contentsOf: URL(filePath: path)) return try BackendEnvironment2.fromJSON(data, environmentType: .default) } catch { - fatalError("unabled to fetch default environment: \(error)") + fatalError("unable to fetch default environment: \(error)") } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift index 34ceeb57c94..9f56020aa8b 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/Debug Report/SettingsDebugReportViewController.swift @@ -55,14 +55,14 @@ class SettingsDebugReportViewController: UIViewController { }() private lazy var sendReportButton = createButton( - title: Strings.TechnicalReport.sendReport.capitalized, + title: Strings.TechnicalReport.sendReport, action: UIAction { [weak self] action in self?.didTapSendReport(sender: action.sender as! UIButton) } ) private lazy var shareReportButton: UIButton = createButton( - title: Strings.TechnicalReport.shareReport.capitalized, + title: Strings.TechnicalReport.shareReport, action: UIAction { [weak self] _ in self?.didTapShareReport() } ) diff --git a/wire-ios/WireUITests/Helper/TestServicesClient.swift b/wire-ios/WireUITests/Helper/TestServicesClient.swift index b5a385a2183..3aaef4266f5 100644 --- a/wire-ios/WireUITests/Helper/TestServicesClient.swift +++ b/wire-ios/WireUITests/Helper/TestServicesClient.swift @@ -23,6 +23,7 @@ class TestServicesClient { let testServiceURL = "http://localhost:8080" let CONNECT_TIMEOUT: TimeInterval = 120 let RESPONSE_TIMEOUT: TimeInterval = 120 + private var instanceCache: [String: String] = [:] func sendHttpRequest(url: String, body: [String: Any], requestType: String) async throws -> (Data, URLResponse) { guard let requestUrl = URL(string: url) else { fatalError("Invalid URL") } @@ -47,10 +48,13 @@ class TestServicesClient { email: String, password: String, name: String, - verificationCode: String?, - deviceName: String? + verificationCode: String? ) async throws -> String { + if let cachedInstanceId = instanceCache[email] { + return cachedInstanceId + } + let url = URL(string: "\(testServiceURL)/api/v1/instance") guard let requestUrl = url else { fatalError() } @@ -59,7 +63,7 @@ class TestServicesClient { "password": password, "name": name, "developmentApiEnabled": true, - "deviceName": deviceName ?? "device1" + "deviceName": "device\(Int.random(in: 10_000 ... 99_999))" ] let (responseData, response) = try await sendHttpRequest( @@ -77,6 +81,7 @@ class TestServicesClient { CreateInstanceResponse.self, from: responseData ) + instanceCache[email] = instanceResponse.instanceId return instanceResponse.instanceId } @@ -88,8 +93,7 @@ class TestServicesClient { email: owner.email, password: owner.password, name: owner.name, - verificationCode: nil, - deviceName: nil + verificationCode: nil ) let url = URL(string: "\(testServiceURL)/api/v1/instance/\(instanceId)/conversation") @@ -134,8 +138,7 @@ class TestServicesClient { email: user.email, password: user.password, name: user.name, - verificationCode: nil, - deviceName: nil + verificationCode: nil ) let url = URL(string: "\(testServiceURL)/api/v1/instance/\(instanceId)/sendText") @@ -144,7 +147,8 @@ class TestServicesClient { var body: [String: Any] = [ "conversationId": conversationId.uuidString.lowercased(), "text": text, - "legalHoldStatus": 0 + "legalHoldStatus": 0, + "expectsReadConfirmation": true ] if domain != BackendTarget.staging.domainInfo { @@ -184,10 +188,11 @@ class TestServicesClient { type: String, user: UserInfo, fileName: String, - filepath: String, + filepath: String?, convoId: UUID, domain: String, timeoutMillis: Int = 0, + audio: [String: Any]? = nil ) async throws { @@ -195,8 +200,7 @@ class TestServicesClient { email: user.email, password: user.password, name: user.name, - verificationCode: nil, - deviceName: nil + verificationCode: nil ) let url = URL(string: "\(testServiceURL)/api/v1/instance/\(instanceId)/sendFile") @@ -204,11 +208,20 @@ class TestServicesClient { var body: [String: Any] = [ "conversationId": convoId.uuidString.lowercased(), - "data": try fileToBase64String(fileURL: URL(fileURLWithPath: filepath)), "fileName": fileName, - "type": type + "type": type, + "legalHoldStatus": 0, + "expectsReadConfirmation": true ] + if let filepath, !filepath.isEmpty { + body["data"] = try fileToBase64String(fileURL: URL(fileURLWithPath: filepath)) + } + + if let audio { + body["audio"] = audio + } + if domain != BackendTarget.staging.domainInfo { body["conversationDomain"] = domain } @@ -241,8 +254,7 @@ class TestServicesClient { email: user.email, password: user.password, name: user.name, - verificationCode: nil, - deviceName: nil + verificationCode: nil ) let url = URL(string: "\(testServiceURL)/api/v1/instance/\(instanceId)/sendImage") @@ -277,8 +289,7 @@ class TestServicesClient { email: user.email, password: user.password, name: user.name, - verificationCode: nil, - deviceName: nil + verificationCode: nil ) let url = URL(string: "\(testServiceURL)/api/v1/instance/\(instanceId)/getMessages") diff --git a/wire-ios/WireUITests/Pages/ActiveConversationPage.swift b/wire-ios/WireUITests/Pages/ActiveConversationPage.swift index d22a3c43dd3..15fc9e09d46 100644 --- a/wire-ios/WireUITests/Pages/ActiveConversationPage.swift +++ b/wire-ios/WireUITests/Pages/ActiveConversationPage.swift @@ -85,6 +85,12 @@ class ActiveConversationPage: PageModel { app.images.matching(identifier: Locators.ActiveConversationPage.fileTypeIcon.rawValue) } + func fileAttachment(name: String, type: String) -> XCUIElement { + app.buttons.containing( + NSPredicate(format: "label CONTAINS[c] %@ AND label CONTAINS[c] %@", name, type) + ).firstMatch + } + var labelSharedDriveIsOn: XCUIElement { app.staticTexts[Locators.ActiveConversationPage.labelSharedDriveON.rawValue] } diff --git a/wire-ios/WireUITests/Pages/MessagingTests.swift b/wire-ios/WireUITests/Pages/MessagingTests.swift index a66f5596365..4bad01a10c1 100644 --- a/wire-ios/WireUITests/Pages/MessagingTests.swift +++ b/wire-ios/WireUITests/Pages/MessagingTests.swift @@ -22,7 +22,7 @@ import XCTest final class MessagingTests: WireUITestCase { @MainActor - func testSendAndReceiveTextInGroupConversation_TC_8833_8840() async throws { + func testSendAndReceiveTextAndAudioInGroupConversation_TC_8833_8840_8835_8842() async throws { // GIVEN let groupName = UserGenerator.generateRandomConversationName() @@ -35,13 +35,15 @@ final class MessagingTests: WireUITestCase { ) let conversationId = try XCTUnwrap(conversationID, "conversationId is nil") - let conversationDomain = BackendContext.current.domainInfo let firstTimePage = try app.loginUser(email: teamOwner.email, password: teamOwner.password) let conversationsPage = try firstTimePage.acceptPopup() - // WHEN member send text + let durationInMillis = 5000 + let normalizedLoudness = (0 ..< 10).map { _ in Int.random(in: 0 ... 255) } + + // WHEN member sends text and audio file try await testServicesClient.sendText( user: teamMembers[0], text: messageFromMember1, @@ -49,6 +51,19 @@ final class MessagingTests: WireUITestCase { domain: conversationDomain ) + try await testServicesClient.sendFile( + type: "audio", + user: teamMembers[0], + fileName: "audio-message", + filepath: nil, + convoId: conversationId, + domain: conversationDomain, + audio: [ + "durationInMillis": durationInMillis, + "normalizedLoudness": normalizedLoudness + ] + ) + XCTAssertTrue( conversationsPage.unreadMessagesCount.waitForExistence(timeout: 2), "Unread messages count element did not appear" @@ -63,16 +78,16 @@ final class MessagingTests: WireUITestCase { "Expected message '\(messageFromMember1)' not found in sent messages: \(receivedMessages)" ) - let senderName = activeConversationPage.getSenderName() - XCTAssertEqual( - senderName, - teamMembers[0].name, - "Sender info didn't match expected value \(teamMembers[0].name)" + verifyMessageReceivedAndSenderInfo( + attachment: activeConversationPage.fileTypeIcons.firstMatch, + on: activeConversationPage, + expectedSenderName: teamMembers[0].name, + failureMessage: "Expected audio attachment not found" ) } @MainActor - func testSendAndReceiveImageInGroupConversation_TC_8834_8841() async throws { + func testSendAndReceiveImageAndVideoInGroupConversation_TC_8834_8841_8836_8843() async throws { // GIVEN let groupName = UserGenerator.generateRandomConversationName() @@ -83,18 +98,24 @@ final class MessagingTests: WireUITestCase { ) let conversationId = try XCTUnwrap(conversationID, "conversationId is nil") - let conversationDomain = BackendContext.current.domainInfo let firstTimePage = try app.loginUser(email: teamOwner.email, password: teamOwner.password) let conversationsPage = try firstTimePage.acceptPopup() + let imageURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() .appendingPathComponent("TestServicesData/Img/testImage.jpg") let imageExtension = imageURL.pathExtension - // WHEN member send image + let videoURL = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("TestServicesData/Video/testVideo.mp4") + let videoExtension = videoURL.pathExtension + + // WHEN member sends image and video files try await testServicesClient.sendImage( user: teamMembers[0], fileURL: imageURL, @@ -103,6 +124,15 @@ final class MessagingTests: WireUITestCase { domain: conversationDomain ) + try await testServicesClient.sendFile( + type: videoExtension, + user: teamMembers[0], + fileName: "testVideo.mp4", + filepath: videoURL.path, + convoId: conversationId, + domain: conversationDomain + ) + XCTAssertTrue( conversationsPage.unreadMessagesCount.waitForExistence(timeout: 2), "Unread messages count element did not appear" @@ -112,15 +142,41 @@ final class MessagingTests: WireUITestCase { // THEN XCTAssertTrue( - activeConversationPage.fileTypeIcons.firstMatch.exists, + activeConversationPage.fileTypeIcons.firstMatch.waitForExistence(timeout: 5), "Expected image attachment not found" ) + verifyMessageReceivedAndSenderInfo( + attachment: activeConversationPage.fileAttachment(name: "TESTVIDEO", type: "MP4"), + on: activeConversationPage, + expectedSenderName: teamMembers[0].name, + failureMessage: "Expected MP4 video attachment not found" + ) + } + + private func verifyMessageReceivedAndSenderInfo( + attachment: XCUIElement, + on activeConversationPage: ActiveConversationPage, + expectedSenderName: String, + timeout: TimeInterval = 5, + failureMessage: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertTrue( + attachment.waitForExistence(timeout: timeout), + "\(failureMessage) Debug: \(attachment.debugDescription)", + file: file, + line: line + ) + let senderName = activeConversationPage.getSenderName() XCTAssertEqual( senderName, - teamMembers[0].name, - "Sender info didn't match expected value \(teamMembers[0].name)" + expectedSenderName, + "Sender info didn't match expected value \(expectedSenderName)", + file: file, + line: line ) } } diff --git a/wire-ios/WireUITests/Pages/RecycleBinPage.swift b/wire-ios/WireUITests/Pages/RecycleBinPage.swift index 46544224647..a0b4e9b4bd9 100644 --- a/wire-ios/WireUITests/Pages/RecycleBinPage.swift +++ b/wire-ios/WireUITests/Pages/RecycleBinPage.swift @@ -29,6 +29,27 @@ class RecycleBinPage: PageModel { app.staticTexts[Locators.WireDrive.FilesPage.recycleBinPageheader.rawValue] } + var moreButton: XCUIElement { + app.buttons + .matching(identifier: Locators.WireDrive.FilesContentPage.fileItem(0)) + .firstMatch + } + + var restoreOnMenuContext: XCUIElement { + app.buttons[Locators.WireDrive.FileMenu.restore.identifier] + } + + var restoreOptionOnBottomSheet: XCUIElement { + app.buttons[Locators.WireDrive.FilesItemPage.confirmRestoreButton.rawValue].firstMatch + } + + var closeRecycleBinButton: XCUIElement { + app.navigationBars[Locators.WireDrive.FilesPage.recycleBinPageheader.rawValue] + .buttons + .matching(identifier: Locators.WireDrive.FilesPage.close.rawValue) + .firstMatch + } + private var fileTexts: XCUIElementQuery { app.staticTexts .matching(identifier: Locators.WireDrive.FilesContentPage.fileItem(0)) @@ -42,4 +63,15 @@ class RecycleBinPage: PageModel { self.fileName.label == fileName } + func openMoreOptionsOnFileAndRestoreFile() throws -> RecycleBinPage { + moreButton.tap() + restoreOnMenuContext.tap() + restoreOptionOnBottomSheet.tap() + return self + } + + func closeRecycleBin() throws -> SharedDriveFilesPage { + closeRecycleBinButton.tap() + return try SharedDriveFilesPage() + } } diff --git a/wire-ios/WireUITests/Pages/SharedDriveFilesPage.swift b/wire-ios/WireUITests/Pages/SharedDriveFilesPage.swift index 7a50ab000ac..c52ae8178e5 100644 --- a/wire-ios/WireUITests/Pages/SharedDriveFilesPage.swift +++ b/wire-ios/WireUITests/Pages/SharedDriveFilesPage.swift @@ -38,12 +38,8 @@ class SharedDriveFilesPage: PageModel { app.images.matching(identifier: Locators.WireDrive.FilesContentPage.fileItem(0)).firstMatch } - var sentBy: XCUIElement { - fileTexts.element(boundBy: 1) - } - - var fileName: XCUIElement { - fileTexts.element(boundBy: 0) + private var fileMetadataText: XCUIElement { + fileTexts.firstMatch } var deleteOnMenuContext: XCUIElement { @@ -51,7 +47,7 @@ class SharedDriveFilesPage: PageModel { } var deleteOptionOnBottomSheet: XCUIElement { - app.buttons[Locators.WireDrive.FilesPage.deleteOnBottomSheet.rawValue] + app.buttons[Locators.WireDrive.FilesItemPage.confirmDeleteButton.rawValue].firstMatch } var moreOptionOnSharedDrive: XCUIElement { @@ -70,18 +66,18 @@ class SharedDriveFilesPage: PageModel { @discardableResult func verifyFileTypeAndMetadata( - username: String, + name: String, file: StaticString = #filePath, line: UInt = #line ) throws -> SharedDriveFilesPage { XCTAssertTrue(fileIcon.exists, file: file, line: line) - XCTAssertTrue(fileName.label.contains(".png"), file: file, line: line) - XCTAssertTrue(sentBy.label.contains(username), file: file, line: line) + XCTAssertTrue(fileMetadataText.label.contains(".png"), file: file, line: line) + XCTAssertTrue(fileMetadataText.label.contains(name), file: file, line: line) return try SharedDriveFilesPage() } var fileNameText: String { - fileName.label + fileMetadataText.label } func openMoreOptionsOnFileAndDelete() throws -> SharedDriveFilesPage { @@ -97,4 +93,9 @@ class SharedDriveFilesPage: PageModel { return try RecycleBinPage() } + + func verifyFileMovedToSharedDrive(fileName: String) -> Bool { + fileMetadataText.label.contains(fileName) + + } } diff --git a/wire-ios/WireUITests/TestServicesData/Video/testVideo.mp4 b/wire-ios/WireUITests/TestServicesData/Video/testVideo.mp4 new file mode 100644 index 00000000000..5ff127886c6 Binary files /dev/null and b/wire-ios/WireUITests/TestServicesData/Video/testVideo.mp4 differ diff --git a/wire-ios/WireUITests/WireDriveTests.swift b/wire-ios/WireUITests/WireDriveTests.swift index f6eeae907e9..9652010f8a9 100644 --- a/wire-ios/WireUITests/WireDriveTests.swift +++ b/wire-ios/WireUITests/WireDriveTests.swift @@ -41,7 +41,7 @@ final class WireDriveTests: WireUITestCase { } private func verifyDriveEnabledConversation(on activeConversationPage: ActiveConversationPage) { - XCTAssertTrue(activeConversationPage.labelSharedDriveIsOn.exists) + XCTAssertTrue(activeConversationPage.labelSharedDriveIsOn.waitForExistence(timeout: 2)) XCTAssertTrue(activeConversationPage.labelSelfDeletingMessageIsOFF.exists) XCTAssertFalse(activeConversationPage.selfDeletingMessageButton.isHittable) @@ -50,14 +50,46 @@ final class WireDriveTests: WireUITestCase { XCTAssertTrue(activeConversationPage.sharedDriveButton.exists) } + private func createTeamAndEnableDrive( + memberCount: Int = 2, + channelEnabled: Bool = false + ) async throws -> (teamOwner: UserInfo, teamMembers: [UserInfo]) { + let (teamOwner, teamMembers, _, _) = try await userHelper.registerTeam(withMemberCount: memberCount) + let teamID = try XCTUnwrap(teamOwner.teamID) + + if channelEnabled { + try await userHelper.unlockAndEnableChannelFeature(teamID: teamID) + } + + try await userHelper.unlockAndEnableDriveFeature(teamID: teamID) + return (teamOwner, teamMembers) + } + + private func uploadSketchAttachment( + message: String, + for user: UserInfo + ) throws -> ActiveConversationPage { + let activeConversationPage = try loginAndOpenConversation(for: user) + .typeMessageAndAttachSketch(message) + + activeConversationPage.waitToUploadToFinishAndSend() + return activeConversationPage + } + + private func uploadSketchAndOpenSharedDrive( + message: String, + for user: UserInfo + ) throws -> SharedDriveFilesPage { + try uploadSketchAttachment(message: message, for: user) + .openSharedDrive() + } + @MainActor func testCreateGroupConversationWithDrive_TC_8955() async throws { // GIVEN let groupName = UserGenerator.generateRandomConversationName() - let (teamOwner, teamMembers, _, _) = try await userHelper.registerTeam(withMemberCount: 2) - let teamID = try XCTUnwrap(teamOwner.teamID) - try await userHelper.unlockAndEnableDriveFeature(teamID: teamID) + let (teamOwner, teamMembers) = try await createTeamAndEnableDrive() // WHEN let activeConversationPage = try app.loginUser(email: teamOwner.email, password: teamOwner.password) @@ -78,10 +110,7 @@ final class WireDriveTests: WireUITestCase { // GIVEN let channelName = UserGenerator.generateRandomConversationName() - let (teamOwner, teamMembers, _, _) = try await userHelper.registerTeam(withMemberCount: 2) - let teamID = try XCTUnwrap(teamOwner.teamID) - try await userHelper.unlockAndEnableChannelFeature(teamID: teamID) - try await userHelper.unlockAndEnableDriveFeature(teamID: teamID) + let (teamOwner, teamMembers) = try await createTeamAndEnableDrive(channelEnabled: true) // WHEN let activeConversationPage = try app.loginUser(email: teamOwner.email, password: teamOwner.password) @@ -125,15 +154,11 @@ final class WireDriveTests: WireUITestCase { ) // WHEN - let activeConversationPage = try loginAndOpenConversation(for: teamOwner) - .typeMessageAndAttachSketch(message) - - activeConversationPage.waitToUploadToFinishAndSend() + let sharedDrivePage = try uploadSketchAndOpenSharedDrive(message: message, for: teamOwner) // THEN - try activeConversationPage - .openSharedDrive() - .verifyFileTypeAndMetadata(username: teamOwner.username) + try sharedDrivePage + .verifyFileTypeAndMetadata(name: teamOwner.name) } @MainActor @@ -146,13 +171,7 @@ final class WireDriveTests: WireUITestCase { ) // WHEN - let activeConversationPage = try loginAndOpenConversation(for: teamOwner) - .typeMessageAndAttachSketch(message) - - activeConversationPage.waitToUploadToFinishAndSend() - - let sharedDrivePage = try activeConversationPage - .openSharedDrive() + let sharedDrivePage = try uploadSketchAndOpenSharedDrive(message: message, for: teamOwner) let sharedFileName = sharedDrivePage.fileNameText @@ -164,4 +183,29 @@ final class WireDriveTests: WireUITestCase { XCTAssertTrue(recycleBinPage.verifyFileMovedToRecycleBin(fileName: sharedFileName)) } + + @MainActor + func testRestoringFileFromRecycleBinToDrive_TC_8959() async throws { + + // GIVEN + let message = "Attachment with Text" + let teamOwner = try await createDriveEnabledConversation( + .group(UserGenerator.generateRandomConversationName()) + ) + + // WHEN + var sharedDrivePage = try uploadSketchAndOpenSharedDrive(message: message, for: teamOwner) + + let sharedFileName = sharedDrivePage.fileNameText + + sharedDrivePage = try sharedDrivePage + .openMoreOptionsOnFileAndDelete() + .openRecycleBin() + .openMoreOptionsOnFileAndRestoreFile() + .closeRecycleBin() + + // THEN + XCTAssertTrue(sharedDrivePage.verifyFileMovedToSharedDrive(fileName: sharedFileName)) + + } } diff --git a/wire-ios/WireUITests/ZCallingTests.swift b/wire-ios/WireUITests/ZCallingTests.swift index 70c85f7bc88..f02bb143936 100644 --- a/wire-ios/WireUITests/ZCallingTests.swift +++ b/wire-ios/WireUITests/ZCallingTests.swift @@ -91,7 +91,7 @@ final class ZCallingTests: WireUITestCase { /// Team Owner create group conversation and initiate a group call with members @MainActor - func test_MultipleUsersJoiningGroupCall_TC_8910_TC_8880() async throws { + func testMultipleUsersJoiningGroupCall_TC_8910_TC_8880() async throws { do { let teamAndGroupCallSetup = try await makeTeamAndGroupCallSetup(memberCount: 3)