From a1cfc5f2bbae11906536f3694abc5f6a4a96fbbb Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:23:45 +0200 Subject: [PATCH 01/18] add getMLSOneToOneConversation endpoint, refactor getLegalHoldStatus endpoint, add UTs - WPB-10727 --- .../ConversationsAPI/ConversationsAPI.swift | 9 ++ .../ConversationsAPIError.swift | 11 +- .../ConversationsAPI/ConversationsAPIV0.swift | 7 + .../ConversationsAPI/ConversationsAPIV5.swift | 20 +++ .../WireAPI/APIs/TeamsAPI/TeamsAPI.swift | 8 +- .../WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift | 36 +++-- .../WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift | 8 +- .../WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift | 8 +- .../Models/Connection/Connection.swift | 17 +++ .../Models/Messaging/MessageProtocol.swift | 2 +- .../Models/SelfUser/ManagingSystem.swift | 2 +- .../WireAPI/Models/SelfUser/SSOID.swift | 2 +- .../WireAPI/Models/SelfUser/SelfUser.swift | 2 +- .../Models/Team/TeamMemberLegalHold.swift | 39 +++++ .../Sources/WireAPI/Models/User/Service.swift | 2 +- .../WireAPI/Models/User/UserAsset.swift | 6 +- .../ConversationsAPITests.swift | 134 +++++++++++++++++- ...OnOneConversationV5SuccessResponse200.json | 64 +++++++++ ...eToOneConversationRequest.request-0-v5.txt | 4 + ...eToOneConversationRequest.request-0-v6.txt | 4 + .../APIs/TeamsAPI/TeamsAPITests.swift | 34 +++-- .../testGetLegalholdRequest.request-0-v0.txt | 4 + .../testGetLegalholdRequest.request-0-v1.txt | 4 + .../testGetLegalholdRequest.request-0-v2.txt | 4 + .../testGetLegalholdRequest.request-0-v3.txt | 4 + .../testGetLegalholdRequest.request-0-v4.txt | 4 + .../testGetLegalholdRequest.request-0-v5.txt | 4 + .../testGetLegalholdRequest.request-0-v6.txt | 4 + 28 files changed, 403 insertions(+), 44 deletions(-) create mode 100644 WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift create mode 100644 WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetMLSOneOnOneConversationV5SuccessResponse200.json create mode 100644 WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift index 024352db155..27613bed953 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift @@ -31,4 +31,13 @@ public protocol ConversationsAPI { /// Fetch conversation list with qualified identifiers. func getConversations(for identifiers: [QualifiedID]) async throws -> ConversationList + /// Fetches a user MLS one to one conversation. + /// - parameters: + /// - userID: The user ID to fetch the MLS one to one conversation for. + /// - domain: The domain of the one to one conversation. + + func getMLSOneToOneConversation( + userID: String, + in domain: String + ) async throws -> Conversation } diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift index 9efaa71d4ca..c5bb3bcc977 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift @@ -21,8 +21,17 @@ public enum ConversationsAPIError: Error { /// Failure if functionality has not been implemented. case notImplemented - + /// Failure if http body is invalid. case invalidBody + + /// Unsupported endpoint for API version + case unsupportedEndpointForAPIVersion + + /// MLS not enabled + case mlsNotEnabled + + /// Users not connected + case usersNotConnected } diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift index 6a613d5fcd4..7af692516e0 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift @@ -90,6 +90,13 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI { .failure(code: .badRequest, error: ConversationsAPIError.invalidBody) .parse(response) } + + func getMLSOneToOneConversation( + userID: String, + in domain: String + ) async throws -> Conversation { + throw ConversationsAPIError.unsupportedEndpointForAPIVersion + } } // MARK: Encodables diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift index c7202b218d8..f3ead41cbb0 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift @@ -38,6 +38,26 @@ class ConversationsAPIV5: ConversationsAPIV4 { .success(code: .ok, type: QualifiedConversationListV5.self) // Change in v5 .parse(response) } + + override func getMLSOneToOneConversation( + userID: String, + in domain: String + ) async throws -> Conversation { + let resourcePath = "\(pathPrefix)/conversations/one2one/\(domain)/\(userID)" + + let request = HTTPRequest( + path: resourcePath, + method: .get + ) + + let response = try await httpClient.executeRequest(request) + + return try ResponseParser() + .success(code: .ok, type: ConversationV5.self) + .failure(code: .badRequest, label: "mls-not-enabled", error: ConversationsAPIError.mlsNotEnabled) + .failure(code: .forbidden, label: "not-connected", error: ConversationsAPIError.usersNotConnected) + .parse(response) + } } // MARK: Decodables diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift index 433751b043b..b15be126316 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift @@ -51,16 +51,16 @@ public protocol TeamsAPI { maxResults: UInt ) async throws -> [TeamMember] - /// Get the legalhold status of a team member. + /// Get the legalhold of a team member. /// /// - Parameters: /// - teamID: The id of the team. /// - userID: The id of the member. - /// - Returns: The legalhold status of the member. + /// - Returns: The legalhold of the member. - func getLegalholdStatus( + func getLegalhold( for teamID: Team.ID, userID: UUID - ) async throws -> LegalholdStatus + ) async throws -> TeamMemberLegalHold } diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift index d83a9dc193e..02b2eb6bcdb 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift @@ -96,12 +96,12 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI { .parse(response) } - // MARK: - Get legalhold status + // MARK: - Get team member legalhold - func getLegalholdStatus( + func getLegalhold( for teamID: Team.ID, userID: UUID - ) async throws -> LegalholdStatus { + ) async throws -> TeamMemberLegalHold { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -110,7 +110,7 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI { let response = try await httpClient.executeRequest(request) return try ResponseParser() - .success(code: .ok, type: LegalholdStatusResponseV0.self) + .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) .failure(code: .notFound, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) @@ -288,7 +288,7 @@ enum LegalholdStatusV0: String, Decodable { case pending case disabled case noConsent = "no_consent" - + func toAPIModel() -> LegalholdStatus { switch self { case .enabled: @@ -301,15 +301,35 @@ enum LegalholdStatusV0: String, Decodable { .noConsent } } +} +struct LegalHoldLastPrekeyV0: Decodable, ToAPIModelConvertible { + let id: Int + let key: String + + func toAPIModel() -> Prekey { + Prekey( + id: id, + base64EncodedKey: key + ) + } } -struct LegalholdStatusResponseV0: Decodable, ToAPIModelConvertible { +struct TeamMemberLegalHoldResponseV0: Decodable, ToAPIModelConvertible { + let lastPrekey: LegalHoldLastPrekeyV0 let status: LegalholdStatusV0 + + enum CodingKeys: String, CodingKey { + case status + case lastPrekey = "last_prekey" + } - func toAPIModel() -> LegalholdStatus { - status.toAPIModel() + func toAPIModel() -> TeamMemberLegalHold { + TeamMemberLegalHold( + status: status.toAPIModel(), + prekey: lastPrekey.toAPIModel() + ) } } diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift index 686ed0ef421..a7c08307cc8 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift @@ -89,12 +89,12 @@ class TeamsAPIV4: TeamsAPIV3 { .parse(response) } - // MARK: - Get legalhold status + // MARK: - Get team member legalhold - override func getLegalholdStatus( + override func getLegalhold( for teamID: Team.ID, userID: UUID - ) async throws -> LegalholdStatus { + ) async throws -> TeamMemberLegalHold { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -104,7 +104,7 @@ class TeamsAPIV4: TeamsAPIV3 { // New: 400 return try ResponseParser() - .success(code: .ok, type: LegalholdStatusResponseV0.self) + .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) .failure(code: .badRequest, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift index 48293daf146..bfbd5d60371 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift @@ -86,12 +86,12 @@ class TeamsAPIV5: TeamsAPIV4 { .parse(response) } - // MARK: - Get legalhold status + // MARK: - Get team member legalhold - override func getLegalholdStatus( + override func getLegalhold( for teamID: Team.ID, userID: UUID - ) async throws -> LegalholdStatus { + ) async throws -> TeamMemberLegalHold { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -101,7 +101,7 @@ class TeamsAPIV5: TeamsAPIV4 { // New: 404 invalid request. return try ResponseParser() - .success(code: .ok, type: LegalholdStatusResponseV0.self) + .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) .failure(code: .notFound, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) diff --git a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift index 73a0e20775c..3d93ee7d3a7 100644 --- a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift +++ b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift @@ -47,5 +47,22 @@ public struct Connection: Equatable, Codable { /// current status of connection public let status: ConnectionStatus + + public init(senderID: UUID?, + receiverID: UUID?, + receiverQualifiedID: QualifiedID?, + conversationID: UUID?, + qualifiedConversationID: QualifiedID?, + lastUpdate: Date, + status: ConnectionStatus + ) { + self.senderID = senderID + self.receiverID = receiverID + self.receiverQualifiedID = receiverQualifiedID + self.conversationID = conversationID + self.qualifiedConversationID = qualifiedConversationID + self.lastUpdate = lastUpdate + self.status = status + } } diff --git a/WireAPI/Sources/WireAPI/Models/Messaging/MessageProtocol.swift b/WireAPI/Sources/WireAPI/Models/Messaging/MessageProtocol.swift index 6d05de168d1..da2a94a76ed 100644 --- a/WireAPI/Sources/WireAPI/Models/Messaging/MessageProtocol.swift +++ b/WireAPI/Sources/WireAPI/Models/Messaging/MessageProtocol.swift @@ -21,7 +21,7 @@ import Foundation /// A message protocol to use in end to end /// encrypted communication. -public enum MessageProtocol: String, Codable { +public enum MessageProtocol: String, Codable, Sendable { /// The Proteus messaging protocol. diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift index 505e6385602..24ae2ab325b 100644 --- a/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift +++ b/WireAPI/Sources/WireAPI/Models/SelfUser/ManagingSystem.swift @@ -20,7 +20,7 @@ import Foundation /// The managing system of the self user identity -public enum ManagingSystem { +public enum ManagingSystem: String, Sendable { /// User identity is managed with Wire diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift index c4ac625a35e..8aba3593296 100644 --- a/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift +++ b/WireAPI/Sources/WireAPI/Models/SelfUser/SSOID.swift @@ -20,7 +20,7 @@ import Foundation /// The sso id of the self user -public struct SSOID: Equatable { +public struct SSOID: Equatable, Sendable { /// The self user's scim external id diff --git a/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift b/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift index abb730be37e..01499d4432a 100644 --- a/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift +++ b/WireAPI/Sources/WireAPI/Models/SelfUser/SelfUser.swift @@ -20,7 +20,7 @@ import Foundation /// User profile for self -public struct SelfUser: Equatable { +public struct SelfUser: Equatable, Sendable { /// The unique id of the self user diff --git a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift new file mode 100644 index 00000000000..c9acce59d0d --- /dev/null +++ b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift @@ -0,0 +1,39 @@ +// +// Wire +// Copyright (C) 2024 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/. +// + +public typealias LegalHoldPrekey = Prekey + +/// The team member legal hold. +public struct TeamMemberLegalHold: Equatable, Sendable { + + /// The legal hold status + + public let status: LegalholdStatus + + /// The legal hold prekey + + public let prekey: LegalHoldPrekey + + public init( + status: LegalholdStatus, + prekey: LegalHoldPrekey + ) { + self.status = status + self.prekey = prekey + } +} diff --git a/WireAPI/Sources/WireAPI/Models/User/Service.swift b/WireAPI/Sources/WireAPI/Models/User/Service.swift index cc5e9fcf3a5..0ca5f4995b5 100644 --- a/WireAPI/Sources/WireAPI/Models/User/Service.swift +++ b/WireAPI/Sources/WireAPI/Models/User/Service.swift @@ -20,7 +20,7 @@ import Foundation /// Service information for a bot. -public struct Service: Equatable, Codable { +public struct Service: Equatable, Codable, Sendable { /// The service's id. diff --git a/WireAPI/Sources/WireAPI/Models/User/UserAsset.swift b/WireAPI/Sources/WireAPI/Models/User/UserAsset.swift index 3676a6b515f..291be64b6c6 100644 --- a/WireAPI/Sources/WireAPI/Models/User/UserAsset.swift +++ b/WireAPI/Sources/WireAPI/Models/User/UserAsset.swift @@ -20,7 +20,7 @@ import Foundation /// Describes the size of the user asset. -public enum UserAssetSize: String, Codable, Equatable { +public enum UserAssetSize: String, Codable, Equatable, Sendable { /// Smaller version of the asset optimised for size @@ -33,7 +33,7 @@ public enum UserAssetSize: String, Codable, Equatable { /// Describes the purpose of the user asset. -public enum UserAssetType: String, Codable, Equatable { +public enum UserAssetType: String, Codable, Equatable, Sendable { /// User profile image @@ -42,7 +42,7 @@ public enum UserAssetType: String, Codable, Equatable { /// An asset associated with a user, typically a profile picture. -public struct UserAsset: Codable, Equatable { +public struct UserAsset: Codable, Equatable, Sendable { /// Unique key for this asset, if the asset is updated it will be assigned new key. diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift index 73995200b25..18f82a12049 100644 --- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift @@ -17,7 +17,7 @@ // import XCTest - +import WireTestingPackage @testable import WireAPI final class ConversationsAPITests: XCTestCase { @@ -62,6 +62,23 @@ final class ConversationsAPITests: XCTestCase { } } } + + + func testGetMLSOneToOneConversationRequest() async throws { + // Given + + let apiVersions = APIVersion.v5.andNextVersions + + // Then + + try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in + // When + _ = try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } func testGetConversationIdentifiers() async throws { // given @@ -465,4 +482,119 @@ final class ConversationsAPITests: XCTestCase { XCTFail("expected error 'FailureResponse'") } } + + func testGetMLSOneToOneConversation_Success_Response_V5_And_Next_Versions() async throws { + // Given + + let httpClient = try HTTPClientMock( + code: .ok, + payloadResourceName: "testGetMLSOneOnOneConversationV5SuccessResponse200" + ) + + let supportedVersions = APIVersion.v5.andNextVersions + + let suts = supportedVersions.map { $0.buildAPI(client: httpClient) } + + // When + + try await withThrowingTaskGroup(of: Conversation.self) { taskGroup in + for sut in suts { + taskGroup.addTask { + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + for try await value in taskGroup { + // Then + XCTAssertEqual(value.id, Scaffolding.mlsConversationID) + } + } + } + + func testGetMLSOneToOneConversation_UnsupportedVersionError_V0_to_V4() async throws { + // Given + let httpClient = HTTPClientMock( + code: .ok, + payload: nil + ) + + let unsupportedVersions: [APIVersion] = [.v0, .v1, .v2, .v3, .v4] + let suts = unsupportedVersions.map { $0.buildAPI(client: httpClient) } + + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + for sut in suts { + taskGroup.addTask { + // Then + await self.XCTAssertThrowsError(ConversationsAPIError.unsupportedEndpointForAPIVersion) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + try await taskGroup.waitForAll() + } + } + } + + func testGetMLSOneToOneConversation_Failure_Response_MLS_Not_Enabled() async throws { + // Given + + let httpClient = try HTTPClientMock( + code: .badRequest, + errorLabel: "mls-not-enabled" + ) + + let sut = APIVersion.v5.buildAPI(client: httpClient) + + // Then + + await XCTAssertThrowsError(ConversationsAPIError.mlsNotEnabled) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + func testGetMLSOneToOneConversation_Failure_Response_Not_Connected() async throws { + // Given + + let httpClient = try HTTPClientMock( + code: .forbidden, + errorLabel: "not-connected" + ) + + let sut = APIVersion.v5.buildAPI(client: httpClient) + + // Then + + await XCTAssertThrowsError(ConversationsAPIError.usersNotConnected) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + private enum Scaffolding { + static let userID = "99db9768-04e3-4b5d-9268-831b6a25c4ab" + static let domain = "domain.com" + static let mlsConversationID = UUID(uuidString: "99db9768-04e3-4b5d-9268-831b6a25c4ab")! + } + +} + +private extension APIVersion { + func buildAPI(client: any HTTPClient) -> any ConversationsAPI { + let builder = ConversationsAPIBuilder(httpClient: client) + return builder.makeAPI(for: self) + } } diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetMLSOneOnOneConversationV5SuccessResponse200.json b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetMLSOneOnOneConversationV5SuccessResponse200.json new file mode 100644 index 00000000000..a025cdf1767 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/Resources/testGetMLSOneOnOneConversationV5SuccessResponse200.json @@ -0,0 +1,64 @@ +{ + "access": [ + "private" + ], + "access_role": [ + "team_member" + ], + "cipher_suite": 2, + "creator": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "epoch": 0, + "epoch_timestamp": "2021-05-12T10:52:02Z", + "group_id": "string", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "last_event": "string", + "last_event_time": "2024-06-04T15:03:07.598Z", + "members": { + "others": [ + { + "conversation_role": "string", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "qualified_id": { + "domain": "example.com", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab" + }, + "service": { + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "provider": "99db9768-04e3-4b5d-9268-831b6a25c4ab" + }, + "status": 0 + } + ], + "self": { + "conversation_role": "string", + "hidden": true, + "hidden_ref": "string", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "otr_archived": true, + "otr_archived_ref": "2024-01-01T00:00:00.000Z", + "otr_muted_ref": "2024-01-01T00:00:00.000Z", + "otr_muted_status": 0, + "qualified_id": { + "domain": "example.com", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab" + }, + "service": { + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "provider": "99db9768-04e3-4b5d-9268-831b6a25c4ab" + }, + "status": "string", + "status_ref": "string", + "status_time": "string" + } + }, + "message_timer": 0, + "name": "string", + "protocol": "proteus", + "qualified_id": { + "domain": "example.com", + "id": "99db9768-04e3-4b5d-9268-831b6a25c4ab" + }, + "receipt_mode": 0, + "team": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "type": 0 + } diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt new file mode 100644 index 00000000000..db3bc27a409 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v5.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v5/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt new file mode 100644 index 00000000000..8fbe78be92b --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/__Snapshots__/ConversationsAPITests/testGetMLSOneToOneConversationRequest.request-0-v6.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v6/conversations/one2one/domain.com/99db9768-04e3-4b5d-9268-831b6a25c4ab" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index 16e95f412f4..a7d90be5333 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -59,9 +59,9 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalholdStatusRequest() async throws { + func testGetLegalholdRequest() async throws { try await apiSnapshotHelper.verifyRequestForAllAPIVersions { sut in - _ = try await sut.getLegalholdStatus(for: .mockID1, userID: .mockID2) + _ = try await sut.getLegalhold(for: .mockID1, userID: .mockID2) } } @@ -249,13 +249,17 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalholdStatus_SuccessResponse_200_V0() async throws { + func testGetLegalhold_SuccessResponse_200_V0() async throws { // Given let httpClient = try HTTPClientMock( code: .ok, jsonResponse: """ { - "status": "pending" + "status": "pending", + "last_prekey": { + "id": 12345, + "key": "foo" + } } """ ) @@ -263,16 +267,18 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // When - let result = try await sut.getLegalholdStatus( + let result = try await sut.getLegalhold( for: Team.ID(), userID: UUID() ) // Then - XCTAssertEqual(result, .pending) + let expectedPrekey = LegalHoldPrekey(id: 12345, base64EncodedKey: "foo") + XCTAssertEqual(result.status, .pending) + XCTAssertEqual(result.prekey, expectedPrekey) } - func testGetLegalholdStatus_FailureResponse_InvalidRequest_V0() async throws { + func testGetLegalhold_FailureResponse_InvalidRequest_V0() async throws { // Given let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "") let sut = TeamsAPIV0(httpClient: httpClient) @@ -280,14 +286,14 @@ final class TeamsAPITests: XCTestCase { // Then await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { // When - try await sut.getLegalholdStatus( + try await sut.getLegalhold( for: Team.ID(), userID: UUID() ) } } - func testGetLegalholdStatus_FailureResponse_MemberNotFound_V0() async throws { + func testGetLegalhold_FailureResponse_MemberNotFound_V0() async throws { // Given let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "no-team-member") let sut = TeamsAPIV0(httpClient: httpClient) @@ -295,7 +301,7 @@ final class TeamsAPITests: XCTestCase { // Then await XCTAssertThrowsError(TeamsAPIError.teamMemberNotFound) { // When - try await sut.getLegalholdStatus( + try await sut.getLegalhold( for: Team.ID(), userID: UUID() ) @@ -372,7 +378,7 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalholdStatus_FailureResponse_InvalidRequest_V4() async throws { + func testGetLegalhold_FailureResponse_InvalidRequest_V4() async throws { // Given let httpClient = try HTTPClientMock(code: .badRequest, errorLabel: "") let sut = TeamsAPIV4(httpClient: httpClient) @@ -380,7 +386,7 @@ final class TeamsAPITests: XCTestCase { // Then await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { // When - try await sut.getLegalholdStatus( + try await sut.getLegalhold( for: Team.ID(), userID: UUID() ) @@ -401,7 +407,7 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalholdStatus_FailureResponse_InvalidRequest_V5() async throws { + func testGetLegalhold_FailureResponse_InvalidRequest_V5() async throws { // Given let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "") let sut = TeamsAPIV5(httpClient: httpClient) @@ -409,7 +415,7 @@ final class TeamsAPITests: XCTestCase { // Then await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { // When - try await sut.getLegalholdStatus( + try await sut.getLegalhold( for: Team.ID(), userID: UUID() ) diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt new file mode 100644 index 00000000000..f08e8e90296 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v0.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt new file mode 100644 index 00000000000..c2629df16cf --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v1.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v1/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt new file mode 100644 index 00000000000..03509c80054 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v2.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v2/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt new file mode 100644 index 00000000000..f0a8a91c317 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v3.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v3/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt new file mode 100644 index 00000000000..df69c86a56a --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v4.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v4/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt new file mode 100644 index 00000000000..bb7d083563a --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v5.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v5/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt new file mode 100644 index 00000000000..7dca81d74cb --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdRequest.request-0-v6.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v6/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none From 1d6796b1130e35e35fcdefa9f2eb907649504f6d Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:34:39 +0200 Subject: [PATCH 02/18] add pullMLSOneOnOneConversation repo method, add UTs - WPB-10727 --- .../ConversationLocalStore.swift | 22 +++++++ .../ConversationRepository.swift | 57 +++++++++++++++++++ .../ConversationRepositoryError.swift | 4 ++ .../ConversationRepositoryTests.swift | 26 ++++++++- 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift index f60ece3159a..4ff2c7c577b 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift @@ -66,6 +66,17 @@ public protocol ConversationLocalStoreProtocol { user: ZMUser, removalDate: Date ) async + + /// Fetches a MLS conversation locally. + /// + /// - parameters: + /// - groupID: The MLS group ID object. + /// + /// - returns : A MLS conversation. + + func fetchMLSConversation( + with groupID: WireDataModel.MLSGroupID + ) async -> ZMConversation? } public final class ConversationLocalStore: ConversationLocalStoreProtocol { @@ -216,6 +227,17 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol { } } } + + public func fetchMLSConversation( + with groupID: WireDataModel.MLSGroupID + ) async -> ZMConversation? { + await context.perform { [context] in + ZMConversation.fetch( + with: groupID, + in: context + ) + } + } // MARK: - Private diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift index e6a5612ef27..a6914974451 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift @@ -38,6 +38,30 @@ public protocol ConversationRepositoryProtocol { user: ZMUser, removalDate: Date ) async + + /// Pulls and stores a MLS one to one conversation locally. + /// + /// - parameters: + /// - userID: The user ID. + /// - domain: The user domain. + /// + /// - returns : The MLS group ID. + + func pullMLSOneToOneConversation( + userID: String, + domain: String + ) async throws -> String + + /// Fetches a MLS conversation locally. + /// + /// - parameters: + /// - groupID: The MLS group ID. + /// + /// - returns : A MLS conversation. + + func fetchMLSConversation( + with groupID: String + ) async -> ZMConversation? } @@ -128,5 +152,38 @@ public final class ConversationRepository: ConversationRepositoryProtocol { removalDate: removalDate ) } + + public func pullMLSOneToOneConversation( + userID: String, + domain: String + ) async throws -> String { + let mlsConversation = try await conversationsAPI.getMLSOneToOneConversation( + userID: userID, + in: domain + ) + + guard let mlsGroupID = mlsConversation.mlsGroupID else { + throw ConversationRepositoryError.mlsConversationShouldHaveAGroupID + } + + await conversationsLocalStore.storeConversation( + mlsConversation, + isFederationEnabled: backendInfo.isFederationEnabled + ) + + return mlsGroupID + } + + public func fetchMLSConversation( + with groupID: String + ) async -> ZMConversation? { + guard let mlsGroupID = MLSGroupID(base64Encoded: groupID) else { + return nil + } + + return await conversationsLocalStore.fetchMLSConversation( + with: mlsGroupID + ) + } } diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift index d13f0fba153..73ace9b1af4 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift @@ -25,5 +25,9 @@ enum ConversationRepositoryError: Error { /// Unable to delete conversation. case failedToDeleteConversation(Error) + + /// Missing MLS group ID + + case mlsConversationShouldHaveAGroupID } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift index 6fc4d021a5f..c01afb91951 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift @@ -308,6 +308,25 @@ class ConversationRepositoryTests: XCTestCase { accuracy: 0.1 ) } + + func testGetMLSOneToOneConversation() async throws { + // Mock + + mockConversationsAPI() + + // When + + let mlsGroupID = try await sut.pullMLSOneToOneConversation( + userID: Scaffolding.userID.uuidString, + domain: Scaffolding.domain + ) + + let mlsConversation = await sut.fetchMLSConversation(with: mlsGroupID) + + // Then + + XCTAssertEqual(mlsConversation?.remoteIdentifier, Scaffolding.conversationOneOnOneType.id) + } } @@ -355,6 +374,8 @@ extension ConversationRepositoryTests { notFound: conversationList.notFound, failed: conversationList.failed ) + + conversationsAPI.getMLSOneToOneConversationUserIDIn_MockValue = Scaffolding.conversationOneOnOneType } private enum Scaffolding { @@ -364,11 +385,14 @@ extension ConversationRepositoryTests { static let teamConversationID = UUID() static let anotherTeamConversationID = UUID() static let conversationID = UUID() + static let domain = "domain.com" static func date(from string: String) -> Date { ISO8601DateFormatter.fractionalInternetDateTime.date(from: string)! } + static let base64EncodedString = "pQABARn//wKhAFggHsa0CszLXYLFcOzg8AA//E1+Dl1rDHQ5iuk44X0/PNYDoQChAFgg309rkhG6SglemG6kWae81P1HtQPx9lyb6wExTovhU4cE9g==" + nonisolated(unsafe) static let conversationList = ConversationList( found: [conversationSelfType, conversationGroupType, @@ -481,7 +505,7 @@ extension ConversationRepositoryTests { teamID: UUID(uuidString: "99db9768-04e3-4b5d-9268-831b6a25c4ae")!, type: .oneOnOne, messageProtocol: .proteus, - mlsGroupID: "", + mlsGroupID: base64EncodedString, cipherSuite: .MLS_128_DHKEMP256_AES128GCM_SHA256_P256, epoch: 0, epochTimestamp: nil, From 4a41195da0dd1bb7bf29825d3167bd1b6ae9cf8d Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:36:21 +0200 Subject: [PATCH 03/18] refactor existing repo methods, fix UTs - WPB-10727 --- .../Connections/ConnectionsRepositiory.swift | 7 +- .../ConversationLabelsRepository.swift | 2 +- .../Repositories/Team/TeamRepository.swift | 101 ++++++++++----- .../Repositories/User/UserRepository.swift | 122 +++++++++++++++--- .../Repositories/TeamRepositoryTests.swift | 13 +- .../Repositories/UserRepositoryTests.swift | 44 ++++++- 6 files changed, 225 insertions(+), 64 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepositiory.swift b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepositiory.swift index 8109798335c..320881e8090 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepositiory.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Connections/ConnectionsRepositiory.swift @@ -20,24 +20,25 @@ import Foundation import WireAPI import WireDataModel +// sourcery: AutoMockable /// Facilitate access to connections related domain objects. /// /// A repository provides an abstraction for the access and storage /// of domain models, concealing how and where the models are stored /// as well as the possible source(s) of the models. -protocol ConnectionsRepositoryProtocol { +public protocol ConnectionsRepositoryProtocol { /// Pull self team metadata frmo the server and store locally. func pullConnections() async throws } -struct ConnectionsRepository: ConnectionsRepositoryProtocol { +public struct ConnectionsRepository: ConnectionsRepositoryProtocol { private let connectionsAPI: any ConnectionsAPI private let context: NSManagedObjectContext - init( + public init( connectionsAPI: any ConnectionsAPI, context: NSManagedObjectContext ) { diff --git a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift index ee625d28af3..373fd9a88c9 100644 --- a/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/ConversationsLabels/ConversationLabelsRepository.swift @@ -20,8 +20,8 @@ import CoreData import WireAPI import WireDataModel +// sourcery: AutoMockable /// Facilitate access to conversation labels related domain objects. - protocol ConversationLabelsRepositoryProtocol { /// Pull conversation labels from the server and store locally diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift index ca75636beed..441d1724ac0 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift @@ -20,13 +20,13 @@ import Foundation import WireAPI import WireDataModel +// sourcery: AutoMockable /// Facilitate access to team related domain objects. /// /// A repository provides an abstraction for the access and storage /// of domain models, concealing how and where the models are stored /// as well as the possible source(s) of the models. - -protocol TeamRepositoryProtocol { +public protocol TeamRepositoryProtocol { /// Pull self team metadata from the server and store locally. @@ -40,9 +40,9 @@ protocol TeamRepositoryProtocol { func pullSelfTeamMembers() async throws - /// Fetch the legalhold status for the self user from the server. + /// Fetch the legalhold for the self user from the server. - func fetchSelfLegalholdStatus() async throws -> LegalholdStatus + func fetchSelfLegalhold() async throws -> TeamMemberLegalHold /// Deletes the member of a team. /// - Parameter userID: The ID of the team member. @@ -59,17 +59,23 @@ protocol TeamRepositoryProtocol { /// - Parameter membershipID: The id of the team member. func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws + + func pullSelfLegalHoldStatus() async throws } -final class TeamRepository: TeamRepositoryProtocol { +public final class TeamRepository: TeamRepositoryProtocol { + + // MARK: - Properties private let selfTeamID: UUID private let userRepository: any UserRepositoryProtocol private let teamsAPI: any TeamsAPI private let context: NSManagedObjectContext + + // MARK: - Object lifecycle - init( + public init( selfTeamID: UUID, userRepository: any UserRepositoryProtocol, teamsAPI: any TeamsAPI, @@ -81,19 +87,19 @@ final class TeamRepository: TeamRepositoryProtocol { self.context = context } - // MARK: - Pull self team + // MARK: - Public - func pullSelfTeam() async throws { + public func pullSelfTeam() async throws { let team = try await fetchSelfTeamRemotely() await storeTeamLocally(team) } - func deleteMembership( + public func deleteMembership( forUser userID: UUID, fromTeam teamID: UUID, at time: Date ) async throws { - let user = try await userRepository.fetchUser(with: userID) + let user = try await userRepository.fetchUser(with: userID, domain: nil) let member = try await context.perform { guard let member = user.membership else { @@ -113,7 +119,7 @@ final class TeamRepository: TeamRepositoryProtocol { } } - func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws { + public func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws { try await context.perform { [context] in guard let member = Member.fetch( @@ -128,6 +134,55 @@ final class TeamRepository: TeamRepositoryProtocol { try context.save() } } + + public func pullSelfTeamRoles() async throws { + let teamRoles = try await fetchSelfTeamRolesRemotely() + try await storeTeamRolesLocally(teamRoles) + } + + public func pullSelfTeamMembers() async throws { + let teamMembers = try await fetchSelfTeamMembersRemotely() + try await storeTeamMembersLocally(teamMembers) + } + + public func pullSelfLegalHoldStatus() async throws { + let selfUser = await context.perform { [userRepository] in + userRepository.fetchSelfUser() + } + + let selfUserLegalHold = try await fetchSelfLegalhold() + + switch selfUserLegalHold.status { + case .pending: + guard let selfClientID = selfUser.selfClient()?.remoteIdentifier else { + return + } + + await userRepository.addLegalHoldRequest( + for: selfUser.remoteIdentifier, + clientID: selfClientID, + lastPrekey: selfUserLegalHold.prekey + ) + + case .disabled: + try await userRepository.disableUserLegalHold() + default: + break + } + } + + public func fetchSelfLegalhold() async throws -> TeamMemberLegalHold { + let selfUserID: UUID = await context.perform { [userRepository] in + userRepository.fetchSelfUser().remoteIdentifier + } + + return try await teamsAPI.getLegalhold( + for: selfTeamID, + userID: selfUserID + ) + } + + // MARK: - Private private func fetchSelfTeamRemotely() async throws -> WireAPI.Team { do { @@ -166,11 +221,6 @@ final class TeamRepository: TeamRepositoryProtocol { // MARK: - Pull self team roles - func pullSelfTeamRoles() async throws { - let teamRoles = try await fetchSelfTeamRolesRemotely() - try await storeTeamRolesLocally(teamRoles) - } - private func fetchSelfTeamRolesRemotely() async throws -> [WireAPI.ConversationRole] { do { return try await teamsAPI.getTeamRoles(for: selfTeamID) @@ -222,11 +272,6 @@ final class TeamRepository: TeamRepositoryProtocol { // MARK: - Pull self team members - func pullSelfTeamMembers() async throws { - let teamMembers = try await fetchSelfTeamMembersRemotely() - try await storeTeamMembersLocally(teamMembers) - } - private func fetchSelfTeamMembersRemotely() async throws -> [WireAPI.TeamMember] { do { return try await teamsAPI.getTeamMembers( @@ -277,20 +322,6 @@ final class TeamRepository: TeamRepositoryProtocol { } } } - - // MARK: - Fetch self legalhold status - - func fetchSelfLegalholdStatus() async throws -> LegalholdStatus { - let selfUserID: UUID = await context.perform { [userRepository] in - userRepository.fetchSelfUser().remoteIdentifier - } - - return try await teamsAPI.getLegalholdStatus( - for: selfTeamID, - userID: selfUserID - ) - } - } private extension ConversationAction { diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift index f048d79dc2b..a2a33a7977b 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift @@ -27,6 +27,10 @@ import WireDataModel /// of domain models, concealing how and where the models are stored /// as well as the possible source(s) of the models. public protocol UserRepositoryProtocol { + + /// Pulls self user and stores it locally + + func pullSelfUser() async throws /// Fetch self user from the local store @@ -52,9 +56,10 @@ public protocol UserRepositoryProtocol { /// Fetches a user with a specific id. /// - Parameter id: The ID of the user. + /// - Parameter domain: The domain of the user. /// - Returns: A `ZMUser` object. - func fetchUser(with id: UUID) async throws -> ZMUser + func fetchUser(with id: UUID, domain: String?) async throws -> ZMUser /// Fetches or creates a user client locally. /// @@ -134,6 +139,14 @@ public final class UserRepository: UserRepositoryProtocol { } // MARK: - Public + + public func pullSelfUser() async throws { + let selfUser = try await selfUserAPI.getSelfUser() + + await context.perform { [self] in + persistSelfUser(from: selfUser) + } + } public func fetchSelfUser() -> ZMUser { ZMUser.selfUser(in: context) @@ -175,9 +188,9 @@ public final class UserRepository: UserRepositoryProtocol { } } - public func fetchUser(with id: UUID) async throws -> ZMUser { + public func fetchUser(with id: UUID, domain: String?) async throws -> ZMUser { try await context.perform { [context] in - guard let user = ZMUser.fetch(with: id, in: context) else { + guard let user = ZMUser.fetch(with: id, domain: domain, in: context) else { throw UserRepositoryError.failedToFetchUser(id) } @@ -324,24 +337,91 @@ public final class UserRepository: UserRepositoryProtocol { // MARK: - Private private func persistUser(from user: WireAPI.User) { - let persistedUser = ZMUser.fetchOrCreate(with: user.id.uuid, domain: user.id.domain, in: context) - - guard user.deleted == false else { - return persistedUser.markAccountAsDeleted(at: Date()) + let persistedUser = ZMUser.fetchOrCreate( + with: user.id.uuid, + domain: user.id.domain, + in: context + ) + + let previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key + let completeProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key + + updateUserMetadata( + persistedUser, + deleted: user.deleted == true, + name: user.name, + handle: user.handle, + teamID: user.teamID, + accentID: user.accentID, + previewProfileAssetIdentifier: previewProfileAssetIdentifier, + completeProfileAssetIdentifier: completeProfileAssetIdentifier, + email: user.email, + expiresAt: user.expiresAt, + serviceIdentifier: user.service?.id.transportString(), + providerIdentifier: user.service?.provider.transportString(), + supportedProtocols: user.supportedProtocols?.toDomainModel() ?? [.proteus] + ) + } + + private func persistSelfUser( + from selfUser: WireAPI.SelfUser + ) { + let persistedSelfUser = ZMUser.selfUser(in: context) + let previewProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .preview })?.key + let completeProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .complete })?.key + + updateUserMetadata( + persistedSelfUser, + deleted: selfUser.deleted == true, + name: selfUser.name, + handle: selfUser.handle, + teamID: selfUser.teamID, + accentID: selfUser.accentID, + previewProfileAssetIdentifier: previewProfileAssetIdentifier, + completeProfileAssetIdentifier: completeProfileAssetIdentifier, + email: selfUser.email, + expiresAt: selfUser.expiresAt, + serviceIdentifier: selfUser.service?.id.transportString(), + providerIdentifier: selfUser.service?.provider.transportString(), + supportedProtocols: selfUser.supportedProtocols?.toDomainModel() ?? [.proteus] + ) + + persistedSelfUser.remoteIdentifier = selfUser.qualifiedID.uuid + persistedSelfUser.domain = selfUser.qualifiedID.domain + persistedSelfUser.managedBy = selfUser.managedBy?.rawValue + } + + private func updateUserMetadata( + _ user: ZMUser, + deleted: Bool, + name: String, + handle: String?, + teamID: UUID?, + accentID: Int, + previewProfileAssetIdentifier: String?, + completeProfileAssetIdentifier: String?, + email: String?, + expiresAt: Date?, + serviceIdentifier: String?, + providerIdentifier: String?, + supportedProtocols: Set + ) { + + guard deleted == false else { + return user.markAccountAsDeleted(at: .now) } - - persistedUser.name = user.name - persistedUser.handle = user.handle - persistedUser.teamIdentifier = user.teamID - persistedUser.accentColorValue = Int16(user.accentID) - persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key - persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key - persistedUser.emailAddress = user.email - persistedUser.expiresAt = user.expiresAt - persistedUser.serviceIdentifier = user.service?.id.transportString() - persistedUser.providerIdentifier = user.service?.provider.transportString() - persistedUser.supportedProtocols = user.supportedProtocols?.toDomainModel() ?? [.proteus] - persistedUser.needsToBeUpdatedFromBackend = false + + user.name = name + user.handle = handle + user.teamIdentifier = teamID + user.accentColorValue = Int16(accentID) + user.previewProfileAssetIdentifier = previewProfileAssetIdentifier + user.completeProfileAssetIdentifier = completeProfileAssetIdentifier + user.emailAddress = email + user.expiresAt = expiresAt + user.serviceIdentifier = serviceIdentifier + user.providerIdentifier = providerIdentifier + user.supportedProtocols = supportedProtocols + user.needsToBeUpdatedFromBackend = false } - } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift index 27931fd5513..afd1998d343 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift @@ -239,13 +239,13 @@ final class TeamRepositoryTests: XCTestCase { func testFetchSelfLegalholdStatus() async throws { // Mock - teamsAPI.getLegalholdStatusForUserID_MockValue = .pending + teamsAPI.getLegalholdForUserID_MockValue = Scaffolding.teamMemberLegalHold // When - let result = try await sut.fetchSelfLegalholdStatus() + let result = try await sut.fetchSelfLegalhold() // Then - XCTAssertEqual(result, .pending) + XCTAssertEqual(result, Scaffolding.teamMemberLegalHold) } } @@ -272,4 +272,11 @@ private enum Scaffolding { static let member2CreatorID = UUID() static let member2legalholdStatus = LegalholdStatus.pending static let member2Permissions = Permissions.member.rawValue + + static let teamMemberLegalHold = TeamMemberLegalHold( + status: .pending, + prekey: prekey + ) + + static let prekey = LegalHoldPrekey(id: 2330, base64EncodedKey: "foo") } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift index 9dd20ba3394..2582b9178b3 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift @@ -323,12 +323,36 @@ class UserRepositoryTests: XCTestCase { XCTAssertEqual(user.isAccountDeleted, true) XCTAssertEqual(conversationsRepository.removeFromConversationsUserRemovalDate_Invocations.count, 1) } + + func testPullSelfUser() async throws { + + // Mock + selfUsersAPI.getSelfUser_MockValue = Scaffolding.selfUser + + // When + + try await sut.pullSelfUser() + + // Then + + await context.perform { [context] in + let selfUser = ZMUser.selfUser(in: context) + XCTAssertEqual(selfUser.remoteIdentifier, Scaffolding.selfUser.id) + XCTAssertEqual(selfUser.name, Scaffolding.selfUser.name) + XCTAssertEqual(selfUser.handle, Scaffolding.selfUser.handle) + XCTAssertEqual(selfUser.managedBy, "wire") + XCTAssertEqual(selfUser.emailAddress, Scaffolding.selfUser.email) + XCTAssertEqual(selfUser.supportedProtocols, [.mls]) + } + + } private enum Scaffolding { static let userID = UUID() static let userClientID = UUID().uuidString static let lastPrekeyId = 65_535 static let base64encodedString = "pQABAQoCoQBYIPEFMBhOtG0dl6gZrh3kgopEK4i62t9sqyqCBckq3IJgA6EAoQBYIC9gPmCdKyqwj9RiAaeSsUI7zPKDZS+CjoN+sfihk/5VBPY=" + static let qualifiedID = UserID(uuid: UUID(), domain: "example.com") nonisolated(unsafe) static let remoteUserClient = WireAPI.UserClient( id: userClientID, @@ -351,7 +375,7 @@ class UserRepositoryTests: XCTestCase { ) nonisolated(unsafe) static let user1 = User( - id: QualifiedID(uuid: UUID(), domain: "example.com"), + id: qualifiedID, name: "user1", handle: "handle1", teamID: nil, @@ -364,6 +388,24 @@ class UserRepositoryTests: XCTestCase { supportedProtocols: [.mls], legalholdStatus: .disabled ) + + static let selfUser = SelfUser( + id: qualifiedID.uuid, + qualifiedID: qualifiedID, + ssoID: nil, + name: "username", + handle: "username", + teamID: UUID(), + phone: "", + accentID: 1, + managedBy: .wire, + assets: [], + deleted: false, + email: "username@wire.com", + expiresAt: .now, + service: nil, + supportedProtocols: [.mls] + ) } } From 60767d267b1e0259bd676601278ed0366b0ad5fe Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:37:32 +0200 Subject: [PATCH 04/18] add OneOnOneResolverUseCase, perform slow sync in SyncManager, add UTs --- .../project.pbxproj | 28 ++ .../Synchronization/SyncManager.swift | 55 ++- .../UseCases/OneOnOneResolverUseCase.swift | 326 ++++++++++++++++ .../PushSupportedProtocolsUseCase.swift | 7 +- .../generated/AutoMockable.generated.swift | 368 +++++++++++++++++- .../MockFeatureConfigRepositoryProtocol.swift | 135 +++++++ .../Synchronization/SyncManagerTests.swift | 78 +++- .../OneOnOneResolverUseCaseTests.swift | 355 +++++++++++++++++ .../Support/Sources/ModelHelper.swift | 30 +- 9 files changed, 1354 insertions(+), 28 deletions(-) create mode 100644 WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift create mode 100644 WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift create mode 100644 WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj index 4978d527aac..e7beb351b32 100644 --- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ C93961922C91B12800EA971A /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0E117D2C2C4080004BBD29 /* TestError.swift */; }; C93961932C91B15B00EA971A /* ConversationRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */; }; C97BCCAA2C98704B004F2D0D /* WireDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */; }; + C97C01412CAEBC6F000683C5 /* OneOnOneResolverUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */; }; + C97C01442CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01432CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift */; }; + C97C01472CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */; }; C99322D22C986E3A0065E10F /* TeamRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B22C986E3A0065E10F /* TeamRepository.swift */; }; C99322D32C986E3A0065E10F /* TeamRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */; }; C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B52C986E3A0065E10F /* SelfUserProvider.swift */; }; @@ -142,6 +145,9 @@ 01D0DCE92C1C8EA10076CB1C /* WireDomain.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = WireDomain.docc; sourceTree = ""; }; 01D0DCFC2C1C8F9B0076CB1C /* WireDomain.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = WireDomain.xctestplan; sourceTree = ""; }; 1623564F2C2B223100C6666C /* UserRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepositoryTests.swift; sourceTree = ""; }; + C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverUseCase.swift; sourceTree = ""; }; + C97C01432CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeatureConfigRepositoryProtocol.swift; sourceTree = ""; }; + C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverUseCaseTests.swift; sourceTree = ""; }; C99322B22C986E3A0065E10F /* TeamRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepository.swift; sourceTree = ""; }; C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepositoryError.swift; sourceTree = ""; }; C99322B52C986E3A0065E10F /* SelfUserProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfUserProvider.swift; sourceTree = ""; }; @@ -271,6 +277,7 @@ 017F67992C20801800B6E02D /* Repositories */ = { isa = PBXGroup; children = ( + C97C01422CAEC66C000683C5 /* Mock */, C9E8A3B62C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift */, C9E8A3AD2C73878B0093DD5C /* ConnectionsRepositoryTests.swift */, 017F67982C20801800B6E02D /* TeamRepositoryTests.swift */, @@ -285,6 +292,7 @@ 017F679A2C20801800B6E02D /* WireDomain */ = { isa = PBXGroup; children = ( + C97C01452CAEC8AF000683C5 /* UseCases */, C9C8FDCD2C9DBE0E00702B91 /* Event Processing */, EEC410252C60D48900E89394 /* Synchronization */, EE0E117C2C2C4076004BBD29 /* Helpers */, @@ -399,6 +407,22 @@ path = ../Tests; sourceTree = ""; }; + C97C01422CAEC66C000683C5 /* Mock */ = { + isa = PBXGroup; + children = ( + C97C01432CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift */, + ); + path = Mock; + sourceTree = ""; + }; + C97C01452CAEC8AF000683C5 /* UseCases */ = { + isa = PBXGroup; + children = ( + C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */, + ); + path = UseCases; + sourceTree = ""; + }; C99322B42C986E3A0065E10F /* Team */ = { isa = PBXGroup; children = ( @@ -512,6 +536,7 @@ C9EA76AD2C98548900A7D35C /* UseCases */ = { isa = PBXGroup; children = ( + C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */, C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */, ); path = UseCases; @@ -854,6 +879,7 @@ 0163EE812C20D71C00B37260 /* WireDomain.docc in Sources */, EEAD0A2E2C46B01900CC8658 /* UserUpdateEventProcessor.swift in Sources */, EEAD0A2A2C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift in Sources */, + C97C01412CAEBC6F000683C5 /* OneOnOneResolverUseCase.swift in Sources */, C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */, EEAD0A332C46B99800CC8658 /* FederationDeleteEventProcessor.swift in Sources */, C99322E42C986E3A0065E10F /* ConversationRepository.swift in Sources */, @@ -918,9 +944,11 @@ C93961922C91B12800EA971A /* TestError.swift in Sources */, EEC410262C60D48900E89394 /* SyncManagerTests.swift in Sources */, C9C8FDD32C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift in Sources */, + C97C01472CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift in Sources */, EE57A7032C2994420096F242 /* UpdateEventsRepositoryTests.swift in Sources */, C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */, C9E8A3C02C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift in Sources */, + C97C01442CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift in Sources */, CB7979162C738547006FBA58 /* TestSetup.swift in Sources */, 162356502C2B223100C6666C /* UserRepositoryTests.swift in Sources */, EE3F97542C2ADC4C00668DF1 /* ProteusMessageDecryptorTests.swift in Sources */, diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index b5d7d6513db..4a0d29016ce 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -21,6 +21,10 @@ import WireAPI import WireSystem protocol SyncManagerProtocol { + + /// Pulls and stores all required objects for the database to be initially up-to-date. + + func performSlowSync() async throws /// Fetch events from the server and process all pending events. @@ -33,19 +37,68 @@ protocol SyncManagerProtocol { } final class SyncManager: SyncManagerProtocol { + + // MARK: - Properties private(set) var syncState: SyncState = .suspended private var isSuspending = false + + // MARK: - Repositories private let updateEventsRepository: any UpdateEventsRepositoryProtocol + private let teamRepository: any TeamRepositoryProtocol + private let connectionsRepository: any ConnectionsRepositoryProtocol + private let conversationsRepository: any ConversationRepositoryProtocol + private let userRepository: any UserRepositoryProtocol + private let conversationLabelsRepository: any ConversationLabelsRepositoryProtocol + private let featureConfigsRepository: any FeatureConfigRepositoryProtocol + private let pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol + private let oneOnOneResolverUseCase: any OneOnOneResolverUseCaseProtocol + + // MARK: - Update event processor + private let updateEventProcessor: any UpdateEventProcessorProtocol + + // MARK: - Object lifecycle init( updateEventsRepository: any UpdateEventsRepositoryProtocol, - updateEventProcessor: any UpdateEventProcessorProtocol + teamRepository: any TeamRepositoryProtocol, + connectionsRepository: any ConnectionsRepositoryProtocol, + conversationsRepository: any ConversationRepositoryProtocol, + userRepository: any UserRepositoryProtocol, + conversationLabelsRepository: any ConversationLabelsRepositoryProtocol, + featureConfigsRepository: any FeatureConfigRepositoryProtocol, + updateEventProcessor: any UpdateEventProcessorProtocol, + pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol, + oneOnOneResolverUseCase: any OneOnOneResolverUseCaseProtocol ) { self.updateEventsRepository = updateEventsRepository + self.teamRepository = teamRepository + self.connectionsRepository = connectionsRepository + self.conversationsRepository = conversationsRepository + self.userRepository = userRepository + self.conversationLabelsRepository = conversationLabelsRepository + self.featureConfigsRepository = featureConfigsRepository self.updateEventProcessor = updateEventProcessor + self.pushSupportedProtocolsUseCase = pushSupportedProtocolsUseCase + self.oneOnOneResolverUseCase = oneOnOneResolverUseCase + } + + func performSlowSync() async throws { + try await updateEventsRepository.pullLastEventID() + try await teamRepository.pullSelfTeam() + try await teamRepository.pullSelfTeamRoles() + try await teamRepository.pullSelfTeamMembers() + try await connectionsRepository.pullConnections() + try await conversationsRepository.pullConversations() + try await userRepository.pullKnownUsers() + try await userRepository.pullSelfUser() + try await teamRepository.pullSelfLegalHoldStatus() + try await conversationLabelsRepository.pullConversationLabels() + try await featureConfigsRepository.pullFeatureConfigs() + try await pushSupportedProtocolsUseCase.invoke() + try await oneOnOneResolverUseCase.invoke() } func performQuickSync() async throws { diff --git a/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift new file mode 100644 index 00000000000..547776f3aa5 --- /dev/null +++ b/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift @@ -0,0 +1,326 @@ +// +// Wire +// Copyright (C) 2024 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 CoreData +import WireDataModel +import WireAPI + +// sourcery: AutoMockable +/// Resolves 1:1 conversations +public protocol OneOnOneResolverUseCaseProtocol { + func invoke() async throws +} + +public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { + + private enum OneOnOneResolverUseCaseError: Error { + case failedToActivateConversation + case failedToFetchConversation + case failedToEstablishGroup(Error) + } + + // MARK: - Properties + + private let context: NSManagedObjectContext + private let userRepository: any UserRepositoryProtocol + private let conversationsRepository: any ConversationRepositoryProtocol + private let mlsService: any MLSServiceInterface + + // MARK: - Object lifecycle + + public init( + context: NSManagedObjectContext, + userRepository: any UserRepositoryProtocol, + conversationsRepository: any ConversationRepositoryProtocol, + mlsService: any MLSServiceInterface + ) { + self.context = context + self.userRepository = userRepository + self.conversationsRepository = conversationsRepository + self.mlsService = mlsService + } + + // MARK: - Public + + public func invoke() async throws { + try await resolveAllOneOnOneConversations() + } + + // MARK: - Private + + private func resolveAllOneOnOneConversations() async throws { + let usersIDs = try await fetchUserIdsWithOneOnOneConversation() + + await withTaskGroup(of: Void.self) { group in + for userID in usersIDs { + group.addTask { + do { + try await self.resolveOneOnOneConversation(with: userID) + } catch { + /// skip conversation migration for this user + WireLogger.conversation.error( + "resolve 1-1 conversation with userID \(userID) failed!" + ) + } + } + } + } + } + + private func fetchUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + try await context.perform { + let request = NSFetchRequest(entityName: ZMUser.entityName()) + let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation)) + request.predicate = predicate + + return try context + .fetch(request) + .compactMap { user in + guard let userID = user.qualifiedID else { + WireLogger.conversation.error( + "missing user's qualifiedID to resolve 1-1 conversation!" + ) + return nil + } + return userID + } + } + } + + public func resolveOneOnOneConversation( + with userID: WireDataModel.QualifiedID + ) async throws { + + let user = try await userRepository.fetchUser( + with: userID.uuid, domain: userID.domain + ) + + let selfUser = userRepository.fetchSelfUser() + let commonProtocol = getCommonProtocol(between: selfUser, and: user) + let mlsEnabled = DeveloperFlag.enableMLSSupport.isOn + + if mlsEnabled, commonProtocol == .mls { + try await resolveMLSConversation( + for: user + ) + } + + if mlsEnabled, commonProtocol == nil { + await resolveNoCommonProtocolConversation( + between: selfUser, + and: user + ) + } + + if commonProtocol == .proteus { + await resolveProteusConversation( + for: user + ) + } + } + + private func resolveMLSConversation(for user: ZMUser) async throws { + WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") + + guard let userID = user.qualifiedID else { + throw OneOnOneResolverUseCaseError.failedToActivateConversation + } + + /// Sync the user MLS conversation from backend. + let mlsGroupID = try await conversationsRepository.pullMLSOneToOneConversation( + userID: userID.uuid.uuidString, + domain: userID.domain + ) + + /// Then, fetch the synced MLS conversation. + let mlsConversation = await conversationsRepository.fetchMLSConversation(with: mlsGroupID) + + guard let mlsConversation, let groupID = mlsConversation.mlsGroupID else { + throw OneOnOneResolverUseCaseError.failedToFetchConversation + } + + let needsMLSMigration = try await mlsService.conversationExists( + groupID: groupID + ) == false + + /// If conversation already exists, there is no need to perform a migration. + if needsMLSMigration { + await migrateToMLS( + mlsConversation: mlsConversation, + mlsGroupID: groupID, + user: user, + userID: userID + ) + } + } + + private func migrateToMLS( + mlsConversation: ZMConversation, + mlsGroupID: MLSGroupID, + user: ZMUser, + userID: WireDataModel.QualifiedID + ) async { + do { + try await setupMLSGroup( + mlsConversation: mlsConversation, + groupID: mlsGroupID, + userID: userID + ) + } catch { + await context.perform { + let userOneOnOneConversation = user.oneOnOneConversation + userOneOnOneConversation?.isForcedReadOnly = true + } + + return WireLogger.conversation.error( + "Failed to setup MLS group with ID: \(mlsGroupID.safeForLoggingDescription)" + ) + } + + await switchLocalConversationToMLS( + mlsConversation: mlsConversation, + for: user + ) + } + + /// Establish a new MLS group (when epoch is 0) or join an existing group. + /// - parameters: + /// - mlsConversation: The 1:1 MLS conversation. + /// - groupID: The MLS group ID. + /// - userID: The user ID that will be part of the MLS group. + + private func setupMLSGroup( + mlsConversation: ZMConversation, + groupID: MLSGroupID, + userID: WireDataModel.QualifiedID + ) async throws { + if mlsConversation.epoch == 0 { + let users = [MLSUser(userID)] + + do { + let ciphersuite = try await mlsService.establishGroup( + for: groupID, + with: users, + removalKeys: nil + ) + + await context.perform { + mlsConversation.ciphersuite = ciphersuite + mlsConversation.mlsStatus = .ready + } + + } catch { + throw OneOnOneResolverUseCaseError.failedToEstablishGroup(error) + } + } else { + try await mlsService.joinGroup(with: groupID) + } + } + + /// Migrates Proteus messages to the MLS conversation and sets the MLS conversation for the user. + /// - Parameter mlsConversation: The MLS conversation. + /// - Parameter user: The user to set the MLS conversation for. + + private func switchLocalConversationToMLS( + mlsConversation: ZMConversation, + for user: ZMUser + ) async { + await context.perform { + /// Move local messages from proteus conversation if it exists + if let proteusConversation = user.oneOnOneConversation { + /// Since ZMMessages only have a single conversation connected, + /// forming this union also removes the relationship to the proteus conversation. + mlsConversation.mutableMessages.union(proteusConversation.allMessages) + mlsConversation.isForcedReadOnly = false + mlsConversation.needsToBeUpdatedFromBackend = true + } + + /// Switch active conversation + user.oneOnOneConversation = mlsConversation + } + } + + /// Resolves a Proteus 1:1 conversation. + /// - Parameter user: The user to resolve the conversation for. + + private func resolveProteusConversation( + for user: ZMUser + ) async { + WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") + + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } + + await context.perform { + conversation.isForcedReadOnly = false + } + } + + /// Resolves a 1:1 conversation with no common protocols between self user and user. + /// - Parameter selfUser: The self user. + /// - Parameter user: The other user. + /// + /// When no common protocols are found, the 1:1 conversation is marked as read only and a system + /// message is append to the conversation to inform the self user or the user. + + private func resolveNoCommonProtocolConversation( + between selfUser: ZMUser, + and user: ZMUser + ) async { + WireLogger.conversation.debug("No common protocols found") + + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } + + if !conversation.isForcedReadOnly { + await context.perform { + if !selfUser.supportedProtocols.contains(.mls) { + conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser) + } else if !user.supportedProtocols.contains(.mls) { + conversation.appendMLSMigrationMLSNotSupportedForOtherUser(user: user) + } + + conversation.isForcedReadOnly = true + } + } + } + + private func getCommonProtocol( + between selfUser: ZMUser, + and otherUser: ZMUser + ) -> ConversationMessageProtocol? { + let selfUserProtocols = selfUser.supportedProtocols + let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols /// default to Proteus if empty. + + let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) + + if commonProtocols.contains(.mls) { + return .mls + } else if commonProtocols.contains(.proteus) { + return .proteus + } else { + return nil + } + } +} diff --git a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift index 70f07bd737f..319160cfa4b 100644 --- a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift +++ b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift @@ -20,8 +20,13 @@ import WireAPI import WireDataModel import WireSystem +// sourcery: AutoMockable /// Calculates and pushes the supported protocols to the backend -public struct PushSupportedProtocolsUseCase { +public protocol PushSupportedProtocolsUseCaseProtocol { + func invoke() async throws +} + +public struct PushSupportedProtocolsUseCase: PushSupportedProtocolsUseCaseProtocol { private enum ProteusToMLSMigrationState: String { case disabled diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index d06c2fa5cbe..545a27962b1 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -55,6 +55,63 @@ import WireDataModel + +public class MockConnectionsRepositoryProtocol: ConnectionsRepositoryProtocol { + + // MARK: - Life cycle + + public init() {} + + + // MARK: - pullConnections + + public var pullConnections_Invocations: [Void] = [] + public var pullConnections_MockError: Error? + public var pullConnections_MockMethod: (() async throws -> Void)? + + public func pullConnections() async throws { + pullConnections_Invocations.append(()) + + if let error = pullConnections_MockError { + throw error + } + + guard let mock = pullConnections_MockMethod else { + fatalError("no mock for `pullConnections`") + } + + try await mock() + } + +} + +class MockConversationLabelsRepositoryProtocol: ConversationLabelsRepositoryProtocol { + + // MARK: - Life cycle + + + + // MARK: - pullConversationLabels + + var pullConversationLabels_Invocations: [Void] = [] + var pullConversationLabels_MockError: Error? + var pullConversationLabels_MockMethod: (() async throws -> Void)? + + func pullConversationLabels() async throws { + pullConversationLabels_Invocations.append(()) + + if let error = pullConversationLabels_MockError { + throw error + } + + guard let mock = pullConversationLabels_MockMethod else { + fatalError("no mock for `pullConversationLabels`") + } + + try await mock() + } + +} public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol { @@ -123,6 +180,24 @@ public class MockConversationLocalStoreProtocol: ConversationLocalStoreProtocol await mock(user, removalDate) } + // MARK: - fetchMLSConversation + + public var fetchMLSConversationWith_Invocations: [WireDataModel.MLSGroupID] = [] + public var fetchMLSConversationWith_MockMethod: ((WireDataModel.MLSGroupID) async -> ZMConversation?)? + public var fetchMLSConversationWith_MockValue: ZMConversation?? + + public func fetchMLSConversation(with groupID: WireDataModel.MLSGroupID) async -> ZMConversation? { + fetchMLSConversationWith_Invocations.append(groupID) + + if let mock = fetchMLSConversationWith_MockMethod { + return await mock(groupID) + } else if let mock = fetchMLSConversationWith_MockValue { + return mock + } else { + fatalError("no mock for `fetchMLSConversationWith`") + } + } + } public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol { @@ -167,6 +242,76 @@ public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol await mock(user, removalDate) } + // MARK: - pullMLSOneToOneConversation + + public var pullMLSOneToOneConversationUserIDDomain_Invocations: [(userID: String, domain: String)] = [] + public var pullMLSOneToOneConversationUserIDDomain_MockError: Error? + public var pullMLSOneToOneConversationUserIDDomain_MockMethod: ((String, String) async throws -> String)? + public var pullMLSOneToOneConversationUserIDDomain_MockValue: String? + + public func pullMLSOneToOneConversation(userID: String, domain: String) async throws -> String { + pullMLSOneToOneConversationUserIDDomain_Invocations.append((userID: userID, domain: domain)) + + if let error = pullMLSOneToOneConversationUserIDDomain_MockError { + throw error + } + + if let mock = pullMLSOneToOneConversationUserIDDomain_MockMethod { + return try await mock(userID, domain) + } else if let mock = pullMLSOneToOneConversationUserIDDomain_MockValue { + return mock + } else { + fatalError("no mock for `pullMLSOneToOneConversationUserIDDomain`") + } + } + + // MARK: - fetchMLSConversation + + public var fetchMLSConversationWith_Invocations: [String] = [] + public var fetchMLSConversationWith_MockMethod: ((String) async -> ZMConversation?)? + public var fetchMLSConversationWith_MockValue: ZMConversation?? + + public func fetchMLSConversation(with groupID: String) async -> ZMConversation? { + fetchMLSConversationWith_Invocations.append(groupID) + + if let mock = fetchMLSConversationWith_MockMethod { + return await mock(groupID) + } else if let mock = fetchMLSConversationWith_MockValue { + return mock + } else { + fatalError("no mock for `fetchMLSConversationWith`") + } + } + +} + +public class MockOneOnOneResolverUseCaseProtocol: OneOnOneResolverUseCaseProtocol { + + // MARK: - Life cycle + + public init() {} + + + // MARK: - invoke + + public var invoke_Invocations: [Void] = [] + public var invoke_MockError: Error? + public var invoke_MockMethod: (() async throws -> Void)? + + public func invoke() async throws { + invoke_Invocations.append(()) + + if let error = invoke_MockError { + throw error + } + + guard let mock = invoke_MockMethod else { + fatalError("no mock for `invoke`") + } + + try await mock() + } + } class MockProteusMessageDecryptorProtocol: ProteusMessageDecryptorProtocol { @@ -200,6 +345,35 @@ class MockProteusMessageDecryptorProtocol: ProteusMessageDecryptorProtocol { } +public class MockPushSupportedProtocolsUseCaseProtocol: PushSupportedProtocolsUseCaseProtocol { + + // MARK: - Life cycle + + public init() {} + + + // MARK: - invoke + + public var invoke_Invocations: [Void] = [] + public var invoke_MockError: Error? + public var invoke_MockMethod: (() async throws -> Void)? + + public func invoke() async throws { + invoke_Invocations.append(()) + + if let error = invoke_MockError { + throw error + } + + guard let mock = invoke_MockMethod else { + fatalError("no mock for `invoke`") + } + + try await mock() + } + +} + public class MockSelfUserProviderProtocol: SelfUserProviderProtocol { // MARK: - Life cycle @@ -227,6 +401,158 @@ public class MockSelfUserProviderProtocol: SelfUserProviderProtocol { } +public class MockTeamRepositoryProtocol: TeamRepositoryProtocol { + + // MARK: - Life cycle + + public init() {} + + + // MARK: - pullSelfTeam + + public var pullSelfTeam_Invocations: [Void] = [] + public var pullSelfTeam_MockError: Error? + public var pullSelfTeam_MockMethod: (() async throws -> Void)? + + public func pullSelfTeam() async throws { + pullSelfTeam_Invocations.append(()) + + if let error = pullSelfTeam_MockError { + throw error + } + + guard let mock = pullSelfTeam_MockMethod else { + fatalError("no mock for `pullSelfTeam`") + } + + try await mock() + } + + // MARK: - pullSelfTeamRoles + + public var pullSelfTeamRoles_Invocations: [Void] = [] + public var pullSelfTeamRoles_MockError: Error? + public var pullSelfTeamRoles_MockMethod: (() async throws -> Void)? + + public func pullSelfTeamRoles() async throws { + pullSelfTeamRoles_Invocations.append(()) + + if let error = pullSelfTeamRoles_MockError { + throw error + } + + guard let mock = pullSelfTeamRoles_MockMethod else { + fatalError("no mock for `pullSelfTeamRoles`") + } + + try await mock() + } + + // MARK: - pullSelfTeamMembers + + public var pullSelfTeamMembers_Invocations: [Void] = [] + public var pullSelfTeamMembers_MockError: Error? + public var pullSelfTeamMembers_MockMethod: (() async throws -> Void)? + + public func pullSelfTeamMembers() async throws { + pullSelfTeamMembers_Invocations.append(()) + + if let error = pullSelfTeamMembers_MockError { + throw error + } + + guard let mock = pullSelfTeamMembers_MockMethod else { + fatalError("no mock for `pullSelfTeamMembers`") + } + + try await mock() + } + + // MARK: - fetchSelfLegalhold + + public var fetchSelfLegalhold_Invocations: [Void] = [] + public var fetchSelfLegalhold_MockError: Error? + public var fetchSelfLegalhold_MockMethod: (() async throws -> TeamMemberLegalHold)? + public var fetchSelfLegalhold_MockValue: TeamMemberLegalHold? + + public func fetchSelfLegalhold() async throws -> TeamMemberLegalHold { + fetchSelfLegalhold_Invocations.append(()) + + if let error = fetchSelfLegalhold_MockError { + throw error + } + + if let mock = fetchSelfLegalhold_MockMethod { + return try await mock() + } else if let mock = fetchSelfLegalhold_MockValue { + return mock + } else { + fatalError("no mock for `fetchSelfLegalhold`") + } + } + + // MARK: - deleteMembership + + public var deleteMembershipForUserFromTeamAt_Invocations: [(userID: UUID, teamID: UUID, time: Date)] = [] + public var deleteMembershipForUserFromTeamAt_MockError: Error? + public var deleteMembershipForUserFromTeamAt_MockMethod: ((UUID, UUID, Date) async throws -> Void)? + + public func deleteMembership(forUser userID: UUID, fromTeam teamID: UUID, at time: Date) async throws { + deleteMembershipForUserFromTeamAt_Invocations.append((userID: userID, teamID: teamID, time: time)) + + if let error = deleteMembershipForUserFromTeamAt_MockError { + throw error + } + + guard let mock = deleteMembershipForUserFromTeamAt_MockMethod else { + fatalError("no mock for `deleteMembershipForUserFromTeamAt`") + } + + try await mock(userID, teamID, time) + } + + // MARK: - storeTeamMemberNeedsBackendUpdate + + public var storeTeamMemberNeedsBackendUpdateMembershipID_Invocations: [UUID] = [] + public var storeTeamMemberNeedsBackendUpdateMembershipID_MockError: Error? + public var storeTeamMemberNeedsBackendUpdateMembershipID_MockMethod: ((UUID) async throws -> Void)? + + public func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws { + storeTeamMemberNeedsBackendUpdateMembershipID_Invocations.append(membershipID) + + if let error = storeTeamMemberNeedsBackendUpdateMembershipID_MockError { + throw error + } + + guard let mock = storeTeamMemberNeedsBackendUpdateMembershipID_MockMethod else { + fatalError("no mock for `storeTeamMemberNeedsBackendUpdateMembershipID`") + } + + try await mock(membershipID) + } + + // MARK: - pullSelfLegalHoldStatus + + public var pullSelfLegalHoldStatus_Invocations: [Void] = [] + public var pullSelfLegalHoldStatus_MockError: Error? + public var pullSelfLegalHoldStatus_MockMethod: (() async throws -> Void)? + + public func pullSelfLegalHoldStatus() async throws { + pullSelfLegalHoldStatus_Invocations.append(()) + + if let error = pullSelfLegalHoldStatus_MockError { + throw error + } + + guard let mock = pullSelfLegalHoldStatus_MockMethod else { + fatalError("no mock for `pullSelfLegalHoldStatus`") + } + + try await mock() + } + +} + class MockUpdateEventDecryptorProtocol: UpdateEventDecryptorProtocol { // MARK: - Life cycle @@ -437,6 +763,26 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol { public init() {} + // MARK: - pullSelfUser + + public var pullSelfUser_Invocations: [Void] = [] + public var pullSelfUser_MockError: Error? + public var pullSelfUser_MockMethod: (() async throws -> Void)? + + public func pullSelfUser() async throws { + pullSelfUser_Invocations.append(()) + + if let error = pullSelfUser_MockError { + throw error + } + + guard let mock = pullSelfUser_MockMethod else { + fatalError("no mock for `pullSelfUser`") + } + + try await mock() + } + // MARK: - fetchSelfUser public var fetchSelfUser_Invocations: [Void] = [] @@ -517,24 +863,24 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol { // MARK: - fetchUser - public var fetchUserWith_Invocations: [UUID] = [] - public var fetchUserWith_MockError: Error? - public var fetchUserWith_MockMethod: ((UUID) async throws -> ZMUser)? - public var fetchUserWith_MockValue: ZMUser? + public var fetchUserWithDomain_Invocations: [(id: UUID, domain: String?)] = [] + public var fetchUserWithDomain_MockError: Error? + public var fetchUserWithDomain_MockMethod: ((UUID, String?) async throws -> ZMUser)? + public var fetchUserWithDomain_MockValue: ZMUser? - public func fetchUser(with id: UUID) async throws -> ZMUser { - fetchUserWith_Invocations.append(id) + public func fetchUser(with id: UUID, domain: String?) async throws -> ZMUser { + fetchUserWithDomain_Invocations.append((id: id, domain: domain)) - if let error = fetchUserWith_MockError { + if let error = fetchUserWithDomain_MockError { throw error } - if let mock = fetchUserWith_MockMethod { - return try await mock(id) - } else if let mock = fetchUserWith_MockValue { + if let mock = fetchUserWithDomain_MockMethod { + return try await mock(id, domain) + } else if let mock = fetchUserWithDomain_MockValue { return mock } else { - fatalError("no mock for `fetchUserWith`") + fatalError("no mock for `fetchUserWithDomain`") } } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift new file mode 100644 index 00000000000..f89133c6483 --- /dev/null +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift @@ -0,0 +1,135 @@ +// +// Wire +// Copyright (C) 2024 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 Combine +import WireAPI +import WireDataModel + +@testable import WireDomain + +class MockFeatureConfigRepositoryProtocol: FeatureConfigRepositoryProtocol { + // MARK: - Life cycle + + // MARK: - pullFeatureConfigs + + var pullFeatureConfigs_Invocations: [Void] = [] + var pullFeatureConfigs_MockError: Error? + var pullFeatureConfigs_MockMethod: (() async throws -> Void)? + + func pullFeatureConfigs() async throws { + pullFeatureConfigs_Invocations.append(()) + + if let error = pullFeatureConfigs_MockError { + throw error + } + + guard let mock = pullFeatureConfigs_MockMethod else { + fatalError("no mock for `pullFeatureConfigs`") + } + + try await mock() + } + + // MARK: - observeFeatureStates + + var observeFeatureStates_Invocations: [Void] = [] + var observeFeatureStates_MockMethod: (() -> AnyPublisher)? + var observeFeatureStates_MockValue: AnyPublisher? + + func observeFeatureStates() -> AnyPublisher { + observeFeatureStates_Invocations.append(()) + + if let mock = observeFeatureStates_MockMethod { + return mock() + } else if let mock = observeFeatureStates_MockValue { + return mock + } else { + fatalError("no mock for `observeFeatureStates`") + } + } + + // MARK: - fetchFeatureConfig + + func fetchFeatureConfig(with name: Feature.Name, type: T.Type) async throws -> LocalFeature { + fatalError("to implement using generics") + } + + // MARK: - updateFeatureConfig + + var updateFeatureConfig_Invocations: [FeatureConfig] = [] + var updateFeatureConfig_MockError: Error? + var updateFeatureConfig_MockMethod: ((FeatureConfig) async throws -> Void)? + + func updateFeatureConfig(_ featureConfig: FeatureConfig) async throws { + updateFeatureConfig_Invocations.append(featureConfig) + + if let error = updateFeatureConfig_MockError { + throw error + } + + guard let mock = updateFeatureConfig_MockMethod else { + fatalError("no mock for `updateFeatureConfig`") + } + + try await mock(featureConfig) + } + + // MARK: - fetchNeedsToNotifyUser + + var fetchNeedsToNotifyUserFor_Invocations: [Feature.Name] = [] + var fetchNeedsToNotifyUserFor_MockError: Error? + var fetchNeedsToNotifyUserFor_MockMethod: ((Feature.Name) async throws -> Bool)? + var fetchNeedsToNotifyUserFor_MockValue: Bool? + + func fetchNeedsToNotifyUser(for name: Feature.Name) async throws -> Bool { + fetchNeedsToNotifyUserFor_Invocations.append(name) + + if let error = fetchNeedsToNotifyUserFor_MockError { + throw error + } + + if let mock = fetchNeedsToNotifyUserFor_MockMethod { + return try await mock(name) + } else if let mock = fetchNeedsToNotifyUserFor_MockValue { + return mock + } else { + fatalError("no mock for `fetchNeedsToNotifyUserFor`") + } + } + + // MARK: - storeNeedsToNotifyUser + + var storeNeedsToNotifyUserForFeatureName_Invocations: [(notifyUser: Bool, name: Feature.Name)] = [] + var storeNeedsToNotifyUserForFeatureName_MockError: Error? + var storeNeedsToNotifyUserForFeatureName_MockMethod: ((Bool, Feature.Name) async throws -> Void)? + + func storeNeedsToNotifyUser(_ notifyUser: Bool, forFeatureName name: Feature.Name) async throws { + storeNeedsToNotifyUserForFeatureName_Invocations.append((notifyUser: notifyUser, name: name)) + + if let error = storeNeedsToNotifyUserForFeatureName_MockError { + throw error + } + + guard let mock = storeNeedsToNotifyUserForFeatureName_MockMethod else { + fatalError("no mock for `storeNeedsToNotifyUserForFeatureName`") + } + + try await mock(notifyUser, name) + } + +} diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index 790d9048d83..f69d739b2c9 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -25,18 +25,43 @@ import XCTest @testable import WireDomainSupport final class SyncManagerTests: XCTestCase { - + private var sut: SyncManager! private var updateEventsRepository: MockUpdateEventsRepositoryProtocol! private var updateEventProcessor: MockUpdateEventProcessorProtocol! + private var teamRepository: MockTeamRepositoryProtocol! + private var connectionsRepository: MockConnectionsRepositoryProtocol! + private var conversationsRepository: MockConversationRepositoryProtocol! + private var userRepository: MockUserRepositoryProtocol! + private var conversationLabelsRepository: MockConversationLabelsRepositoryProtocol! + private var featureConfigsRepository: MockFeatureConfigRepositoryProtocol! + private var pushSupportedProtocolsUseCase: MockPushSupportedProtocolsUseCaseProtocol! + private var oneOnOneResolverUseCase: MockOneOnOneResolverUseCaseProtocol! override func setUp() async throws { try await super.setUp() updateEventsRepository = MockUpdateEventsRepositoryProtocol() updateEventProcessor = MockUpdateEventProcessorProtocol() + teamRepository = MockTeamRepositoryProtocol() + connectionsRepository = MockConnectionsRepositoryProtocol() + conversationsRepository = MockConversationRepositoryProtocol() + userRepository = MockUserRepositoryProtocol() + conversationLabelsRepository = MockConversationLabelsRepositoryProtocol() + featureConfigsRepository = MockFeatureConfigRepositoryProtocol() + pushSupportedProtocolsUseCase = MockPushSupportedProtocolsUseCaseProtocol() + oneOnOneResolverUseCase = MockOneOnOneResolverUseCaseProtocol() + sut = SyncManager( updateEventsRepository: updateEventsRepository, - updateEventProcessor: updateEventProcessor + teamRepository: teamRepository, + connectionsRepository: connectionsRepository, + conversationsRepository: conversationsRepository, + userRepository: userRepository, + conversationLabelsRepository: conversationLabelsRepository, + featureConfigsRepository: featureConfigsRepository, + updateEventProcessor: updateEventProcessor, + pushSupportedProtocolsUseCase: pushSupportedProtocolsUseCase, + oneOnOneResolverUseCase: oneOnOneResolverUseCase ) // Base mocks. @@ -50,10 +75,18 @@ final class SyncManagerTests: XCTestCase { } override func tearDown() async throws { + try await super.tearDown() sut = nil updateEventsRepository = nil updateEventProcessor = nil - try await super.tearDown() + teamRepository = nil + connectionsRepository = nil + conversationsRepository = nil + userRepository = nil + conversationLabelsRepository = nil + featureConfigsRepository = nil + pushSupportedProtocolsUseCase = nil + oneOnOneResolverUseCase = nil } // MARK: - Tests @@ -282,6 +315,45 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(updateEventsRepository.stopReceivingLiveEvents_Invocations.count, 0) } + + func testPerformSlowSync() async throws { + + // Mock + + updateEventsRepository.pullLastEventID_MockMethod = { } + teamRepository.pullSelfTeam_MockMethod = { } + teamRepository.pullSelfTeamRoles_MockMethod = { } + teamRepository.pullSelfTeamMembers_MockMethod = { } + connectionsRepository.pullConnections_MockMethod = { } + conversationsRepository.pullConversations_MockMethod = { } + userRepository.pullKnownUsers_MockMethod = { } + conversationLabelsRepository.pullConversationLabels_MockMethod = { } + featureConfigsRepository.pullFeatureConfigs_MockMethod = { } + userRepository.pullSelfUser_MockMethod = { } + teamRepository.pullSelfLegalHoldStatus_MockMethod = { } + pushSupportedProtocolsUseCase.invoke_MockMethod = { } + oneOnOneResolverUseCase.invoke_MockMethod = { } + + // When + + try await sut.performSlowSync() + + // Then + + XCTAssertEqual(updateEventsRepository.pullLastEventID_Invocations.count, 1) + XCTAssertEqual(teamRepository.pullSelfTeam_Invocations.count, 1) + XCTAssertEqual(teamRepository.pullSelfTeamRoles_Invocations.count, 1) + XCTAssertEqual(teamRepository.pullSelfTeamMembers_Invocations.count, 1) + XCTAssertEqual(connectionsRepository.pullConnections_Invocations.count, 1) + XCTAssertEqual(conversationsRepository.pullConversations_Invocations.count, 1) + XCTAssertEqual(userRepository.pullKnownUsers_Invocations.count, 1) + XCTAssertEqual(conversationLabelsRepository.pullConversationLabels_Invocations.count, 1) + XCTAssertEqual(featureConfigsRepository.pullFeatureConfigs_Invocations.count, 1) + XCTAssertEqual(userRepository.pullSelfUser_Invocations.count, 1) + XCTAssertEqual(teamRepository.pullSelfLegalHoldStatus_Invocations.count, 1) + XCTAssertEqual(pushSupportedProtocolsUseCase.invoke_Invocations.count, 1) + XCTAssertEqual(oneOnOneResolverUseCase.invoke_Invocations.count, 1) + } } diff --git a/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift b/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift new file mode 100644 index 00000000000..876a3503277 --- /dev/null +++ b/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift @@ -0,0 +1,355 @@ +// +// Wire +// Copyright (C) 2024 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 WireAPI +import WireAPISupport +import WireDataModel +import WireDataModelSupport +import WireDomainSupport +import XCTest + +@testable import WireDomain + +final class OneOnOneResolverUseCaseTests: XCTestCase { + var sut: OneOnOneResolverUseCase! + + var coreDataStack: CoreDataStack! + var coreDataStackHelper: CoreDataStackHelper! + var modelHelper: ModelHelper! + var userRepository: MockUserRepositoryProtocol! + var conversationsRepository: MockConversationRepositoryProtocol! + var mlsService: MockMLSServiceInterface! + + var context: NSManagedObjectContext { + coreDataStack.syncContext + } + + override func setUp() async throws { + try await super.setUp() + coreDataStackHelper = CoreDataStackHelper() + modelHelper = ModelHelper() + coreDataStack = try await coreDataStackHelper.createStack() + userRepository = MockUserRepositoryProtocol() + conversationsRepository = MockConversationRepositoryProtocol() + mlsService = MockMLSServiceInterface() + sut = OneOnOneResolverUseCase( + context: context, + userRepository: userRepository, + conversationsRepository: conversationsRepository, + mlsService: mlsService + ) + + DeveloperFlag.storage = UserDefaults(suiteName: Scaffolding.defaultsSuiteName)! + var flag = DeveloperFlag.enableMLSSupport + flag.isOn = true + } + + override func tearDown() async throws { + try await super.tearDown() + coreDataStack = nil + sut = nil + modelHelper = nil + try coreDataStackHelper.cleanupDirectory() + DeveloperFlag.storage.removePersistentDomain(forName: Scaffolding.defaultsSuiteName) + coreDataStackHelper = nil + userRepository = nil + conversationsRepository = nil + mlsService = nil + } + + func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Zero() async throws { + // Given + + let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls + let mlsEpoch: UInt64 = 0 + + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol, + mlsEpoch: mlsEpoch + ) + + // Mock + + setupMock( + selfUser: selfUser, + user: user, + mlsOneOnOneConversation: mlsOneOnOneConversation + ) + + // When + + try await sut.invoke() + + // Then + + XCTAssertEqual(mlsService.establishGroupForWithRemovalKeys_Invocations.count, 1) + let createGroupInvocation = try XCTUnwrap( + mlsService.establishGroupForWithRemovalKeys_Invocations.first + ) + + XCTAssertEqual(createGroupInvocation.groupID, Scaffolding.mlsGroupID) + XCTAssertEqual( + createGroupInvocation.users, + [MLSUser(Scaffolding.receiverQualifiedID.toDomainModel())] + ) + XCTAssertEqual(mlsOneOnOneConversation.ciphersuite, Scaffolding.ciphersuite) + XCTAssertEqual(mlsOneOnOneConversation.mlsStatus, .ready) + XCTAssertEqual(mlsOneOnOneConversation.isForcedReadOnly, false) + XCTAssertEqual(mlsOneOnOneConversation.needsToBeUpdatedFromBackend, true) + XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) + XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + } + + func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Not_Zero() async throws { + // Given + + let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls + let mlsEpoch: UInt64 = 1 + + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol, + mlsEpoch: mlsEpoch + ) + + // Mock + + setupMock( + selfUser: selfUser, + user: user, + mlsOneOnOneConversation: mlsOneOnOneConversation + ) + + // When + + try await sut.invoke() + + // Then + + XCTAssertEqual(mlsService.joinGroupWith_Invocations.count, 1) + let invokedMLSGroupID = try XCTUnwrap(mlsService.joinGroupWith_Invocations.first) + XCTAssertEqual(invokedMLSGroupID, Scaffolding.mlsGroupID) + XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) + XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + } + + func testProcessEvent_It_Migrates_Proteus_Messages_To_MLS_Conversation() async throws { + // Given + + let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls + + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol + ) + + // Mock + + setupMock( + selfUser: selfUser, + user: user, + mlsOneOnOneConversation: mlsOneOnOneConversation + ) + + // When + + try await sut.invoke() + + // Then + + let migratedMessagesTexts = mlsOneOnOneConversation.allMessages + .compactMap(\.textMessageData) + .compactMap(\.messageText) + .sorted() + + /// Ensuring proteus messages were migrated to MLS conversation. + XCTAssertEqual(migratedMessagesTexts.first, "Hello") + XCTAssertEqual(migratedMessagesTexts.last, "World!") + } + + func testProcessEvent_It_Resolves_Proteus_Conversation() async throws { + // Given + + let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.proteus + + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol + ) + + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + + // Mock + + setupMock( + selfUser: selfUser, + user: user, + mlsOneOnOneConversation: mlsOneOnOneConversation + ) + + // When + + try await sut.invoke() + + // Then + + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + } + + func testProcessEvent_It_Resolves_Conversation_With_No_Common_Protocol() async throws { + // Given + + let connectionStatus = ConnectionStatus.accepted + let forcedReadOnly = false + + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( + selfUserProtocol: .mls, + userProtocol: .proteus, + forcedReadOnly: forcedReadOnly + ) + + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + + // Mock + + setupMock( + selfUser: selfUser, + user: user, + mlsOneOnOneConversation: mlsOneOnOneConversation + ) + + // When + + try await sut.invoke() + + // Then + + let lastMessage = try XCTUnwrap(user.oneOnOneConversation?.lastMessage as? ZMSystemMessage) + XCTAssertEqual(lastMessage.systemMessageType, .mlsNotSupportedOtherUser) + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + } + + // MARK: - Setup + + private func setupManagedObjects( + selfUserProtocol: WireDataModel.MessageProtocol, + userProtocol: WireDataModel.MessageProtocol, + forcedReadOnly: Bool = true, + mlsEpoch: UInt64 = 0 + ) throws -> (selfUser: ZMUser, + user: ZMUser, + mlsConversation: ZMConversation) { + let user = modelHelper.createUser( + id: Scaffolding.receiverQualifiedID.uuid, + domain: Scaffolding.receiverQualifiedID.domain, + in: context + ) + + user.supportedProtocols = [userProtocol] + + let selfUser = modelHelper.createSelfUser( + id: UUID(), + domain: nil, + in: context + ) + + selfUser.supportedProtocols = [selfUserProtocol] + + let proteusConversation = modelHelper.createOneOnOne( + with: selfUser, + in: context + ) + + proteusConversation.isForcedReadOnly = forcedReadOnly + user.oneOnOneConversation = proteusConversation + + try proteusConversation.appendText(content: "Hello") + try proteusConversation.appendText(content: "World!") + + let mlsOneOnOneConversation = modelHelper.createMLSConversation( + mlsGroupID: Scaffolding.mlsGroupID, + mlsStatus: .pendingJoin, + conversationType: .oneOnOne, + epoch: mlsEpoch, + in: context + ) + + return (selfUser, user, mlsOneOnOneConversation) + } + + private func setupMock( + selfUser: ZMUser, + user: ZMUser, + mlsOneOnOneConversation: ZMConversation, + mlsConversationExists: Bool = false + ) { + userRepository.fetchUserWithDomain_MockValue = user + userRepository.fetchSelfUser_MockValue = selfUser + + conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = Scaffolding.conversationID.uuidString + conversationsRepository.fetchMLSConversationWith_MockValue = mlsOneOnOneConversation + + mlsService.establishGroupForWithRemovalKeys_MockValue = Scaffolding.ciphersuite + mlsService.conversationExistsGroupID_MockValue = mlsConversationExists + mlsService.joinGroupWith_MockMethod = { _ in } + } + + private func setupConnection(status: ConnectionStatus) -> Connection { + Connection( + senderID: Scaffolding.senderID, + receiverID: Scaffolding.receiverID, + receiverQualifiedID: Scaffolding.receiverQualifiedID, + conversationID: Scaffolding.conversationID, + qualifiedConversationID: Scaffolding.qualifiedConversationID, + lastUpdate: .now, + status: status + ) + } + + private enum Scaffolding { + static let username = "username" + static let senderID = UUID() + static let receiverID = UUID() + static let receiverQualifiedID = WireAPI.QualifiedID( + uuid: receiverID, + domain: "domain.com" + ) + static let conversationID = UUID() + static let qualifiedConversationID = WireAPI.QualifiedID( + uuid: conversationID, + domain: "domain.com" + ) + + static let base64EncodedString = "pQABARn//wKhAFggHsa0CszLXYLFcOzg8AA//E1+Dl1rDHQ5iuk44X0/PNYDoQChAFgg309rkhG6SglemG6kWae81P1HtQPx9lyb6wExTovhU4cE9g==" + + static let ciphersuite = WireDataModel.MLSCipherSuite.MLS_256_DHKEMP521_AES256GCM_SHA512_P521 + + static let mlsGroupID = WireDataModel.MLSGroupID( + base64Encoded: base64EncodedString + )! + + static let defaultsSuiteName = UUID().uuidString + } + +} diff --git a/wire-ios-data-model/Support/Sources/ModelHelper.swift b/wire-ios-data-model/Support/Sources/ModelHelper.swift index dd6c3ca03ca..be1fa23b6d8 100644 --- a/wire-ios-data-model/Support/Sources/ModelHelper.swift +++ b/wire-ios-data-model/Support/Sources/ModelHelper.swift @@ -304,16 +304,22 @@ public struct ModelHelper { } @discardableResult - public func createMLSConversation( - mlsGroupID: MLSGroupID? = nil, - in context: NSManagedObjectContext - ) -> ZMConversation { - let conversation = ZMConversation.insertNewObject(in: context) - conversation.mlsGroupID = mlsGroupID - conversation.messageProtocol = .mls - conversation.mlsStatus = .ready - conversation.conversationType = .group - - return conversation - } + public func createMLSConversation( + mlsGroupID: MLSGroupID? = nil, + mlsStatus: MLSGroupStatus = .ready, + conversationType: ZMConversationType = .group, + epoch: UInt64 = 0, + in context: NSManagedObjectContext + ) -> ZMConversation { + let conversation = ZMConversation.insertNewObject(in: context) + conversation.remoteIdentifier = UUID() + conversation.domain = "domain.com" + conversation.mlsGroupID = mlsGroupID + conversation.messageProtocol = .mls + conversation.mlsStatus = mlsStatus + conversation.conversationType = conversationType + conversation.epoch = epoch + + return conversation + } } From f5663e2477e6d407e3b27a7d0be6bb98706efde8 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:08:09 +0200 Subject: [PATCH 05/18] lint and format - WPB-10727 --- .../ConversationsAPI/ConversationsAPI.swift | 14 +- .../ConversationsAPIError.swift | 8 +- .../ConversationsAPI/ConversationsAPIV0.swift | 12 +- .../ConversationsAPI/ConversationsAPIV5.swift | 38 +- .../WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift | 6 +- .../WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift | 2 +- .../Models/Connection/Connection.swift | 15 +- .../Models/Team/TeamMemberLegalHold.swift | 10 +- .../ConversationsAPITests.swift | 219 ++++---- .../APIs/TeamsAPI/TeamsAPITests.swift | 2 +- .../ConversationLocalStore.swift | 40 +- .../ConversationRepository.swift | 110 ++-- .../ConversationRepositoryError.swift | 4 +- .../Repositories/Team/TeamRepository.swift | 23 +- .../Synchronization/SyncManager.swift | 14 +- .../UseCases/OneOnOneResolverUseCase.swift | 477 +++++++++--------- .../generated/AutoMockable.generated.swift | 23 + .../ConversationRepositoryTests.swift | 28 +- .../MockFeatureConfigRepositoryProtocol.swift | 60 +-- .../Repositories/TeamRepositoryTests.swift | 6 +- .../Synchronization/SyncManagerTests.swift | 49 +- .../OneOnOneResolverUseCaseTests.swift | 160 +++--- 22 files changed, 657 insertions(+), 663 deletions(-) diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift index 27613bed953..a8d50c66329 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPI.swift @@ -32,12 +32,12 @@ public protocol ConversationsAPI { func getConversations(for identifiers: [QualifiedID]) async throws -> ConversationList /// Fetches a user MLS one to one conversation. - /// - parameters: - /// - userID: The user ID to fetch the MLS one to one conversation for. - /// - domain: The domain of the one to one conversation. + /// - parameters: + /// - userID: The user ID to fetch the MLS one to one conversation for. + /// - domain: The domain of the one to one conversation. - func getMLSOneToOneConversation( - userID: String, - in domain: String - ) async throws -> Conversation + func getMLSOneToOneConversation( + userID: String, + in domain: String + ) async throws -> Conversation } diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift index c5bb3bcc977..fe41d9099e0 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIError.swift @@ -21,16 +21,16 @@ public enum ConversationsAPIError: Error { /// Failure if functionality has not been implemented. case notImplemented - + /// Failure if http body is invalid. case invalidBody - + /// Unsupported endpoint for API version case unsupportedEndpointForAPIVersion - + /// MLS not enabled case mlsNotEnabled - + /// Users not connected case usersNotConnected diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift index 7af692516e0..de07f1a94ef 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV0.swift @@ -90,13 +90,13 @@ class ConversationsAPIV0: ConversationsAPI, VersionedAPI { .failure(code: .badRequest, error: ConversationsAPIError.invalidBody) .parse(response) } - + func getMLSOneToOneConversation( - userID: String, - in domain: String - ) async throws -> Conversation { - throw ConversationsAPIError.unsupportedEndpointForAPIVersion - } + userID: String, + in domain: String + ) async throws -> Conversation { + throw ConversationsAPIError.unsupportedEndpointForAPIVersion + } } // MARK: Encodables diff --git a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift index f3ead41cbb0..e852778f6f8 100644 --- a/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift +++ b/WireAPI/Sources/WireAPI/APIs/ConversationsAPI/ConversationsAPIV5.swift @@ -38,26 +38,26 @@ class ConversationsAPIV5: ConversationsAPIV4 { .success(code: .ok, type: QualifiedConversationListV5.self) // Change in v5 .parse(response) } - + override func getMLSOneToOneConversation( - userID: String, - in domain: String - ) async throws -> Conversation { - let resourcePath = "\(pathPrefix)/conversations/one2one/\(domain)/\(userID)" - - let request = HTTPRequest( - path: resourcePath, - method: .get - ) - - let response = try await httpClient.executeRequest(request) - - return try ResponseParser() - .success(code: .ok, type: ConversationV5.self) - .failure(code: .badRequest, label: "mls-not-enabled", error: ConversationsAPIError.mlsNotEnabled) - .failure(code: .forbidden, label: "not-connected", error: ConversationsAPIError.usersNotConnected) - .parse(response) - } + userID: String, + in domain: String + ) async throws -> Conversation { + let resourcePath = "\(pathPrefix)/conversations/one2one/\(domain)/\(userID)" + + let request = HTTPRequest( + path: resourcePath, + method: .get + ) + + let response = try await httpClient.executeRequest(request) + + return try ResponseParser() + .success(code: .ok, type: ConversationV5.self) + .failure(code: .badRequest, label: "mls-not-enabled", error: ConversationsAPIError.mlsNotEnabled) + .failure(code: .forbidden, label: "not-connected", error: ConversationsAPIError.usersNotConnected) + .parse(response) + } } // MARK: Decodables diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift index 02b2eb6bcdb..50116218b2c 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift @@ -288,7 +288,7 @@ enum LegalholdStatusV0: String, Decodable { case pending case disabled case noConsent = "no_consent" - + func toAPIModel() -> LegalholdStatus { switch self { case .enabled: @@ -306,7 +306,7 @@ enum LegalholdStatusV0: String, Decodable { struct LegalHoldLastPrekeyV0: Decodable, ToAPIModelConvertible { let id: Int let key: String - + func toAPIModel() -> Prekey { Prekey( id: id, @@ -319,7 +319,7 @@ struct TeamMemberLegalHoldResponseV0: Decodable, ToAPIModelConvertible { let lastPrekey: LegalHoldLastPrekeyV0 let status: LegalholdStatusV0 - + enum CodingKeys: String, CodingKey { case status case lastPrekey = "last_prekey" diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift index a7c08307cc8..47cabf87746 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift @@ -89,7 +89,7 @@ class TeamsAPIV4: TeamsAPIV3 { .parse(response) } - // MARK: - Get team member legalhold + // MARK: - Get team member legalhold override func getLegalhold( for teamID: Team.ID, diff --git a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift index 3d93ee7d3a7..bdf0f765aba 100644 --- a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift +++ b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift @@ -47,15 +47,14 @@ public struct Connection: Equatable, Codable { /// current status of connection public let status: ConnectionStatus - + public init(senderID: UUID?, - receiverID: UUID?, - receiverQualifiedID: QualifiedID?, - conversationID: UUID?, - qualifiedConversationID: QualifiedID?, - lastUpdate: Date, - status: ConnectionStatus - ) { + receiverID: UUID?, + receiverQualifiedID: QualifiedID?, + conversationID: UUID?, + qualifiedConversationID: QualifiedID?, + lastUpdate: Date, + status: ConnectionStatus) { self.senderID = senderID self.receiverID = receiverID self.receiverQualifiedID = receiverQualifiedID diff --git a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift index c9acce59d0d..dacf1296fd6 100644 --- a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift +++ b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift @@ -20,15 +20,15 @@ public typealias LegalHoldPrekey = Prekey /// The team member legal hold. public struct TeamMemberLegalHold: Equatable, Sendable { - + /// The legal hold status - + public let status: LegalholdStatus - + /// The legal hold prekey - + public let prekey: LegalHoldPrekey - + public init( status: LegalholdStatus, prekey: LegalHoldPrekey diff --git a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift index 18f82a12049..1980fd503cb 100644 --- a/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/ConversationsAPI/ConversationsAPITests.swift @@ -16,9 +16,9 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest -import WireTestingPackage @testable import WireAPI +import WireTestingPackage +import XCTest final class ConversationsAPITests: XCTestCase { @@ -62,8 +62,7 @@ final class ConversationsAPITests: XCTestCase { } } } - - + func testGetMLSOneToOneConversationRequest() async throws { // Given @@ -482,113 +481,113 @@ final class ConversationsAPITests: XCTestCase { XCTFail("expected error 'FailureResponse'") } } - + func testGetMLSOneToOneConversation_Success_Response_V5_And_Next_Versions() async throws { - // Given - - let httpClient = try HTTPClientMock( - code: .ok, - payloadResourceName: "testGetMLSOneOnOneConversationV5SuccessResponse200" - ) - - let supportedVersions = APIVersion.v5.andNextVersions - - let suts = supportedVersions.map { $0.buildAPI(client: httpClient) } - - // When - - try await withThrowingTaskGroup(of: Conversation.self) { taskGroup in - for sut in suts { - taskGroup.addTask { - try await sut.getMLSOneToOneConversation( - userID: Scaffolding.userID, - in: Scaffolding.domain - ) - } - } - - for try await value in taskGroup { - // Then - XCTAssertEqual(value.id, Scaffolding.mlsConversationID) - } - } - } - - func testGetMLSOneToOneConversation_UnsupportedVersionError_V0_to_V4() async throws { - // Given - let httpClient = HTTPClientMock( - code: .ok, - payload: nil - ) - - let unsupportedVersions: [APIVersion] = [.v0, .v1, .v2, .v3, .v4] - let suts = unsupportedVersions.map { $0.buildAPI(client: httpClient) } - - try await withThrowingTaskGroup(of: Void.self) { taskGroup in - for sut in suts { - taskGroup.addTask { - // Then - await self.XCTAssertThrowsError(ConversationsAPIError.unsupportedEndpointForAPIVersion) { - // When - try await sut.getMLSOneToOneConversation( - userID: Scaffolding.userID, - in: Scaffolding.domain - ) - } - } - - try await taskGroup.waitForAll() - } - } - } - - func testGetMLSOneToOneConversation_Failure_Response_MLS_Not_Enabled() async throws { - // Given - - let httpClient = try HTTPClientMock( - code: .badRequest, - errorLabel: "mls-not-enabled" - ) - - let sut = APIVersion.v5.buildAPI(client: httpClient) - - // Then - - await XCTAssertThrowsError(ConversationsAPIError.mlsNotEnabled) { - // When - try await sut.getMLSOneToOneConversation( - userID: Scaffolding.userID, - in: Scaffolding.domain - ) - } - } - - func testGetMLSOneToOneConversation_Failure_Response_Not_Connected() async throws { - // Given - - let httpClient = try HTTPClientMock( - code: .forbidden, - errorLabel: "not-connected" - ) - - let sut = APIVersion.v5.buildAPI(client: httpClient) - - // Then - - await XCTAssertThrowsError(ConversationsAPIError.usersNotConnected) { - // When - try await sut.getMLSOneToOneConversation( - userID: Scaffolding.userID, - in: Scaffolding.domain - ) - } - } - - private enum Scaffolding { - static let userID = "99db9768-04e3-4b5d-9268-831b6a25c4ab" - static let domain = "domain.com" - static let mlsConversationID = UUID(uuidString: "99db9768-04e3-4b5d-9268-831b6a25c4ab")! - } + // Given + + let httpClient = try HTTPClientMock( + code: .ok, + payloadResourceName: "testGetMLSOneOnOneConversationV5SuccessResponse200" + ) + + let supportedVersions = APIVersion.v5.andNextVersions + + let suts = supportedVersions.map { $0.buildAPI(client: httpClient) } + + // When + + try await withThrowingTaskGroup(of: Conversation.self) { taskGroup in + for sut in suts { + taskGroup.addTask { + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + for try await value in taskGroup { + // Then + XCTAssertEqual(value.id, Scaffolding.mlsConversationID) + } + } + } + + func testGetMLSOneToOneConversation_UnsupportedVersionError_V0_to_V4() async throws { + // Given + let httpClient = HTTPClientMock( + code: .ok, + payload: nil + ) + + let unsupportedVersions: [APIVersion] = [.v0, .v1, .v2, .v3, .v4] + let suts = unsupportedVersions.map { $0.buildAPI(client: httpClient) } + + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + for sut in suts { + taskGroup.addTask { + // Then + await self.XCTAssertThrowsError(ConversationsAPIError.unsupportedEndpointForAPIVersion) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + try await taskGroup.waitForAll() + } + } + } + + func testGetMLSOneToOneConversation_Failure_Response_MLS_Not_Enabled() async throws { + // Given + + let httpClient = try HTTPClientMock( + code: .badRequest, + errorLabel: "mls-not-enabled" + ) + + let sut = APIVersion.v5.buildAPI(client: httpClient) + + // Then + + await XCTAssertThrowsError(ConversationsAPIError.mlsNotEnabled) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + func testGetMLSOneToOneConversation_Failure_Response_Not_Connected() async throws { + // Given + + let httpClient = try HTTPClientMock( + code: .forbidden, + errorLabel: "not-connected" + ) + + let sut = APIVersion.v5.buildAPI(client: httpClient) + + // Then + + await XCTAssertThrowsError(ConversationsAPIError.usersNotConnected) { + // When + try await sut.getMLSOneToOneConversation( + userID: Scaffolding.userID, + in: Scaffolding.domain + ) + } + } + + private enum Scaffolding { + static let userID = "99db9768-04e3-4b5d-9268-831b6a25c4ab" + static let domain = "domain.com" + static let mlsConversationID = UUID(uuidString: "99db9768-04e3-4b5d-9268-831b6a25c4ab")! + } } diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index a7d90be5333..65ed0a3862f 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -273,7 +273,7 @@ final class TeamsAPITests: XCTestCase { ) // Then - let expectedPrekey = LegalHoldPrekey(id: 12345, base64EncodedKey: "foo") + let expectedPrekey = LegalHoldPrekey(id: 12_345, base64EncodedKey: "foo") XCTAssertEqual(result.status, .pending) XCTAssertEqual(result.prekey, expectedPrekey) } diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift index 4ff2c7c577b..e62ada982b9 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationLocalStore.swift @@ -66,17 +66,17 @@ public protocol ConversationLocalStoreProtocol { user: ZMUser, removalDate: Date ) async - + /// Fetches a MLS conversation locally. - /// - /// - parameters: - /// - groupID: The MLS group ID object. - /// - /// - returns : A MLS conversation. - - func fetchMLSConversation( - with groupID: WireDataModel.MLSGroupID - ) async -> ZMConversation? + /// + /// - parameters: + /// - groupID: The MLS group ID object. + /// + /// - returns : A MLS conversation. + + func fetchMLSConversation( + with groupID: WireDataModel.MLSGroupID + ) async -> ZMConversation? } public final class ConversationLocalStore: ConversationLocalStoreProtocol { @@ -227,17 +227,17 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol { } } } - + public func fetchMLSConversation( - with groupID: WireDataModel.MLSGroupID - ) async -> ZMConversation? { - await context.perform { [context] in - ZMConversation.fetch( - with: groupID, - in: context - ) - } - } + with groupID: WireDataModel.MLSGroupID + ) async -> ZMConversation? { + await context.perform { [context] in + ZMConversation.fetch( + with: groupID, + in: context + ) + } + } // MARK: - Private diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift index a6914974451..b10172412cb 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepository.swift @@ -38,30 +38,30 @@ public protocol ConversationRepositoryProtocol { user: ZMUser, removalDate: Date ) async - + /// Pulls and stores a MLS one to one conversation locally. - /// - /// - parameters: - /// - userID: The user ID. - /// - domain: The user domain. - /// - /// - returns : The MLS group ID. - - func pullMLSOneToOneConversation( - userID: String, - domain: String - ) async throws -> String - - /// Fetches a MLS conversation locally. - /// - /// - parameters: - /// - groupID: The MLS group ID. - /// - /// - returns : A MLS conversation. - - func fetchMLSConversation( - with groupID: String - ) async -> ZMConversation? + /// + /// - parameters: + /// - userID: The user ID. + /// - domain: The user domain. + /// + /// - returns : The MLS group ID. + + func pullMLSOneToOneConversation( + userID: String, + domain: String + ) async throws -> String + + /// Fetches a MLS conversation locally. + /// + /// - parameters: + /// - groupID: The MLS group ID. + /// + /// - returns : A MLS conversation. + + func fetchMLSConversation( + with groupID: String + ) async -> ZMConversation? } @@ -152,38 +152,38 @@ public final class ConversationRepository: ConversationRepositoryProtocol { removalDate: removalDate ) } - + public func pullMLSOneToOneConversation( - userID: String, - domain: String - ) async throws -> String { - let mlsConversation = try await conversationsAPI.getMLSOneToOneConversation( - userID: userID, - in: domain - ) - - guard let mlsGroupID = mlsConversation.mlsGroupID else { - throw ConversationRepositoryError.mlsConversationShouldHaveAGroupID - } - - await conversationsLocalStore.storeConversation( - mlsConversation, - isFederationEnabled: backendInfo.isFederationEnabled - ) - - return mlsGroupID - } - - public func fetchMLSConversation( - with groupID: String - ) async -> ZMConversation? { - guard let mlsGroupID = MLSGroupID(base64Encoded: groupID) else { - return nil - } - - return await conversationsLocalStore.fetchMLSConversation( - with: mlsGroupID - ) - } + userID: String, + domain: String + ) async throws -> String { + let mlsConversation = try await conversationsAPI.getMLSOneToOneConversation( + userID: userID, + in: domain + ) + + guard let mlsGroupID = mlsConversation.mlsGroupID else { + throw ConversationRepositoryError.mlsConversationShouldHaveAGroupID + } + + await conversationsLocalStore.storeConversation( + mlsConversation, + isFederationEnabled: backendInfo.isFederationEnabled + ) + + return mlsGroupID + } + + public func fetchMLSConversation( + with groupID: String + ) async -> ZMConversation? { + guard let mlsGroupID = MLSGroupID(base64Encoded: groupID) else { + return nil + } + + return await conversationsLocalStore.fetchMLSConversation( + with: mlsGroupID + ) + } } diff --git a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift index 73ace9b1af4..8f9d820efe3 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Conversations/ConversationRepositoryError.swift @@ -25,9 +25,9 @@ enum ConversationRepositoryError: Error { /// Unable to delete conversation. case failedToDeleteConversation(Error) - + /// Missing MLS group ID - case mlsConversationShouldHaveAGroupID + case mlsConversationShouldHaveAGroupID } diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift index 441d1724ac0..3d9f7b34188 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift @@ -59,20 +59,20 @@ public protocol TeamRepositoryProtocol { /// - Parameter membershipID: The id of the team member. func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws - + func pullSelfLegalHoldStatus() async throws } public final class TeamRepository: TeamRepositoryProtocol { - + // MARK: - Properties private let selfTeamID: UUID private let userRepository: any UserRepositoryProtocol private let teamsAPI: any TeamsAPI private let context: NSManagedObjectContext - + // MARK: - Object lifecycle public init( @@ -134,38 +134,39 @@ public final class TeamRepository: TeamRepositoryProtocol { try context.save() } } - + public func pullSelfTeamRoles() async throws { let teamRoles = try await fetchSelfTeamRolesRemotely() try await storeTeamRolesLocally(teamRoles) } - + public func pullSelfTeamMembers() async throws { let teamMembers = try await fetchSelfTeamMembersRemotely() try await storeTeamMembersLocally(teamMembers) } - + public func pullSelfLegalHoldStatus() async throws { let selfUser = await context.perform { [userRepository] in userRepository.fetchSelfUser() } - + let selfUserLegalHold = try await fetchSelfLegalhold() - + switch selfUserLegalHold.status { case .pending: guard let selfClientID = selfUser.selfClient()?.remoteIdentifier else { return } - + await userRepository.addLegalHoldRequest( for: selfUser.remoteIdentifier, clientID: selfClientID, lastPrekey: selfUserLegalHold.prekey ) - + case .disabled: try await userRepository.disableUserLegalHold() + default: break } @@ -181,7 +182,7 @@ public final class TeamRepository: TeamRepositoryProtocol { userID: selfUserID ) } - + // MARK: - Private private func fetchSelfTeamRemotely() async throws -> WireAPI.Team { diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index 4a0d29016ce..b4ec3190e09 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -21,7 +21,7 @@ import WireAPI import WireSystem protocol SyncManagerProtocol { - + /// Pulls and stores all required objects for the database to be initially up-to-date. func performSlowSync() async throws @@ -37,12 +37,12 @@ protocol SyncManagerProtocol { } final class SyncManager: SyncManagerProtocol { - + // MARK: - Properties private(set) var syncState: SyncState = .suspended private var isSuspending = false - + // MARK: - Repositories private let updateEventsRepository: any UpdateEventsRepositoryProtocol @@ -54,11 +54,11 @@ final class SyncManager: SyncManagerProtocol { private let featureConfigsRepository: any FeatureConfigRepositoryProtocol private let pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol private let oneOnOneResolverUseCase: any OneOnOneResolverUseCaseProtocol - + // MARK: - Update event processor - + private let updateEventProcessor: any UpdateEventProcessorProtocol - + // MARK: - Object lifecycle init( @@ -84,7 +84,7 @@ final class SyncManager: SyncManagerProtocol { self.pushSupportedProtocolsUseCase = pushSupportedProtocolsUseCase self.oneOnOneResolverUseCase = oneOnOneResolverUseCase } - + func performSlowSync() async throws { try await updateEventsRepository.pullLastEventID() try await teamRepository.pullSelfTeam() diff --git a/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift index 547776f3aa5..842d8a54e00 100644 --- a/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift +++ b/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift @@ -17,8 +17,8 @@ // import CoreData -import WireDataModel import WireAPI +import WireDataModel // sourcery: AutoMockable /// Resolves 1:1 conversations @@ -27,50 +27,53 @@ public protocol OneOnOneResolverUseCaseProtocol { } public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { - + private enum OneOnOneResolverUseCaseError: Error { case failedToActivateConversation case failedToFetchConversation case failedToEstablishGroup(Error) } - + // MARK: - Properties - + private let context: NSManagedObjectContext private let userRepository: any UserRepositoryProtocol private let conversationsRepository: any ConversationRepositoryProtocol private let mlsService: any MLSServiceInterface - + private let isMLSEnabled: Bool + // MARK: - Object lifecycle - + public init( context: NSManagedObjectContext, userRepository: any UserRepositoryProtocol, conversationsRepository: any ConversationRepositoryProtocol, - mlsService: any MLSServiceInterface + mlsService: any MLSServiceInterface, + isMLSEnabled: Bool ) { self.context = context self.userRepository = userRepository self.conversationsRepository = conversationsRepository self.mlsService = mlsService + self.isMLSEnabled = isMLSEnabled } - + // MARK: - Public - + public func invoke() async throws { try await resolveAllOneOnOneConversations() } - + // MARK: - Private - + private func resolveAllOneOnOneConversations() async throws { - let usersIDs = try await fetchUserIdsWithOneOnOneConversation() - + let usersIDs = try await userRepository.fetchAllUserIdsWithOneOnOneConversation() + await withTaskGroup(of: Void.self) { group in for userID in usersIDs { group.addTask { do { - try await self.resolveOneOnOneConversation(with: userID) + try await resolveOneOnOneConversation(with: userID) } catch { /// skip conversation migration for this user WireLogger.conversation.error( @@ -81,246 +84,224 @@ public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { } } } - - private func fetchUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { - try await context.perform { - let request = NSFetchRequest(entityName: ZMUser.entityName()) - let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation)) - request.predicate = predicate - - return try context - .fetch(request) - .compactMap { user in - guard let userID = user.qualifiedID else { - WireLogger.conversation.error( - "missing user's qualifiedID to resolve 1-1 conversation!" - ) - return nil - } - return userID - } - } - } - - public func resolveOneOnOneConversation( + + private func resolveOneOnOneConversation( with userID: WireDataModel.QualifiedID ) async throws { - let user = try await userRepository.fetchUser( with: userID.uuid, domain: userID.domain ) - let selfUser = userRepository.fetchSelfUser() - let commonProtocol = getCommonProtocol(between: selfUser, and: user) - let mlsEnabled = DeveloperFlag.enableMLSSupport.isOn - - if mlsEnabled, commonProtocol == .mls { - try await resolveMLSConversation( - for: user - ) - } - - if mlsEnabled, commonProtocol == nil { - await resolveNoCommonProtocolConversation( - between: selfUser, - and: user - ) - } - - if commonProtocol == .proteus { - await resolveProteusConversation( - for: user - ) - } + let selfUser = userRepository.fetchSelfUser() + let commonProtocol = getCommonProtocol(between: selfUser, and: user) + + if isMLSEnabled, commonProtocol == .mls { + try await resolveMLSConversation( + for: user + ) + } + + if isMLSEnabled, commonProtocol == nil { + await resolveNoCommonProtocolConversation( + between: selfUser, + and: user + ) + } + + if commonProtocol == .proteus { + await resolveProteusConversation( + for: user + ) + } } - + private func resolveMLSConversation(for user: ZMUser) async throws { - WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") - - guard let userID = user.qualifiedID else { - throw OneOnOneResolverUseCaseError.failedToActivateConversation - } - - /// Sync the user MLS conversation from backend. - let mlsGroupID = try await conversationsRepository.pullMLSOneToOneConversation( - userID: userID.uuid.uuidString, - domain: userID.domain - ) - - /// Then, fetch the synced MLS conversation. - let mlsConversation = await conversationsRepository.fetchMLSConversation(with: mlsGroupID) - - guard let mlsConversation, let groupID = mlsConversation.mlsGroupID else { - throw OneOnOneResolverUseCaseError.failedToFetchConversation - } - - let needsMLSMigration = try await mlsService.conversationExists( - groupID: groupID - ) == false - - /// If conversation already exists, there is no need to perform a migration. - if needsMLSMigration { - await migrateToMLS( - mlsConversation: mlsConversation, - mlsGroupID: groupID, - user: user, - userID: userID - ) - } - } - - private func migrateToMLS( - mlsConversation: ZMConversation, - mlsGroupID: MLSGroupID, - user: ZMUser, - userID: WireDataModel.QualifiedID - ) async { - do { - try await setupMLSGroup( - mlsConversation: mlsConversation, - groupID: mlsGroupID, - userID: userID - ) - } catch { - await context.perform { - let userOneOnOneConversation = user.oneOnOneConversation - userOneOnOneConversation?.isForcedReadOnly = true - } - - return WireLogger.conversation.error( - "Failed to setup MLS group with ID: \(mlsGroupID.safeForLoggingDescription)" - ) - } - - await switchLocalConversationToMLS( - mlsConversation: mlsConversation, - for: user - ) - } - - /// Establish a new MLS group (when epoch is 0) or join an existing group. - /// - parameters: - /// - mlsConversation: The 1:1 MLS conversation. - /// - groupID: The MLS group ID. - /// - userID: The user ID that will be part of the MLS group. - - private func setupMLSGroup( - mlsConversation: ZMConversation, - groupID: MLSGroupID, - userID: WireDataModel.QualifiedID - ) async throws { - if mlsConversation.epoch == 0 { - let users = [MLSUser(userID)] - - do { - let ciphersuite = try await mlsService.establishGroup( - for: groupID, - with: users, - removalKeys: nil - ) - - await context.perform { - mlsConversation.ciphersuite = ciphersuite - mlsConversation.mlsStatus = .ready - } - - } catch { - throw OneOnOneResolverUseCaseError.failedToEstablishGroup(error) - } - } else { - try await mlsService.joinGroup(with: groupID) - } - } - - /// Migrates Proteus messages to the MLS conversation and sets the MLS conversation for the user. - /// - Parameter mlsConversation: The MLS conversation. - /// - Parameter user: The user to set the MLS conversation for. - - private func switchLocalConversationToMLS( - mlsConversation: ZMConversation, - for user: ZMUser - ) async { - await context.perform { - /// Move local messages from proteus conversation if it exists - if let proteusConversation = user.oneOnOneConversation { - /// Since ZMMessages only have a single conversation connected, - /// forming this union also removes the relationship to the proteus conversation. - mlsConversation.mutableMessages.union(proteusConversation.allMessages) - mlsConversation.isForcedReadOnly = false - mlsConversation.needsToBeUpdatedFromBackend = true - } - - /// Switch active conversation - user.oneOnOneConversation = mlsConversation - } - } - - /// Resolves a Proteus 1:1 conversation. - /// - Parameter user: The user to resolve the conversation for. - - private func resolveProteusConversation( - for user: ZMUser - ) async { - WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") - - guard let conversation = user.oneOnOneConversation else { - return WireLogger.conversation.warn( - "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } - - await context.perform { - conversation.isForcedReadOnly = false - } - } - - /// Resolves a 1:1 conversation with no common protocols between self user and user. - /// - Parameter selfUser: The self user. - /// - Parameter user: The other user. - /// - /// When no common protocols are found, the 1:1 conversation is marked as read only and a system - /// message is append to the conversation to inform the self user or the user. - - private func resolveNoCommonProtocolConversation( - between selfUser: ZMUser, - and user: ZMUser - ) async { - WireLogger.conversation.debug("No common protocols found") - - guard let conversation = user.oneOnOneConversation else { - return WireLogger.conversation.warn( - "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } - - if !conversation.isForcedReadOnly { - await context.perform { - if !selfUser.supportedProtocols.contains(.mls) { - conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser) - } else if !user.supportedProtocols.contains(.mls) { - conversation.appendMLSMigrationMLSNotSupportedForOtherUser(user: user) - } - - conversation.isForcedReadOnly = true - } - } - } - - private func getCommonProtocol( - between selfUser: ZMUser, - and otherUser: ZMUser - ) -> ConversationMessageProtocol? { - let selfUserProtocols = selfUser.supportedProtocols - let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols /// default to Proteus if empty. - - let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) - - if commonProtocols.contains(.mls) { - return .mls - } else if commonProtocols.contains(.proteus) { - return .proteus - } else { - return nil - } - } + WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") + + guard let userID = user.qualifiedID else { + throw OneOnOneResolverUseCaseError.failedToActivateConversation + } + + /// Sync the user MLS conversation from backend. + let mlsGroupID = try await conversationsRepository.pullMLSOneToOneConversation( + userID: userID.uuid.uuidString, + domain: userID.domain + ) + + /// Then, fetch the synced MLS conversation. + let mlsConversation = await conversationsRepository.fetchMLSConversation(with: mlsGroupID) + + guard let mlsConversation, let groupID = mlsConversation.mlsGroupID else { + throw OneOnOneResolverUseCaseError.failedToFetchConversation + } + + /// If conversation already exists, there is no need to perform a migration. + let needsMLSMigration = try await mlsService.conversationExists( + groupID: groupID + ) == false + + if needsMLSMigration { + await migrateToMLS( + mlsConversation: mlsConversation, + mlsGroupID: groupID, + user: user, + userID: userID + ) + } + } + + private func migrateToMLS( + mlsConversation: ZMConversation, + mlsGroupID: MLSGroupID, + user: ZMUser, + userID: WireDataModel.QualifiedID + ) async { + do { + try await setupMLSGroup( + mlsConversation: mlsConversation, + groupID: mlsGroupID, + userID: userID + ) + } catch { + await context.perform { + let userOneOnOneConversation = user.oneOnOneConversation + userOneOnOneConversation?.isForcedReadOnly = true + } + + return WireLogger.conversation.error( + "Failed to setup MLS group with ID: \(mlsGroupID.safeForLoggingDescription)" + ) + } + + await switchLocalConversationToMLS( + mlsConversation: mlsConversation, + for: user + ) + } + + /// Establish a new MLS group (when epoch is 0) or join an existing group. + /// - parameters: + /// - mlsConversation: The 1:1 MLS conversation. + /// - groupID: The MLS group ID. + /// - userID: The user ID that will be part of the MLS group. + + private func setupMLSGroup( + mlsConversation: ZMConversation, + groupID: MLSGroupID, + userID: WireDataModel.QualifiedID + ) async throws { + if mlsConversation.epoch == 0 { + let users = [MLSUser(userID)] + + do { + let ciphersuite = try await mlsService.establishGroup( + for: groupID, + with: users, + removalKeys: nil + ) + + await context.perform { + mlsConversation.ciphersuite = ciphersuite + mlsConversation.mlsStatus = .ready + } + + } catch { + throw OneOnOneResolverUseCaseError.failedToEstablishGroup(error) + } + } else { + try await mlsService.joinGroup(with: groupID) + } + } + + /// Migrates Proteus messages to the MLS conversation and sets the MLS conversation for the user. + /// - Parameter mlsConversation: The MLS conversation. + /// - Parameter user: The user to set the MLS conversation for. + + private func switchLocalConversationToMLS( + mlsConversation: ZMConversation, + for user: ZMUser + ) async { + await context.perform { + /// Move local messages from proteus conversation if it exists + if let proteusConversation = user.oneOnOneConversation { + /// Since ZMMessages only have a single conversation connected, + /// forming this union also removes the relationship to the proteus conversation. + mlsConversation.mutableMessages.union(proteusConversation.allMessages) + mlsConversation.isForcedReadOnly = false + mlsConversation.needsToBeUpdatedFromBackend = true + } + + /// Switch active conversation + user.oneOnOneConversation = mlsConversation + } + } + + /// Resolves a Proteus 1:1 conversation. + /// - Parameter user: The user to resolve the conversation for. + + private func resolveProteusConversation( + for user: ZMUser + ) async { + WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") + + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } + + await context.perform { + conversation.isForcedReadOnly = false + } + } + + /// Resolves a 1:1 conversation with no common protocols between self user and user. + /// - Parameter selfUser: The self user. + /// - Parameter user: The other user. + /// + /// When no common protocols are found, the 1:1 conversation is marked as read only and a system + /// message is append to the conversation to inform the self user or the user. + + private func resolveNoCommonProtocolConversation( + between selfUser: ZMUser, + and user: ZMUser + ) async { + WireLogger.conversation.debug("No common protocols found") + + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } + + if !conversation.isForcedReadOnly { + await context.perform { + if !selfUser.supportedProtocols.contains(.mls) { + conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser) + } else if !user.supportedProtocols.contains(.mls) { + conversation.appendMLSMigrationMLSNotSupportedForOtherUser(user: user) + } + + conversation.isForcedReadOnly = true + } + } + } + + private func getCommonProtocol( + between selfUser: ZMUser, + and otherUser: ZMUser + ) -> ConversationMessageProtocol? { + let selfUserProtocols = selfUser.supportedProtocols + let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols /// default to Proteus if empty. + + let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) + + if commonProtocols.contains(.mls) { + return .mls + } else if commonProtocols.contains(.proteus) { + return .proteus + } else { + return nil + } + } } diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 545a27962b1..22c5a75a75e 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -977,6 +977,29 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol { await mock(user, date) } + // MARK: - fetchAllUserIdsWithOneOnOneConversation + + public var fetchAllUserIdsWithOneOnOneConversation_Invocations: [Void] = [] + public var fetchAllUserIdsWithOneOnOneConversation_MockError: Error? + public var fetchAllUserIdsWithOneOnOneConversation_MockMethod: (() async throws -> [WireDataModel.QualifiedID])? + public var fetchAllUserIdsWithOneOnOneConversation_MockValue: [WireDataModel.QualifiedID]? + + public func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + fetchAllUserIdsWithOneOnOneConversation_Invocations.append(()) + + if let error = fetchAllUserIdsWithOneOnOneConversation_MockError { + throw error + } + + if let mock = fetchAllUserIdsWithOneOnOneConversation_MockMethod { + return try await mock() + } else if let mock = fetchAllUserIdsWithOneOnOneConversation_MockValue { + return mock + } else { + fatalError("no mock for `fetchAllUserIdsWithOneOnOneConversation`") + } + } + } // swiftlint:enable variable_name diff --git a/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift index c01afb91951..368a7c8a019 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/ConversationRepositoryTests.swift @@ -308,25 +308,25 @@ class ConversationRepositoryTests: XCTestCase { accuracy: 0.1 ) } - + func testGetMLSOneToOneConversation() async throws { - // Mock + // Mock - mockConversationsAPI() + mockConversationsAPI() - // When + // When - let mlsGroupID = try await sut.pullMLSOneToOneConversation( - userID: Scaffolding.userID.uuidString, - domain: Scaffolding.domain - ) + let mlsGroupID = try await sut.pullMLSOneToOneConversation( + userID: Scaffolding.userID.uuidString, + domain: Scaffolding.domain + ) - let mlsConversation = await sut.fetchMLSConversation(with: mlsGroupID) + let mlsConversation = await sut.fetchMLSConversation(with: mlsGroupID) - // Then + // Then - XCTAssertEqual(mlsConversation?.remoteIdentifier, Scaffolding.conversationOneOnOneType.id) - } + XCTAssertEqual(mlsConversation?.remoteIdentifier, Scaffolding.conversationOneOnOneType.id) + } } @@ -374,7 +374,7 @@ extension ConversationRepositoryTests { notFound: conversationList.notFound, failed: conversationList.failed ) - + conversationsAPI.getMLSOneToOneConversationUserIDIn_MockValue = Scaffolding.conversationOneOnOneType } @@ -392,7 +392,7 @@ extension ConversationRepositoryTests { } static let base64EncodedString = "pQABARn//wKhAFggHsa0CszLXYLFcOzg8AA//E1+Dl1rDHQ5iuk44X0/PNYDoQChAFgg309rkhG6SglemG6kWae81P1HtQPx9lyb6wExTovhU4cE9g==" - + nonisolated(unsafe) static let conversationList = ConversationList( found: [conversationSelfType, conversationGroupType, diff --git a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift index f89133c6483..7ecef8f4488 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/Mock/MockFeatureConfigRepositoryProtocol.swift @@ -24,36 +24,36 @@ import WireDataModel class MockFeatureConfigRepositoryProtocol: FeatureConfigRepositoryProtocol { // MARK: - Life cycle - + // MARK: - pullFeatureConfigs - + var pullFeatureConfigs_Invocations: [Void] = [] var pullFeatureConfigs_MockError: Error? var pullFeatureConfigs_MockMethod: (() async throws -> Void)? - + func pullFeatureConfigs() async throws { pullFeatureConfigs_Invocations.append(()) - + if let error = pullFeatureConfigs_MockError { throw error } - + guard let mock = pullFeatureConfigs_MockMethod else { fatalError("no mock for `pullFeatureConfigs`") } - + try await mock() } - + // MARK: - observeFeatureStates - + var observeFeatureStates_Invocations: [Void] = [] var observeFeatureStates_MockMethod: (() -> AnyPublisher)? var observeFeatureStates_MockValue: AnyPublisher? - + func observeFeatureStates() -> AnyPublisher { observeFeatureStates_Invocations.append(()) - + if let mock = observeFeatureStates_MockMethod { return mock() } else if let mock = observeFeatureStates_MockValue { @@ -62,47 +62,47 @@ class MockFeatureConfigRepositoryProtocol: FeatureConfigRepositoryProtocol { fatalError("no mock for `observeFeatureStates`") } } - + // MARK: - fetchFeatureConfig - + func fetchFeatureConfig(with name: Feature.Name, type: T.Type) async throws -> LocalFeature { fatalError("to implement using generics") } - + // MARK: - updateFeatureConfig - + var updateFeatureConfig_Invocations: [FeatureConfig] = [] var updateFeatureConfig_MockError: Error? var updateFeatureConfig_MockMethod: ((FeatureConfig) async throws -> Void)? - + func updateFeatureConfig(_ featureConfig: FeatureConfig) async throws { updateFeatureConfig_Invocations.append(featureConfig) - + if let error = updateFeatureConfig_MockError { throw error } - + guard let mock = updateFeatureConfig_MockMethod else { fatalError("no mock for `updateFeatureConfig`") } - + try await mock(featureConfig) } - + // MARK: - fetchNeedsToNotifyUser - + var fetchNeedsToNotifyUserFor_Invocations: [Feature.Name] = [] var fetchNeedsToNotifyUserFor_MockError: Error? var fetchNeedsToNotifyUserFor_MockMethod: ((Feature.Name) async throws -> Bool)? var fetchNeedsToNotifyUserFor_MockValue: Bool? - + func fetchNeedsToNotifyUser(for name: Feature.Name) async throws -> Bool { fetchNeedsToNotifyUserFor_Invocations.append(name) - + if let error = fetchNeedsToNotifyUserFor_MockError { throw error } - + if let mock = fetchNeedsToNotifyUserFor_MockMethod { return try await mock(name) } else if let mock = fetchNeedsToNotifyUserFor_MockValue { @@ -111,25 +111,25 @@ class MockFeatureConfigRepositoryProtocol: FeatureConfigRepositoryProtocol { fatalError("no mock for `fetchNeedsToNotifyUserFor`") } } - + // MARK: - storeNeedsToNotifyUser - + var storeNeedsToNotifyUserForFeatureName_Invocations: [(notifyUser: Bool, name: Feature.Name)] = [] var storeNeedsToNotifyUserForFeatureName_MockError: Error? var storeNeedsToNotifyUserForFeatureName_MockMethod: ((Bool, Feature.Name) async throws -> Void)? - + func storeNeedsToNotifyUser(_ notifyUser: Bool, forFeatureName name: Feature.Name) async throws { storeNeedsToNotifyUserForFeatureName_Invocations.append((notifyUser: notifyUser, name: name)) - + if let error = storeNeedsToNotifyUserForFeatureName_MockError { throw error } - + guard let mock = storeNeedsToNotifyUserForFeatureName_MockMethod else { fatalError("no mock for `storeNeedsToNotifyUserForFeatureName`") } - + try await mock(notifyUser, name) } - + } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift index afd1998d343..5878fe8c6e9 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift @@ -272,11 +272,11 @@ private enum Scaffolding { static let member2CreatorID = UUID() static let member2legalholdStatus = LegalholdStatus.pending static let member2Permissions = Permissions.member.rawValue - + static let teamMemberLegalHold = TeamMemberLegalHold( status: .pending, prekey: prekey ) - - static let prekey = LegalHoldPrekey(id: 2330, base64EncodedKey: "foo") + + static let prekey = LegalHoldPrekey(id: 2_330, base64EncodedKey: "foo") } diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index f69d739b2c9..2f6c36e91f6 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -25,7 +25,7 @@ import XCTest @testable import WireDomainSupport final class SyncManagerTests: XCTestCase { - + private var sut: SyncManager! private var updateEventsRepository: MockUpdateEventsRepositoryProtocol! private var updateEventProcessor: MockUpdateEventProcessorProtocol! @@ -33,7 +33,7 @@ final class SyncManagerTests: XCTestCase { private var connectionsRepository: MockConnectionsRepositoryProtocol! private var conversationsRepository: MockConversationRepositoryProtocol! private var userRepository: MockUserRepositoryProtocol! - private var conversationLabelsRepository: MockConversationLabelsRepositoryProtocol! + private var conversationLabelsRepository: MockConversationLabelsRepositoryProtocol! private var featureConfigsRepository: MockFeatureConfigRepositoryProtocol! private var pushSupportedProtocolsUseCase: MockPushSupportedProtocolsUseCaseProtocol! private var oneOnOneResolverUseCase: MockOneOnOneResolverUseCaseProtocol! @@ -46,11 +46,11 @@ final class SyncManagerTests: XCTestCase { connectionsRepository = MockConnectionsRepositoryProtocol() conversationsRepository = MockConversationRepositoryProtocol() userRepository = MockUserRepositoryProtocol() - conversationLabelsRepository = MockConversationLabelsRepositoryProtocol() + conversationLabelsRepository = MockConversationLabelsRepositoryProtocol() featureConfigsRepository = MockFeatureConfigRepositoryProtocol() pushSupportedProtocolsUseCase = MockPushSupportedProtocolsUseCaseProtocol() oneOnOneResolverUseCase = MockOneOnOneResolverUseCaseProtocol() - + sut = SyncManager( updateEventsRepository: updateEventsRepository, teamRepository: teamRepository, @@ -83,7 +83,7 @@ final class SyncManagerTests: XCTestCase { connectionsRepository = nil conversationsRepository = nil userRepository = nil - conversationLabelsRepository = nil + conversationLabelsRepository = nil featureConfigsRepository = nil pushSupportedProtocolsUseCase = nil oneOnOneResolverUseCase = nil @@ -315,31 +315,30 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(updateEventsRepository.stopReceivingLiveEvents_Invocations.count, 0) } - + func testPerformSlowSync() async throws { - // Mock - - updateEventsRepository.pullLastEventID_MockMethod = { } - teamRepository.pullSelfTeam_MockMethod = { } - teamRepository.pullSelfTeamRoles_MockMethod = { } - teamRepository.pullSelfTeamMembers_MockMethod = { } - connectionsRepository.pullConnections_MockMethod = { } - conversationsRepository.pullConversations_MockMethod = { } - userRepository.pullKnownUsers_MockMethod = { } - conversationLabelsRepository.pullConversationLabels_MockMethod = { } - featureConfigsRepository.pullFeatureConfigs_MockMethod = { } - userRepository.pullSelfUser_MockMethod = { } - teamRepository.pullSelfLegalHoldStatus_MockMethod = { } - pushSupportedProtocolsUseCase.invoke_MockMethod = { } - oneOnOneResolverUseCase.invoke_MockMethod = { } - + + updateEventsRepository.pullLastEventID_MockMethod = {} + teamRepository.pullSelfTeam_MockMethod = {} + teamRepository.pullSelfTeamRoles_MockMethod = {} + teamRepository.pullSelfTeamMembers_MockMethod = {} + connectionsRepository.pullConnections_MockMethod = {} + conversationsRepository.pullConversations_MockMethod = {} + userRepository.pullKnownUsers_MockMethod = {} + conversationLabelsRepository.pullConversationLabels_MockMethod = {} + featureConfigsRepository.pullFeatureConfigs_MockMethod = {} + userRepository.pullSelfUser_MockMethod = {} + teamRepository.pullSelfLegalHoldStatus_MockMethod = {} + pushSupportedProtocolsUseCase.invoke_MockMethod = {} + oneOnOneResolverUseCase.invoke_MockMethod = {} + // When - + try await sut.performSlowSync() - + // Then - + XCTAssertEqual(updateEventsRepository.pullLastEventID_Invocations.count, 1) XCTAssertEqual(teamRepository.pullSelfTeam_Invocations.count, 1) XCTAssertEqual(teamRepository.pullSelfTeamRoles_Invocations.count, 1) diff --git a/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift b/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift index 876a3503277..e9158da064f 100644 --- a/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift +++ b/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift @@ -28,18 +28,18 @@ import XCTest final class OneOnOneResolverUseCaseTests: XCTestCase { var sut: OneOnOneResolverUseCase! - + var coreDataStack: CoreDataStack! var coreDataStackHelper: CoreDataStackHelper! var modelHelper: ModelHelper! var userRepository: MockUserRepositoryProtocol! var conversationsRepository: MockConversationRepositoryProtocol! var mlsService: MockMLSServiceInterface! - + var context: NSManagedObjectContext { coreDataStack.syncContext } - + override func setUp() async throws { try await super.setUp() coreDataStackHelper = CoreDataStackHelper() @@ -52,59 +52,54 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { context: context, userRepository: userRepository, conversationsRepository: conversationsRepository, - mlsService: mlsService + mlsService: mlsService, + isMLSEnabled: true ) - - DeveloperFlag.storage = UserDefaults(suiteName: Scaffolding.defaultsSuiteName)! - var flag = DeveloperFlag.enableMLSSupport - flag.isOn = true } - + override func tearDown() async throws { try await super.tearDown() coreDataStack = nil sut = nil modelHelper = nil try coreDataStackHelper.cleanupDirectory() - DeveloperFlag.storage.removePersistentDomain(forName: Scaffolding.defaultsSuiteName) coreDataStackHelper = nil userRepository = nil conversationsRepository = nil mlsService = nil } - + func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Zero() async throws { // Given - - let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls let mlsEpoch: UInt64 = 0 - + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( selfUserProtocol: commonProtocol, userProtocol: commonProtocol, mlsEpoch: mlsEpoch ) - + // Mock - + setupMock( selfUser: selfUser, user: user, mlsOneOnOneConversation: mlsOneOnOneConversation ) - + // When - + try await sut.invoke() - + // Then - + XCTAssertEqual(mlsService.establishGroupForWithRemovalKeys_Invocations.count, 1) let createGroupInvocation = try XCTUnwrap( mlsService.establishGroupForWithRemovalKeys_Invocations.first ) - + XCTAssertEqual(createGroupInvocation.groupID, Scaffolding.mlsGroupID) XCTAssertEqual( createGroupInvocation.users, @@ -117,141 +112,137 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) } - + func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Not_Zero() async throws { // Given - - let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls let mlsEpoch: UInt64 = 1 - + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( selfUserProtocol: commonProtocol, userProtocol: commonProtocol, mlsEpoch: mlsEpoch ) - + // Mock - + setupMock( selfUser: selfUser, user: user, mlsOneOnOneConversation: mlsOneOnOneConversation ) - + // When - + try await sut.invoke() - + // Then - + XCTAssertEqual(mlsService.joinGroupWith_Invocations.count, 1) let invokedMLSGroupID = try XCTUnwrap(mlsService.joinGroupWith_Invocations.first) XCTAssertEqual(invokedMLSGroupID, Scaffolding.mlsGroupID) XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) } - + func testProcessEvent_It_Migrates_Proteus_Messages_To_MLS_Conversation() async throws { // Given - - let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.mls - + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( selfUserProtocol: commonProtocol, userProtocol: commonProtocol ) - + // Mock - + setupMock( selfUser: selfUser, user: user, mlsOneOnOneConversation: mlsOneOnOneConversation ) - + // When - + try await sut.invoke() - + // Then - + let migratedMessagesTexts = mlsOneOnOneConversation.allMessages .compactMap(\.textMessageData) .compactMap(\.messageText) .sorted() - + /// Ensuring proteus messages were migrated to MLS conversation. XCTAssertEqual(migratedMessagesTexts.first, "Hello") XCTAssertEqual(migratedMessagesTexts.last, "World!") } - + func testProcessEvent_It_Resolves_Proteus_Conversation() async throws { // Given - - let connectionStatus = ConnectionStatus.accepted + let commonProtocol = WireDataModel.MessageProtocol.proteus - + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( selfUserProtocol: commonProtocol, userProtocol: commonProtocol ) - + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) - + // Mock - + setupMock( selfUser: selfUser, user: user, mlsOneOnOneConversation: mlsOneOnOneConversation ) - + // When - + try await sut.invoke() - + // Then - + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) } - + func testProcessEvent_It_Resolves_Conversation_With_No_Common_Protocol() async throws { // Given - - let connectionStatus = ConnectionStatus.accepted + let forcedReadOnly = false - + let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( selfUserProtocol: .mls, userProtocol: .proteus, forcedReadOnly: forcedReadOnly ) - + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) - + // Mock - + setupMock( selfUser: selfUser, user: user, mlsOneOnOneConversation: mlsOneOnOneConversation ) - + // When - + try await sut.invoke() - + // Then - + let lastMessage = try XCTUnwrap(user.oneOnOneConversation?.lastMessage as? ZMSystemMessage) XCTAssertEqual(lastMessage.systemMessageType, .mlsNotSupportedOtherUser) XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) } - + // MARK: - Setup - + private func setupManagedObjects( selfUserProtocol: WireDataModel.MessageProtocol, userProtocol: WireDataModel.MessageProtocol, @@ -265,28 +256,28 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { domain: Scaffolding.receiverQualifiedID.domain, in: context ) - + user.supportedProtocols = [userProtocol] - + let selfUser = modelHelper.createSelfUser( id: UUID(), domain: nil, in: context ) - + selfUser.supportedProtocols = [selfUserProtocol] - + let proteusConversation = modelHelper.createOneOnOne( with: selfUser, in: context ) - + proteusConversation.isForcedReadOnly = forcedReadOnly user.oneOnOneConversation = proteusConversation - + try proteusConversation.appendText(content: "Hello") try proteusConversation.appendText(content: "World!") - + let mlsOneOnOneConversation = modelHelper.createMLSConversation( mlsGroupID: Scaffolding.mlsGroupID, mlsStatus: .pendingJoin, @@ -294,10 +285,10 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { epoch: mlsEpoch, in: context ) - + return (selfUser, user, mlsOneOnOneConversation) } - + private func setupMock( selfUser: ZMUser, user: ZMUser, @@ -306,15 +297,16 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { ) { userRepository.fetchUserWithDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser - + userRepository.fetchAllUserIdsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] + conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = Scaffolding.conversationID.uuidString conversationsRepository.fetchMLSConversationWith_MockValue = mlsOneOnOneConversation - + mlsService.establishGroupForWithRemovalKeys_MockValue = Scaffolding.ciphersuite mlsService.conversationExistsGroupID_MockValue = mlsConversationExists mlsService.joinGroupWith_MockMethod = { _ in } } - + private func setupConnection(status: ConnectionStatus) -> Connection { Connection( senderID: Scaffolding.senderID, @@ -326,7 +318,7 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { status: status ) } - + private enum Scaffolding { static let username = "username" static let senderID = UUID() @@ -340,16 +332,16 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { uuid: conversationID, domain: "domain.com" ) - + static let base64EncodedString = "pQABARn//wKhAFggHsa0CszLXYLFcOzg8AA//E1+Dl1rDHQ5iuk44X0/PNYDoQChAFgg309rkhG6SglemG6kWae81P1HtQPx9lyb6wExTovhU4cE9g==" - + static let ciphersuite = WireDataModel.MLSCipherSuite.MLS_256_DHKEMP521_AES256GCM_SHA512_P521 - + static let mlsGroupID = WireDataModel.MLSGroupID( base64Encoded: base64EncodedString )! - + static let defaultsSuiteName = UUID().uuidString } - + } From aeed41e4d0ff7017c087fc9887692802e663832b Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:08:59 +0200 Subject: [PATCH 06/18] factor out fetchAllUserIdsWithOneOnOneConversation method to user repo, add UT - WPB-10727 --- .../Repositories/User/UserRepository.swift | 48 ++++++++++++++----- .../Repositories/UserRepositoryTests.swift | 32 ++++++++++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift index a2a33a7977b..0c3faf036ab 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift @@ -27,9 +27,9 @@ import WireDataModel /// of domain models, concealing how and where the models are stored /// as well as the possible source(s) of the models. public protocol UserRepositoryProtocol { - + /// Pulls self user and stores it locally - + func pullSelfUser() async throws /// Fetch self user from the local store @@ -113,6 +113,11 @@ public protocol UserRepositoryProtocol { func deleteUserAccount(for user: ZMUser, at date: Date) async + /// Fetches all user IDs that have a one on one conversation + /// - returns: A list of users' qualified IDs. + + func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] + } public final class UserRepository: UserRepositoryProtocol { @@ -139,10 +144,10 @@ public final class UserRepository: UserRepositoryProtocol { } // MARK: - Public - + public func pullSelfUser() async throws { let selfUser = try await selfUserAPI.getSelfUser() - + await context.perform { [self] in persistSelfUser(from: selfUser) } @@ -334,6 +339,26 @@ public final class UserRepository: UserRepositoryProtocol { } } + public func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + try await context.perform { [context] in + let request = NSFetchRequest(entityName: ZMUser.entityName()) + let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation)) + request.predicate = predicate + + return try context + .fetch(request) + .compactMap { user in + guard let userID = user.qualifiedID else { + WireLogger.conversation.error( + "Missing user's qualifiedID" + ) + return nil + } + return userID + } + } + } + // MARK: - Private private func persistUser(from user: WireAPI.User) { @@ -342,10 +367,10 @@ public final class UserRepository: UserRepositoryProtocol { domain: user.id.domain, in: context ) - + let previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key let completeProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key - + updateUserMetadata( persistedUser, deleted: user.deleted == true, @@ -362,14 +387,14 @@ public final class UserRepository: UserRepositoryProtocol { supportedProtocols: user.supportedProtocols?.toDomainModel() ?? [.proteus] ) } - + private func persistSelfUser( from selfUser: WireAPI.SelfUser ) { let persistedSelfUser = ZMUser.selfUser(in: context) let previewProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .preview })?.key let completeProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .complete })?.key - + updateUserMetadata( persistedSelfUser, deleted: selfUser.deleted == true, @@ -385,12 +410,12 @@ public final class UserRepository: UserRepositoryProtocol { providerIdentifier: selfUser.service?.provider.transportString(), supportedProtocols: selfUser.supportedProtocols?.toDomainModel() ?? [.proteus] ) - + persistedSelfUser.remoteIdentifier = selfUser.qualifiedID.uuid persistedSelfUser.domain = selfUser.qualifiedID.domain persistedSelfUser.managedBy = selfUser.managedBy?.rawValue } - + private func updateUserMetadata( _ user: ZMUser, deleted: Bool, @@ -406,11 +431,10 @@ public final class UserRepository: UserRepositoryProtocol { providerIdentifier: String?, supportedProtocols: Set ) { - guard deleted == false else { return user.markAccountAsDeleted(at: .now) } - + user.name = name user.handle = handle user.teamIdentifier = teamID diff --git a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift index 2582b9178b3..d703c81e9b1 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift @@ -323,18 +323,17 @@ class UserRepositoryTests: XCTestCase { XCTAssertEqual(user.isAccountDeleted, true) XCTAssertEqual(conversationsRepository.removeFromConversationsUserRemovalDate_Invocations.count, 1) } - + func testPullSelfUser() async throws { - // Mock selfUsersAPI.getSelfUser_MockValue = Scaffolding.selfUser // When try await sut.pullSelfUser() - + // Then - + await context.perform { [context] in let selfUser = ZMUser.selfUser(in: context) XCTAssertEqual(selfUser.remoteIdentifier, Scaffolding.selfUser.id) @@ -344,7 +343,28 @@ class UserRepositoryTests: XCTestCase { XCTAssertEqual(selfUser.emailAddress, Scaffolding.selfUser.email) XCTAssertEqual(selfUser.supportedProtocols, [.mls]) } - + } + + func testFetchAllUserIdsWithOneOnOneConversation() async throws { + // Given + + let user = modelHelper.createUser( + qualifiedID: Scaffolding.qualifiedID.toDomainModel(), + in: context + ) + + let oneOnOneConversation = modelHelper.createOneOnOne( + with: user, + in: context + ) + + // When + + let userIds = try await sut.fetchAllUserIdsWithOneOnOneConversation() + + // Then + + XCTAssertEqual(userIds, [Scaffolding.qualifiedID.toDomainModel()]) } private enum Scaffolding { @@ -388,7 +408,7 @@ class UserRepositoryTests: XCTestCase { supportedProtocols: [.mls], legalholdStatus: .disabled ) - + static let selfUser = SelfUser( id: qualifiedID.uuid, qualifiedID: qualifiedID, From 79ab5abd99f212af6d35c47b3b6a72f9ff8bde19 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:39:21 +0200 Subject: [PATCH 07/18] remove public access from oneOnOneResolver, add MLSProvider struct, fix UTs - WPB-10727 (#1999) --- .../project.pbxproj | 28 +++++----- .../OneOnOneResolver.swift} | 27 +++++----- .../Synchronization/SyncManager.swift | 36 +++++++++++-- .../generated/AutoMockable.generated.swift | 11 ++-- .../OneOnOneResolverTests.swift} | 9 ++-- .../Synchronization/SyncManagerTests.swift | 51 ++++++++++++++++--- .../PushSupportedProtocolsUseCaseTests.swift | 0 7 files changed, 111 insertions(+), 51 deletions(-) rename WireDomain/Sources/WireDomain/{UseCases/OneOnOneResolverUseCase.swift => Synchronization/OneOnOneResolver.swift} (94%) rename WireDomain/Tests/WireDomainTests/{UseCases/OneOnOneResolverUseCaseTests.swift => Synchronization/OneOnOneResolverTests.swift} (98%) rename WireDomain/Tests/WireDomainTests/{Synchronization => UseCases}/PushSupportedProtocolsUseCaseTests.swift (100%) diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj index e7beb351b32..a5c580bdff7 100644 --- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj @@ -23,9 +23,10 @@ C93961922C91B12800EA971A /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0E117D2C2C4080004BBD29 /* TestError.swift */; }; C93961932C91B15B00EA971A /* ConversationRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */; }; C97BCCAA2C98704B004F2D0D /* WireDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */; }; - C97C01412CAEBC6F000683C5 /* OneOnOneResolverUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */; }; C97C01442CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01432CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift */; }; - C97C01472CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */; }; + C97C019C2CB66B9E000683C5 /* OneOnOneResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C019B2CB66B9E000683C5 /* OneOnOneResolverTests.swift */; }; + C97C019F2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C019E2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift */; }; + C97C01A12CB66BFF000683C5 /* OneOnOneResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */; }; C99322D22C986E3A0065E10F /* TeamRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B22C986E3A0065E10F /* TeamRepository.swift */; }; C99322D32C986E3A0065E10F /* TeamRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */; }; C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B52C986E3A0065E10F /* SelfUserProvider.swift */; }; @@ -61,7 +62,6 @@ C9E8A3B72C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3B62C749F2A0093DD5C /* ConversationLabelsRepositoryTests.swift */; }; C9E8A3C02C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A3BF2C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift */; }; C9EA769F2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */; }; - C9EA76A12C93104C00A7D35C /* PushSupportedProtocolsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9EA76A02C93104C00A7D35C /* PushSupportedProtocolsUseCaseTests.swift */; }; CB7979132C738508006FBA58 /* WireTransportSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB7979122C738508006FBA58 /* WireTransportSupport.framework */; }; CB7979162C738547006FBA58 /* TestSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7979152C738547006FBA58 /* TestSetup.swift */; }; EE368CCE2C2DAA87009DBAB0 /* ConversationEventProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE368CC72C2DAA87009DBAB0 /* ConversationEventProcessor.swift */; }; @@ -145,9 +145,10 @@ 01D0DCE92C1C8EA10076CB1C /* WireDomain.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = WireDomain.docc; sourceTree = ""; }; 01D0DCFC2C1C8F9B0076CB1C /* WireDomain.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = WireDomain.xctestplan; sourceTree = ""; }; 1623564F2C2B223100C6666C /* UserRepositoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRepositoryTests.swift; sourceTree = ""; }; - C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverUseCase.swift; sourceTree = ""; }; C97C01432CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeatureConfigRepositoryProtocol.swift; sourceTree = ""; }; - C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverUseCaseTests.swift; sourceTree = ""; }; + C97C019B2CB66B9E000683C5 /* OneOnOneResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverTests.swift; sourceTree = ""; }; + C97C019E2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCaseTests.swift; sourceTree = ""; }; + C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolver.swift; sourceTree = ""; }; C99322B22C986E3A0065E10F /* TeamRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepository.swift; sourceTree = ""; }; C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepositoryError.swift; sourceTree = ""; }; C99322B52C986E3A0065E10F /* SelfUserProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfUserProvider.swift; sourceTree = ""; }; @@ -183,7 +184,6 @@ C9E8A3BF2C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureConfigRepositoryTests.swift; sourceTree = ""; }; C9E8A3E72C7F6EA40093DD5C /* ConversationRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationRepositoryTests.swift; sourceTree = ""; }; C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCase.swift; sourceTree = ""; }; - C9EA76A02C93104C00A7D35C /* PushSupportedProtocolsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCaseTests.swift; sourceTree = ""; }; CB7979122C738508006FBA58 /* WireTransportSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireTransportSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CB7979152C738547006FBA58 /* TestSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSetup.swift; sourceTree = ""; }; EE0E117D2C2C4080004BBD29 /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = ""; }; @@ -292,7 +292,7 @@ 017F679A2C20801800B6E02D /* WireDomain */ = { isa = PBXGroup; children = ( - C97C01452CAEC8AF000683C5 /* UseCases */, + C97C019D2CB66BAB000683C5 /* UseCases */, C9C8FDCD2C9DBE0E00702B91 /* Event Processing */, EEC410252C60D48900E89394 /* Synchronization */, EE0E117C2C2C4076004BBD29 /* Helpers */, @@ -415,10 +415,10 @@ path = Mock; sourceTree = ""; }; - C97C01452CAEC8AF000683C5 /* UseCases */ = { + C97C019D2CB66BAB000683C5 /* UseCases */ = { isa = PBXGroup; children = ( - C97C01462CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift */, + C97C019E2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift */, ); path = UseCases; sourceTree = ""; @@ -536,7 +536,6 @@ C9EA76AD2C98548900A7D35C /* UseCases */ = { isa = PBXGroup; children = ( - C97C01402CAEBC66000683C5 /* OneOnOneResolverUseCase.swift */, C9EA769E2C92DD0F00A7D35C /* PushSupportedProtocolsUseCase.swift */, ); path = UseCases; @@ -659,8 +658,8 @@ EEC410252C60D48900E89394 /* Synchronization */ = { isa = PBXGroup; children = ( + C97C019B2CB66B9E000683C5 /* OneOnOneResolverTests.swift */, EEC410242C60D48900E89394 /* SyncManagerTests.swift */, - C9EA76A02C93104C00A7D35C /* PushSupportedProtocolsUseCaseTests.swift */, ); path = Synchronization; sourceTree = ""; @@ -668,6 +667,7 @@ EECC35A42C2EB6C100679448 /* Synchronization */ = { isa = PBXGroup; children = ( + C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */, EECC35A52C2EB6CD00679448 /* SyncManager.swift */, EECC35A72C2EB70400679448 /* SyncState.swift */, ); @@ -879,7 +879,6 @@ 0163EE812C20D71C00B37260 /* WireDomain.docc in Sources */, EEAD0A2E2C46B01900CC8658 /* UserUpdateEventProcessor.swift in Sources */, EEAD0A2A2C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift in Sources */, - C97C01412CAEBC6F000683C5 /* OneOnOneResolverUseCase.swift in Sources */, C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */, EEAD0A332C46B99800CC8658 /* FederationDeleteEventProcessor.swift in Sources */, C99322E42C986E3A0065E10F /* ConversationRepository.swift in Sources */, @@ -890,6 +889,7 @@ EEAD09FC2C46773900CC8658 /* ConversationMemberLeaveEventProcessor.swift in Sources */, EE57A7082C2A8B740096F242 /* ProteusMessageDecryptorError.swift in Sources */, EEAD0A1A2C46A92000CC8658 /* UserClientRemoveEventProcessor.swift in Sources */, + C97C01A12CB66BFF000683C5 /* OneOnOneResolver.swift in Sources */, EEAD0A0A2C46776B00CC8658 /* ConversationReceiptModeUpdateEventProcessor.swift in Sources */, C99322D62C986E3A0065E10F /* UserRepository.swift in Sources */, EEAD0A002C46774900CC8658 /* ConversationMessageTimerUpdateEventProcessor.swift in Sources */, @@ -944,9 +944,10 @@ C93961922C91B12800EA971A /* TestError.swift in Sources */, EEC410262C60D48900E89394 /* SyncManagerTests.swift in Sources */, C9C8FDD32C9DBE0E00702B91 /* UserLegalHoldDisableEventProcessorTests.swift in Sources */, - C97C01472CAEC8D4000683C5 /* OneOnOneResolverUseCaseTests.swift in Sources */, + C97C019C2CB66B9E000683C5 /* OneOnOneResolverTests.swift in Sources */, EE57A7032C2994420096F242 /* UpdateEventsRepositoryTests.swift in Sources */, C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */, + C97C019F2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift in Sources */, C9E8A3C02C761EDD0093DD5C /* FeatureConfigRepositoryTests.swift in Sources */, C97C01442CAEC689000683C5 /* MockFeatureConfigRepositoryProtocol.swift in Sources */, CB7979162C738547006FBA58 /* TestSetup.swift in Sources */, @@ -956,7 +957,6 @@ C9E8A3AE2C73878B0093DD5C /* ConnectionsRepositoryTests.swift in Sources */, C9C8FDD22C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift in Sources */, C9C8FDD12C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift in Sources */, - C9EA76A12C93104C00A7D35C /* PushSupportedProtocolsUseCaseTests.swift in Sources */, C9C8FDCE2C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift in Sources */, C9C8FDCF2C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift in Sources */, C9C8FDD42C9DBE0E00702B91 /* UserLegalholdRequestEventProcessorTests.swift in Sources */, diff --git a/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift similarity index 94% rename from WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift rename to WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift index 842d8a54e00..a01a67e32c4 100644 --- a/WireDomain/Sources/WireDomain/UseCases/OneOnOneResolverUseCase.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift @@ -22,11 +22,11 @@ import WireDataModel // sourcery: AutoMockable /// Resolves 1:1 conversations -public protocol OneOnOneResolverUseCaseProtocol { +protocol OneOnOneResolverProtocol { func invoke() async throws } -public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { +struct OneOnOneResolver: OneOnOneResolverProtocol { private enum OneOnOneResolverUseCaseError: Error { case failedToActivateConversation @@ -39,28 +39,23 @@ public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { private let context: NSManagedObjectContext private let userRepository: any UserRepositoryProtocol private let conversationsRepository: any ConversationRepositoryProtocol - private let mlsService: any MLSServiceInterface - private let isMLSEnabled: Bool + private let mlsProvider: MLSProvider // MARK: - Object lifecycle - public init( + init( context: NSManagedObjectContext, userRepository: any UserRepositoryProtocol, conversationsRepository: any ConversationRepositoryProtocol, - mlsService: any MLSServiceInterface, - isMLSEnabled: Bool + mlsProvider: MLSProvider ) { self.context = context self.userRepository = userRepository self.conversationsRepository = conversationsRepository - self.mlsService = mlsService - self.isMLSEnabled = isMLSEnabled + self.mlsProvider = mlsProvider } - // MARK: - Public - - public func invoke() async throws { + func invoke() async throws { try await resolveAllOneOnOneConversations() } @@ -95,13 +90,13 @@ public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { let selfUser = userRepository.fetchSelfUser() let commonProtocol = getCommonProtocol(between: selfUser, and: user) - if isMLSEnabled, commonProtocol == .mls { + if mlsProvider.isMLSEnabled, commonProtocol == .mls { try await resolveMLSConversation( for: user ) } - if isMLSEnabled, commonProtocol == nil { + if mlsProvider.isMLSEnabled, commonProtocol == nil { await resolveNoCommonProtocolConversation( between: selfUser, and: user @@ -135,6 +130,8 @@ public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { throw OneOnOneResolverUseCaseError.failedToFetchConversation } + let mlsService = mlsProvider.service + /// If conversation already exists, there is no need to perform a migration. let needsMLSMigration = try await mlsService.conversationExists( groupID: groupID @@ -190,6 +187,8 @@ public struct OneOnOneResolverUseCase: OneOnOneResolverUseCaseProtocol { groupID: MLSGroupID, userID: WireDataModel.QualifiedID ) async throws { + let mlsService = mlsProvider.service + if mlsConversation.epoch == 0 { let users = [MLSUser(userID)] diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index b4ec3190e09..7b01b61e25d 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -16,8 +16,10 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import CoreData import Foundation import WireAPI +import WireDataModel import WireSystem protocol SyncManagerProtocol { @@ -36,6 +38,19 @@ protocol SyncManagerProtocol { } +public struct MLSProvider { + let service: any MLSServiceInterface + let isMLSEnabled: Bool + + public init( + service: any MLSServiceInterface, + isMLSEnabled: Bool + ) { + self.service = service + self.isMLSEnabled = isMLSEnabled + } +} + final class SyncManager: SyncManagerProtocol { // MARK: - Properties @@ -53,7 +68,8 @@ final class SyncManager: SyncManagerProtocol { private let conversationLabelsRepository: any ConversationLabelsRepositoryProtocol private let featureConfigsRepository: any FeatureConfigRepositoryProtocol private let pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol - private let oneOnOneResolverUseCase: any OneOnOneResolverUseCaseProtocol + private let mlsProvider: MLSProvider + private let context: NSManagedObjectContext // MARK: - Update event processor @@ -71,7 +87,8 @@ final class SyncManager: SyncManagerProtocol { featureConfigsRepository: any FeatureConfigRepositoryProtocol, updateEventProcessor: any UpdateEventProcessorProtocol, pushSupportedProtocolsUseCase: any PushSupportedProtocolsUseCaseProtocol, - oneOnOneResolverUseCase: any OneOnOneResolverUseCaseProtocol + mlsProvider: MLSProvider, + context: NSManagedObjectContext ) { self.updateEventsRepository = updateEventsRepository self.teamRepository = teamRepository @@ -82,7 +99,8 @@ final class SyncManager: SyncManagerProtocol { self.featureConfigsRepository = featureConfigsRepository self.updateEventProcessor = updateEventProcessor self.pushSupportedProtocolsUseCase = pushSupportedProtocolsUseCase - self.oneOnOneResolverUseCase = oneOnOneResolverUseCase + self.mlsProvider = mlsProvider + self.context = context } func performSlowSync() async throws { @@ -98,7 +116,17 @@ final class SyncManager: SyncManagerProtocol { try await conversationLabelsRepository.pullConversationLabels() try await featureConfigsRepository.pullFeatureConfigs() try await pushSupportedProtocolsUseCase.invoke() - try await oneOnOneResolverUseCase.invoke() + let oneOnOneResolver = makeOneOnOneResolver() + try await oneOnOneResolver.invoke() + } + + private func makeOneOnOneResolver() -> OneOnOneResolverProtocol { + OneOnOneResolver( + context: context, + userRepository: userRepository, + conversationsRepository: conversationsRepository, + mlsProvider: mlsProvider + ) } func performQuickSync() async throws { diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 22c5a75a75e..14de6abab4a 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -285,20 +285,19 @@ public class MockConversationRepositoryProtocol: ConversationRepositoryProtocol } -public class MockOneOnOneResolverUseCaseProtocol: OneOnOneResolverUseCaseProtocol { +class MockOneOnOneResolverProtocol: OneOnOneResolverProtocol { // MARK: - Life cycle - public init() {} // MARK: - invoke - public var invoke_Invocations: [Void] = [] - public var invoke_MockError: Error? - public var invoke_MockMethod: (() async throws -> Void)? + var invoke_Invocations: [Void] = [] + var invoke_MockError: Error? + var invoke_MockMethod: (() async throws -> Void)? - public func invoke() async throws { + func invoke() async throws { invoke_Invocations.append(()) if let error = invoke_MockError { diff --git a/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift similarity index 98% rename from WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift rename to WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index e9158da064f..2ddcd72dff1 100644 --- a/WireDomain/Tests/WireDomainTests/UseCases/OneOnOneResolverUseCaseTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -26,8 +26,8 @@ import XCTest @testable import WireDomain -final class OneOnOneResolverUseCaseTests: XCTestCase { - var sut: OneOnOneResolverUseCase! +final class OneOnOneResolverTests: XCTestCase { + var sut: WireDomain.OneOnOneResolver! var coreDataStack: CoreDataStack! var coreDataStackHelper: CoreDataStackHelper! @@ -48,12 +48,11 @@ final class OneOnOneResolverUseCaseTests: XCTestCase { userRepository = MockUserRepositoryProtocol() conversationsRepository = MockConversationRepositoryProtocol() mlsService = MockMLSServiceInterface() - sut = OneOnOneResolverUseCase( + sut = OneOnOneResolver( context: context, userRepository: userRepository, conversationsRepository: conversationsRepository, - mlsService: mlsService, - isMLSEnabled: true + mlsProvider: MLSProvider(service: mlsService, isMLSEnabled: true) ) } diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index 2f6c36e91f6..ecb56b94d49 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -19,14 +19,18 @@ import Combine import WireAPI import WireAPISupport -import XCTest - +import WireDataModel +import WireDataModelSupport @testable import WireDomain @testable import WireDomainSupport +import XCTest final class SyncManagerTests: XCTestCase { private var sut: SyncManager! + private var coreDataStackHelper: CoreDataStackHelper! + private var stack: CoreDataStack! + private var modelHelper: ModelHelper! private var updateEventsRepository: MockUpdateEventsRepositoryProtocol! private var updateEventProcessor: MockUpdateEventProcessorProtocol! private var teamRepository: MockTeamRepositoryProtocol! @@ -36,10 +40,18 @@ final class SyncManagerTests: XCTestCase { private var conversationLabelsRepository: MockConversationLabelsRepositoryProtocol! private var featureConfigsRepository: MockFeatureConfigRepositoryProtocol! private var pushSupportedProtocolsUseCase: MockPushSupportedProtocolsUseCaseProtocol! - private var oneOnOneResolverUseCase: MockOneOnOneResolverUseCaseProtocol! + private var mlsService: MockMLSServiceInterface! + + var context: NSManagedObjectContext { + stack.syncContext + } override func setUp() async throws { try await super.setUp() + coreDataStackHelper = CoreDataStackHelper() + stack = try await coreDataStackHelper.createStack() + mlsService = MockMLSServiceInterface() + modelHelper = ModelHelper() updateEventsRepository = MockUpdateEventsRepositoryProtocol() updateEventProcessor = MockUpdateEventProcessorProtocol() teamRepository = MockTeamRepositoryProtocol() @@ -49,7 +61,6 @@ final class SyncManagerTests: XCTestCase { conversationLabelsRepository = MockConversationLabelsRepositoryProtocol() featureConfigsRepository = MockFeatureConfigRepositoryProtocol() pushSupportedProtocolsUseCase = MockPushSupportedProtocolsUseCaseProtocol() - oneOnOneResolverUseCase = MockOneOnOneResolverUseCaseProtocol() sut = SyncManager( updateEventsRepository: updateEventsRepository, @@ -61,7 +72,8 @@ final class SyncManagerTests: XCTestCase { featureConfigsRepository: featureConfigsRepository, updateEventProcessor: updateEventProcessor, pushSupportedProtocolsUseCase: pushSupportedProtocolsUseCase, - oneOnOneResolverUseCase: oneOnOneResolverUseCase + mlsProvider: MLSProvider(service: mlsService, isMLSEnabled: true), + context: context ) // Base mocks. @@ -77,6 +89,11 @@ final class SyncManagerTests: XCTestCase { override func tearDown() async throws { try await super.tearDown() sut = nil + modelHelper = nil + try coreDataStackHelper.cleanupDirectory() + coreDataStackHelper = nil + stack = nil + mlsService = nil updateEventsRepository = nil updateEventProcessor = nil teamRepository = nil @@ -86,7 +103,6 @@ final class SyncManagerTests: XCTestCase { conversationLabelsRepository = nil featureConfigsRepository = nil pushSupportedProtocolsUseCase = nil - oneOnOneResolverUseCase = nil } // MARK: - Tests @@ -317,7 +333,20 @@ final class SyncManagerTests: XCTestCase { } func testPerformSlowSync() async throws { + // Mock + + let user = await context.perform { [self] in + modelHelper.createUser(in: context) + } + + let selfUser = await context.perform { [self] in + modelHelper.createSelfUser(in: context) + } + + let conversation = await context.perform { [self] in + modelHelper.createGroupConversation(in: context) + } updateEventsRepository.pullLastEventID_MockMethod = {} teamRepository.pullSelfTeam_MockMethod = {} @@ -325,13 +354,20 @@ final class SyncManagerTests: XCTestCase { teamRepository.pullSelfTeamMembers_MockMethod = {} connectionsRepository.pullConnections_MockMethod = {} conversationsRepository.pullConversations_MockMethod = {} + conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = UUID().uuidString + conversationsRepository.fetchMLSConversationWith_MockValue = conversation userRepository.pullKnownUsers_MockMethod = {} conversationLabelsRepository.pullConversationLabels_MockMethod = {} featureConfigsRepository.pullFeatureConfigs_MockMethod = {} userRepository.pullSelfUser_MockMethod = {} teamRepository.pullSelfLegalHoldStatus_MockMethod = {} pushSupportedProtocolsUseCase.invoke_MockMethod = {} - oneOnOneResolverUseCase.invoke_MockMethod = {} + userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } + userRepository.fetchUserWithDomain_MockValue = user + userRepository.fetchSelfUser_MockValue = selfUser + mlsService.conversationExistsGroupID_MockValue = true + mlsService.establishGroupForWithRemovalKeys_MockValue = .MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + mlsService.joinGroupWith_MockMethod = { _ in } // When @@ -351,7 +387,6 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(userRepository.pullSelfUser_Invocations.count, 1) XCTAssertEqual(teamRepository.pullSelfLegalHoldStatus_Invocations.count, 1) XCTAssertEqual(pushSupportedProtocolsUseCase.invoke_Invocations.count, 1) - XCTAssertEqual(oneOnOneResolverUseCase.invoke_Invocations.count, 1) } } diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/PushSupportedProtocolsUseCaseTests.swift b/WireDomain/Tests/WireDomainTests/UseCases/PushSupportedProtocolsUseCaseTests.swift similarity index 100% rename from WireDomain/Tests/WireDomainTests/Synchronization/PushSupportedProtocolsUseCaseTests.swift rename to WireDomain/Tests/WireDomainTests/UseCases/PushSupportedProtocolsUseCaseTests.swift From 6808852586237e2f91c091ee3fd733489b1fb767 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:52:22 +0200 Subject: [PATCH 08/18] add internal test helper method - WPB-10727 (#1999) --- .../APIs/TeamsAPI/TeamsAPITests.swift | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index 65ed0a3862f..0e18fa0e257 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -279,33 +279,21 @@ final class TeamsAPITests: XCTestCase { } func testGetLegalhold_FailureResponse_InvalidRequest_V0() async throws { - // Given - let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "") - let sut = TeamsAPIV0(httpClient: httpClient) - - // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { - // When - try await sut.getLegalhold( - for: Team.ID(), - userID: UUID() - ) - } + try await internalTest_GetLegalhold_Failure( + expectedError: TeamsAPIError.invalidRequest, + for: .v0, + code: .notFound, + errorLabel: "" + ) } func testGetLegalhold_FailureResponse_MemberNotFound_V0() async throws { - // Given - let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "no-team-member") - let sut = TeamsAPIV0(httpClient: httpClient) - - // Then - await XCTAssertThrowsError(TeamsAPIError.teamMemberNotFound) { - // When - try await sut.getLegalhold( - for: Team.ID(), - userID: UUID() - ) - } + try await internalTest_GetLegalhold_Failure( + expectedError: TeamsAPIError.teamMemberNotFound, + for: .v0, + code: .notFound, + errorLabel: "no-team-member" + ) } // MARK: - V2 @@ -379,18 +367,12 @@ final class TeamsAPITests: XCTestCase { } func testGetLegalhold_FailureResponse_InvalidRequest_V4() async throws { - // Given - let httpClient = try HTTPClientMock(code: .badRequest, errorLabel: "") - let sut = TeamsAPIV4(httpClient: httpClient) - - // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { - // When - try await sut.getLegalhold( - for: Team.ID(), - userID: UUID() - ) - } + try await internalTest_GetLegalhold_Failure( + expectedError: TeamsAPIError.invalidRequest, + for: .v4, + code: .badRequest, + errorLabel: "" + ) } // MARK: - V5 @@ -408,12 +390,28 @@ final class TeamsAPITests: XCTestCase { } func testGetLegalhold_FailureResponse_InvalidRequest_V5() async throws { + try await internalTest_GetLegalhold_Failure( + expectedError: TeamsAPIError.invalidRequest, + for: .v5, + code: .notFound, + errorLabel: "" + ) + } + + private func internalTest_GetLegalhold_Failure( + expectedError: any Error & Equatable, + for apiVersion: APIVersion, + code: HTTPStatusCode, + errorLabel: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { // Given - let httpClient = try HTTPClientMock(code: .notFound, errorLabel: "") - let sut = TeamsAPIV5(httpClient: httpClient) - + let httpClient = try HTTPClientMock(code: code, errorLabel: errorLabel) + let sut = apiVersion.buildAPI(client: httpClient) + // Then - await XCTAssertThrowsError(TeamsAPIError.invalidRequest) { + await XCTAssertThrowsError(expectedError) { // When try await sut.getLegalhold( for: Team.ID(), @@ -423,3 +421,10 @@ final class TeamsAPITests: XCTestCase { } } + +private extension APIVersion { + func buildAPI(client: any HTTPClient) -> any TeamsAPI { + let builder = TeamsAPIBuilder(httpClient: client) + return builder.makeAPI(for: self) + } +} From 557391b8308dabd6c0db318885ad14a8cba7ef66 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:02:52 +0200 Subject: [PATCH 09/18] fix threading violation - WPB-10727 (#1999) --- .../Repositories/Team/TeamRepository.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift index 3d9f7b34188..7d90a162586 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift @@ -146,20 +146,24 @@ public final class TeamRepository: TeamRepositoryProtocol { } public func pullSelfLegalHoldStatus() async throws { - let selfUser = await context.perform { [userRepository] in - userRepository.fetchSelfUser() + let (selfUserID, selfClientID) = await context.perform { [userRepository] in + let selfUser = userRepository.fetchSelfUser() + let selfUserID: UUID = selfUser.remoteIdentifier + let selfClientID = selfUser.selfClient()?.remoteIdentifier + + return (selfUserID, selfClientID) } let selfUserLegalHold = try await fetchSelfLegalhold() switch selfUserLegalHold.status { case .pending: - guard let selfClientID = selfUser.selfClient()?.remoteIdentifier else { + guard let selfClientID else { return } await userRepository.addLegalHoldRequest( - for: selfUser.remoteIdentifier, + for: selfUserID, clientID: selfClientID, lastPrekey: selfUserLegalHold.prekey ) From 61c3ae462c4691583719fe7e8555249c90f3a7f7 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:13:16 +0200 Subject: [PATCH 10/18] add Sendable to Conversation value type objects - WPB-10727 (#1999) --- .../ConversationReceiptModeUpdateEventDecoder.swift | 2 +- .../WireAPI/Models/Conversation/ConversationMember.swift | 2 +- .../Models/Conversation/ConversationMemberChange.swift | 2 +- .../Models/Conversation/ConversationMemberLeaveReason.swift | 2 +- WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift | 2 +- .../ConversationEvent/ConversationAccessUpdateEvent.swift | 2 +- .../ConversationEvent/ConversationCodeUpdateEvent.swift | 2 +- .../ConversationEvent/ConversationCreateEvent.swift | 2 +- .../ConversationEvent/ConversationDeleteEvent.swift | 2 +- .../UpdateEvent/ConversationEvent/ConversationEvent.swift | 2 +- .../ConversationEvent/ConversationMLSMessageAddEvent.swift | 2 +- .../ConversationEvent/ConversationMLSWelcomeEvent.swift | 2 +- .../ConversationEvent/ConversationMemberJoinEvent.swift | 2 +- .../ConversationEvent/ConversationMemberLeaveEvent.swift | 2 +- .../ConversationEvent/ConversationMemberUpdateEvent.swift | 2 +- .../ConversationMessageTimerUpdateEvent.swift | 2 +- .../ConversationProteusMessageAddEvent.swift | 2 +- .../ConversationEvent/ConversationProtocolUpdateEvent.swift | 2 +- .../ConversationReceiptModeUpdateEvent.swift | 4 ++-- .../ConversationEvent/ConversationRenameEvent.swift | 2 +- .../ConversationEvent/ConversationTypingEvent.swift | 2 +- WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift | 2 +- WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift | 4 ++-- 23 files changed, 25 insertions(+), 25 deletions(-) diff --git a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift index aedb8128342..9c0c62cfd24 100644 --- a/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift +++ b/WireAPI/Sources/WireAPI/APIs/UpdateEventsAPI/Event decoding/Conversation/ConversationReceiptModeUpdateEventDecoder.swift @@ -41,7 +41,7 @@ struct ConversationReceiptModeUpdateEventDecoder { return ConversationReceiptModeUpdateEvent( conversationID: conversationID, senderID: senderID, - newRecieptMode: payload.receiptMode + newReceiptMode: payload.receiptMode ) } diff --git a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMember.swift b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMember.swift index ec89670e40b..e66fa76338e 100644 --- a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMember.swift +++ b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMember.swift @@ -22,7 +22,7 @@ public extension Conversation { /// Represents a conversation's member. - struct Member: Equatable, Codable { + struct Member: Equatable, Codable, Sendable { public let qualifiedID: QualifiedID? public let id: UUID? diff --git a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberChange.swift b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberChange.swift index 885ec14c0db..dd518d74e1e 100644 --- a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberChange.swift +++ b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberChange.swift @@ -20,7 +20,7 @@ import Foundation /// Changed metadata for a member of a conversation. -public struct ConversationMemberChange: Equatable, Codable { +public struct ConversationMemberChange: Equatable, Codable, Sendable { /// The id of the member. diff --git a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift index b7fe07c3506..176c2aecc3f 100644 --- a/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift +++ b/WireAPI/Sources/WireAPI/Models/Conversation/ConversationMemberLeaveReason.swift @@ -20,7 +20,7 @@ import Foundation /// The reason why a member was removed from a conversation. -public enum ConversationMemberLeaveReason: String, Codable { +public enum ConversationMemberLeaveReason: String, Codable, Sendable { /// The user has been removed from the team and therefore removed /// from all conversations. diff --git a/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift b/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift index 294b520f80c..c5096233f12 100644 --- a/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift +++ b/WireAPI/Sources/WireAPI/Models/Messaging/MessageContent.swift @@ -21,7 +21,7 @@ import Foundation /// The contents of a message, typically as a base-64 encoded /// Protobuf string. -public enum MessageContent: Equatable, Codable { +public enum MessageContent: Equatable, Codable, Sendable { /// Encrypted message content. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationAccessUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationAccessUpdateEvent.swift index 2a53de2e1c2..5346f657966 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationAccessUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationAccessUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the access settings of a conversation were updated. -public struct ConversationAccessUpdateEvent: Equatable, Codable { +public struct ConversationAccessUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift index 56917c512ce..feb85f6baf3 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCodeUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the conversation's guest link code was updated. -public struct ConversationCodeUpdateEvent: Equatable, Codable { +public struct ConversationCodeUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCreateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCreateEvent.swift index 4330159d5ff..0c9a69afbe4 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCreateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationCreateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where a new conversation was created. -public struct ConversationCreateEvent: Equatable, Codable { +public struct ConversationCreateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationDeleteEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationDeleteEvent.swift index 60a2a49320f..ba59384fa6d 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationDeleteEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationDeleteEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where a conversation was deleted. -public struct ConversationDeleteEvent: Equatable, Codable { +public struct ConversationDeleteEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift index ab3949715ad..b6b3fe94d87 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event concerning conversations. -public enum ConversationEvent: Equatable, Codable { +public enum ConversationEvent: Equatable, Codable, Sendable { /// A conversation's access settings were updated. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift index ce3aff250b8..cfe4d88ddf5 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSMessageAddEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where an mls message was received in a conversation. -public struct ConversationMLSMessageAddEvent: Equatable, Codable { +public struct ConversationMLSMessageAddEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift index fd8919a4f63..75ed4a0a214 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMLSWelcomeEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where an mls welcome message was received in a conversation. -public struct ConversationMLSWelcomeEvent: Equatable, Codable { +public struct ConversationMLSWelcomeEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberJoinEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberJoinEvent.swift index 24f7df2ccb0..f6c037e7f01 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberJoinEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberJoinEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where some participants were added to a conversation. -public struct ConversationMemberJoinEvent: Equatable, Codable { +public struct ConversationMemberJoinEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberLeaveEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberLeaveEvent.swift index 92c7bca139c..14e5b5b10e2 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberLeaveEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberLeaveEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where some participants were removed from a conversation. -public struct ConversationMemberLeaveEvent: Equatable, Codable { +public struct ConversationMemberLeaveEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberUpdateEvent.swift index 80616b72a90..6e632f50a7e 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMemberUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where some metadata changed for a member in a conversation. -public struct ConversationMemberUpdateEvent: Equatable, Codable { +public struct ConversationMemberUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift index 3dae5c6e6cc..20e87d8b54d 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationMessageTimerUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the message timer of a conversation was updated. -public struct ConversationMessageTimerUpdateEvent: Equatable, Codable { +public struct ConversationMessageTimerUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift index 7eb33c4c70d..373c87c1996 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProteusMessageAddEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where a proteus message was received in a conversation. -public struct ConversationProteusMessageAddEvent: Equatable, Codable { +public struct ConversationProteusMessageAddEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProtocolUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProtocolUpdateEvent.swift index 9c119b2d82b..1996d871f25 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProtocolUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationProtocolUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the message protocol was updated in a conversation. -public struct ConversationProtocolUpdateEvent: Equatable, Codable { +public struct ConversationProtocolUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift index ea11f318088..880ec2075df 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationReceiptModeUpdateEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the receipt mode of a conversation was updated. -public struct ConversationReceiptModeUpdateEvent: Equatable, Codable { +public struct ConversationReceiptModeUpdateEvent: Equatable, Codable, Sendable { /// The id of the conversation. @@ -35,6 +35,6 @@ public struct ConversationReceiptModeUpdateEvent: Equatable, Codable { /// A value of `1` indicates read reciepts are enabled /// and any other value indicates receipts are disabled. - public let newRecieptMode: Int + public let newReceiptMode: Int } diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift index 9f28e590038..079dfabc869 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationRenameEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where the conversation's name was changed. -public struct ConversationRenameEvent: Equatable, Codable { +public struct ConversationRenameEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift index 3e7f4977b2c..18998ce4cb6 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/ConversationEvent/ConversationTypingEvent.swift @@ -20,7 +20,7 @@ import Foundation /// An event where a user is typing in a conversation. -public struct ConversationTypingEvent: Equatable, Codable { +public struct ConversationTypingEvent: Equatable, Codable, Sendable { /// The id of the conversation. diff --git a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift index 66fe62bae1a..322c04b1215 100644 --- a/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift +++ b/WireAPI/Sources/WireAPI/Models/UpdateEvent/UpdateEvent.swift @@ -22,7 +22,7 @@ import Foundation /// that can be used to incrementaly update the state of /// the client. -public enum UpdateEvent: Equatable, Codable { +public enum UpdateEvent: Equatable, Codable, Sendable { /// A conversation event. diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index 0e18fa0e257..cf5f4f74d81 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -397,7 +397,7 @@ final class TeamsAPITests: XCTestCase { errorLabel: "" ) } - + private func internalTest_GetLegalhold_Failure( expectedError: any Error & Equatable, for apiVersion: APIVersion, @@ -409,7 +409,7 @@ final class TeamsAPITests: XCTestCase { // Given let httpClient = try HTTPClientMock(code: code, errorLabel: errorLabel) let sut = apiVersion.buildAPI(client: httpClient) - + // Then await XCTAssertThrowsError(expectedError) { // When From fccfda883c009a5f7fb8378d54b31a6873b24b0e Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:14:45 +0200 Subject: [PATCH 11/18] add SyncManager Error enum, add UT for slow sync failure - WPB-10727 (#1999) --- .../Repositories/Team/TeamRepository.swift | 2 +- .../Synchronization/SyncManager.swift | 38 +++-- .../Synchronization/SyncManagerTests.swift | 155 ++++++++++++------ 3 files changed, 126 insertions(+), 69 deletions(-) diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift index 7d90a162586..47036677e9c 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift @@ -150,7 +150,7 @@ public final class TeamRepository: TeamRepositoryProtocol { let selfUser = userRepository.fetchSelfUser() let selfUserID: UUID = selfUser.remoteIdentifier let selfClientID = selfUser.selfClient()?.remoteIdentifier - + return (selfUserID, selfClientID) } diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index 7b01b61e25d..9d997cc59cf 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -53,6 +53,10 @@ public struct MLSProvider { final class SyncManager: SyncManagerProtocol { + enum Error: Swift.Error { + case failedToPerformSlowSync(Swift.Error) + } + // MARK: - Properties private(set) var syncState: SyncState = .suspended @@ -104,20 +108,24 @@ final class SyncManager: SyncManagerProtocol { } func performSlowSync() async throws { - try await updateEventsRepository.pullLastEventID() - try await teamRepository.pullSelfTeam() - try await teamRepository.pullSelfTeamRoles() - try await teamRepository.pullSelfTeamMembers() - try await connectionsRepository.pullConnections() - try await conversationsRepository.pullConversations() - try await userRepository.pullKnownUsers() - try await userRepository.pullSelfUser() - try await teamRepository.pullSelfLegalHoldStatus() - try await conversationLabelsRepository.pullConversationLabels() - try await featureConfigsRepository.pullFeatureConfigs() - try await pushSupportedProtocolsUseCase.invoke() - let oneOnOneResolver = makeOneOnOneResolver() - try await oneOnOneResolver.invoke() + do { + try await updateEventsRepository.pullLastEventID() + try await teamRepository.pullSelfTeam() + try await teamRepository.pullSelfTeamRoles() + try await teamRepository.pullSelfTeamMembers() + try await connectionsRepository.pullConnections() + try await conversationsRepository.pullConversations() + try await userRepository.pullKnownUsers() + try await userRepository.pullSelfUser() + try await teamRepository.pullSelfLegalHoldStatus() + try await conversationLabelsRepository.pullConversationLabels() + try await featureConfigsRepository.pullFeatureConfigs() + try await pushSupportedProtocolsUseCase.invoke() + let oneOnOneResolver = makeOneOnOneResolver() + try await oneOnOneResolver.invoke() + } catch { + throw Error.failedToPerformSlowSync(error) + } } private func makeOneOnOneResolver() -> OneOnOneResolverProtocol { @@ -192,7 +200,7 @@ final class SyncManager: SyncManagerProtocol { isSuspending = false } - private var ongoingTask: Task? { + private var ongoingTask: Task? { switch syncState { case .quickSync(let task): task diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index ecb56b94d49..34e0dc931fb 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -332,10 +332,9 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(updateEventsRepository.stopReceivingLiveEvents_Invocations.count, 0) } - func testPerformSlowSync() async throws { - + func testPerformSlowSync_Success() async throws { // Mock - + let user = await context.perform { [self] in modelHelper.createUser(in: context) } @@ -389,57 +388,107 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(pushSupportedProtocolsUseCase.invoke_Invocations.count, 1) } -} + func testPerformSlowSync_Failure() async throws { + // Mock -private enum Scaffolding { - - static let localDomain = "example.com" - static let conversationID1 = ConversationID(uuid: UUID(), domain: localDomain) - static let conversationID2 = ConversationID(uuid: UUID(), domain: localDomain) - static let aliceID = UserID(uuid: UUID(), domain: localDomain) - - static let event1 = UpdateEvent.user(.clientAdd(UserClientAddEvent(client: UserClient( - id: "userClientID", - type: .permanent, - activationDate: .now, - capabilities: [.legalholdConsent] - )))) - - static let event2 = UpdateEvent.conversation(.typing(ConversationTypingEvent( - conversationID: conversationID1, - senderID: aliceID, - isTyping: true - ))) - - static let event3 = UpdateEvent.conversation(.delete(ConversationDeleteEvent( - conversationID: conversationID1, - senderID: aliceID, - timestamp: .now - ))) - - static let event4 = UpdateEvent.conversation(.rename(ConversationRenameEvent( - conversationID: conversationID2, - senderID: aliceID, - timestamp: .now, - newName: "Foo" - ))) - - static let event5 = UpdateEvent.conversation(.rename(ConversationRenameEvent( - conversationID: conversationID2, - senderID: aliceID, - timestamp: .now, - newName: "Bar" - ))) - - static func makeEnvelope( - with event: UpdateEvent, - isTransient: Bool = false - ) -> UpdateEventEnvelope { - .init( - id: UUID(), - events: [event], - isTransient: isTransient - ) + let user = await context.perform { [self] in + modelHelper.createUser(in: context) + } + + let (selfUser, selfUserID) = await context.perform { [self] in + let selfUser = modelHelper.createSelfUser(in: context) + let selfUserID: UUID = selfUser.remoteIdentifier + + return (selfUser, selfUserID) + } + + let conversation = await context.perform { [self] in + modelHelper.createGroupConversation(in: context) + } + + updateEventsRepository.pullLastEventID_MockMethod = {} + teamRepository.pullSelfTeam_MockMethod = {} + teamRepository.pullSelfTeamRoles_MockMethod = {} + teamRepository.pullSelfTeamMembers_MockMethod = {} + connectionsRepository.pullConnections_MockMethod = {} + conversationsRepository.pullConversations_MockMethod = {} + conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = UUID().uuidString + conversationsRepository.fetchMLSConversationWith_MockValue = conversation + userRepository.pullKnownUsers_MockMethod = {} + conversationLabelsRepository.pullConversationLabels_MockMethod = {} + featureConfigsRepository.pullFeatureConfigs_MockMethod = {} + userRepository.pullSelfUser_MockError = UserRepositoryError.failedToFetchUser(selfUserID) /// throws error + teamRepository.pullSelfLegalHoldStatus_MockMethod = {} + pushSupportedProtocolsUseCase.invoke_MockMethod = {} + userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } + userRepository.fetchUserWithDomain_MockValue = user + userRepository.fetchSelfUser_MockValue = selfUser + mlsService.conversationExistsGroupID_MockValue = true + mlsService.establishGroupForWithRemovalKeys_MockValue = .MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + mlsService.joinGroupWith_MockMethod = { _ in } + + do { + try await sut.performSlowSync() + } catch { + let syncError = try XCTUnwrap(error as? SyncManager.Error) + + switch syncError { + case .failedToPerformSlowSync(let error): + XCTAssertTrue(error is UserRepositoryError) + } + } } + private enum Scaffolding { + + static let localDomain = "example.com" + static let conversationID1 = ConversationID(uuid: UUID(), domain: localDomain) + static let conversationID2 = ConversationID(uuid: UUID(), domain: localDomain) + static let aliceID = UserID(uuid: UUID(), domain: localDomain) + + static let event1 = UpdateEvent.user(.clientAdd(UserClientAddEvent(client: UserClient( + id: "userClientID", + type: .permanent, + activationDate: .now, + capabilities: [.legalholdConsent] + )))) + + static let event2 = UpdateEvent.conversation(.typing(ConversationTypingEvent( + conversationID: conversationID1, + senderID: aliceID, + isTyping: true + ))) + + static let event3 = UpdateEvent.conversation(.delete(ConversationDeleteEvent( + conversationID: conversationID1, + senderID: aliceID, + timestamp: .now + ))) + + static let event4 = UpdateEvent.conversation(.rename(ConversationRenameEvent( + conversationID: conversationID2, + senderID: aliceID, + timestamp: .now, + newName: "Foo" + ))) + + static let event5 = UpdateEvent.conversation(.rename(ConversationRenameEvent( + conversationID: conversationID2, + senderID: aliceID, + timestamp: .now, + newName: "Bar" + ))) + + static func makeEnvelope( + with event: UpdateEvent, + isTransient: Bool = false + ) -> UpdateEventEnvelope { + .init( + id: UUID(), + events: [event], + isTransient: isTransient + ) + } + + } } From b598b9c704245248698dd77157a9462b0a806361 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:27:30 +0200 Subject: [PATCH 12/18] fix legalhold naming - WPB-10727 (#1999) --- .../WireAPI/APIs/TeamsAPI/TeamsAPI.swift | 4 +- .../WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift | 12 +++--- .../WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift | 6 +-- .../WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift | 6 +-- ...ld.swift => TeamMemberLegalholdInfo.swift} | 8 ++-- .../WireAPI/Models/User/LegalholdStatus.swift | 2 +- .../WireAPI/Models/UserClient/Prekey.swift | 2 +- .../APIs/TeamsAPI/TeamsAPITests.swift | 6 +-- .../PushChannel/PushChannelTests.swift | 2 +- .../ConversationEventDecodingTests.swift | 2 +- .../Repositories/Team/TeamRepository.swift | 17 ++++---- .../Synchronization/SyncManager.swift | 2 +- .../generated/AutoMockable.generated.swift | 40 +++++++++---------- .../Repositories/TeamRepositoryTests.swift | 10 ++--- .../Synchronization/SyncManagerTests.swift | 6 +-- 15 files changed, 64 insertions(+), 61 deletions(-) rename WireAPI/Sources/WireAPI/Models/Team/{TeamMemberLegalHold.swift => TeamMemberLegalholdInfo.swift} (84%) diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift index b15be126316..51fb3f1a354 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPI.swift @@ -58,9 +58,9 @@ public protocol TeamsAPI { /// - userID: The id of the member. /// - Returns: The legalhold of the member. - func getLegalhold( + func getLegalholdInfo( for teamID: Team.ID, userID: UUID - ) async throws -> TeamMemberLegalHold + ) async throws -> TeamMemberLegalholdInfo } diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift index 50116218b2c..85fb180df69 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV0.swift @@ -98,10 +98,10 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI { // MARK: - Get team member legalhold - func getLegalhold( + func getLegalholdInfo( for teamID: Team.ID, userID: UUID - ) async throws -> TeamMemberLegalHold { + ) async throws -> TeamMemberLegalholdInfo { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -110,7 +110,7 @@ class TeamsAPIV0: TeamsAPI, VersionedAPI { let response = try await httpClient.executeRequest(request) return try ResponseParser() - .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) + .success(code: .ok, type: TeamMemberLegalholdResponseV0.self) .failure(code: .notFound, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) @@ -315,7 +315,7 @@ struct LegalHoldLastPrekeyV0: Decodable, ToAPIModelConvertible { } } -struct TeamMemberLegalHoldResponseV0: Decodable, ToAPIModelConvertible { +struct TeamMemberLegalholdResponseV0: Decodable, ToAPIModelConvertible { let lastPrekey: LegalHoldLastPrekeyV0 let status: LegalholdStatusV0 @@ -325,8 +325,8 @@ struct TeamMemberLegalHoldResponseV0: Decodable, ToAPIModelConvertible { case lastPrekey = "last_prekey" } - func toAPIModel() -> TeamMemberLegalHold { - TeamMemberLegalHold( + func toAPIModel() -> TeamMemberLegalholdInfo { + TeamMemberLegalholdInfo( status: status.toAPIModel(), prekey: lastPrekey.toAPIModel() ) diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift index 47cabf87746..e0f2cd5e68a 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV4.swift @@ -91,10 +91,10 @@ class TeamsAPIV4: TeamsAPIV3 { // MARK: - Get team member legalhold - override func getLegalhold( + override func getLegalholdInfo( for teamID: Team.ID, userID: UUID - ) async throws -> TeamMemberLegalHold { + ) async throws -> TeamMemberLegalholdInfo { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -104,7 +104,7 @@ class TeamsAPIV4: TeamsAPIV3 { // New: 400 return try ResponseParser() - .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) + .success(code: .ok, type: TeamMemberLegalholdResponseV0.self) .failure(code: .badRequest, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) diff --git a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift index bfbd5d60371..715f95a78db 100644 --- a/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift +++ b/WireAPI/Sources/WireAPI/APIs/TeamsAPI/TeamsAPIV5.swift @@ -88,10 +88,10 @@ class TeamsAPIV5: TeamsAPIV4 { // MARK: - Get team member legalhold - override func getLegalhold( + override func getLegalholdInfo( for teamID: Team.ID, userID: UUID - ) async throws -> TeamMemberLegalHold { + ) async throws -> TeamMemberLegalholdInfo { let request = HTTPRequest( path: "\(basePath(for: teamID))/legalhold/\(userID.transportString())", method: .get @@ -101,7 +101,7 @@ class TeamsAPIV5: TeamsAPIV4 { // New: 404 invalid request. return try ResponseParser() - .success(code: .ok, type: TeamMemberLegalHoldResponseV0.self) + .success(code: .ok, type: TeamMemberLegalholdResponseV0.self) .failure(code: .notFound, error: TeamsAPIError.invalidRequest) .failure(code: .notFound, label: "no-team-member", error: TeamsAPIError.teamMemberNotFound) .parse(response) diff --git a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift similarity index 84% rename from WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift rename to WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift index dacf1296fd6..d302f697334 100644 --- a/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalHold.swift +++ b/WireAPI/Sources/WireAPI/Models/Team/TeamMemberLegalholdInfo.swift @@ -16,10 +16,10 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -public typealias LegalHoldPrekey = Prekey +public typealias LegalholdPrekey = Prekey /// The team member legal hold. -public struct TeamMemberLegalHold: Equatable, Sendable { +public struct TeamMemberLegalholdInfo: Equatable, Sendable { /// The legal hold status @@ -27,11 +27,11 @@ public struct TeamMemberLegalHold: Equatable, Sendable { /// The legal hold prekey - public let prekey: LegalHoldPrekey + public let prekey: LegalholdPrekey public init( status: LegalholdStatus, - prekey: LegalHoldPrekey + prekey: LegalholdPrekey ) { self.status = status self.prekey = prekey diff --git a/WireAPI/Sources/WireAPI/Models/User/LegalholdStatus.swift b/WireAPI/Sources/WireAPI/Models/User/LegalholdStatus.swift index 5accdcf5d0a..723d61fe8f4 100644 --- a/WireAPI/Sources/WireAPI/Models/User/LegalholdStatus.swift +++ b/WireAPI/Sources/WireAPI/Models/User/LegalholdStatus.swift @@ -20,7 +20,7 @@ import Foundation /// Represents all possible legalhold status for a user. -public enum LegalholdStatus { +public enum LegalholdStatus: Sendable { /// Legalhold is active for the user. diff --git a/WireAPI/Sources/WireAPI/Models/UserClient/Prekey.swift b/WireAPI/Sources/WireAPI/Models/UserClient/Prekey.swift index d06c828f3aa..48b634cc5b6 100644 --- a/WireAPI/Sources/WireAPI/Models/UserClient/Prekey.swift +++ b/WireAPI/Sources/WireAPI/Models/UserClient/Prekey.swift @@ -20,7 +20,7 @@ import Foundation /// A Proteus client prekey used to establish a Proteus session. -public struct Prekey: Equatable, Codable { +public struct Prekey: Equatable, Codable, Sendable { /// The prekey id. diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index cf5f4f74d81..79ab19bacaf 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -61,7 +61,7 @@ final class TeamsAPITests: XCTestCase { func testGetLegalholdRequest() async throws { try await apiSnapshotHelper.verifyRequestForAllAPIVersions { sut in - _ = try await sut.getLegalhold(for: .mockID1, userID: .mockID2) + _ = try await sut.getLegalholdInfo(for: .mockID1, userID: .mockID2) } } @@ -267,7 +267,7 @@ final class TeamsAPITests: XCTestCase { let sut = TeamsAPIV0(httpClient: httpClient) // When - let result = try await sut.getLegalhold( + let result = try await sut.getLegalholdInfo( for: Team.ID(), userID: UUID() ) @@ -413,7 +413,7 @@ final class TeamsAPITests: XCTestCase { // Then await XCTAssertThrowsError(expectedError) { // When - try await sut.getLegalhold( + try await sut.getLegalholdInfo( for: Team.ID(), userID: UUID() ) diff --git a/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift b/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift index 5dc14b566cf..6bff96d088f 100644 --- a/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift +++ b/WireAPI/Tests/WireAPITests/Network/PushChannel/PushChannelTests.swift @@ -205,7 +205,7 @@ private enum Scaffolding { static let receiptModeUpdateEvent = ConversationReceiptModeUpdateEvent( conversationID: conversationID, senderID: senderID, - newRecieptMode: 1 + newReceiptMode: 1 ) static let renameEvent = ConversationRenameEvent( diff --git a/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift b/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift index 052ef700fde..9ed04d00b7e 100644 --- a/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift +++ b/WireAPI/Tests/WireAPITests/UpdateEvent/ConversationEventDecodingTests.swift @@ -486,7 +486,7 @@ final class ConversationEventDecodingTests: XCTestCase { static let receiptModeUpdateEvent = ConversationReceiptModeUpdateEvent( conversationID: conversationID, senderID: senderID, - newRecieptMode: 1 + newReceiptMode: 1 ) static let renameEvent = ConversationRenameEvent( diff --git a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift index 47036677e9c..14a5fa6f6cd 100644 --- a/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/Team/TeamRepository.swift @@ -40,9 +40,10 @@ public protocol TeamRepositoryProtocol { func pullSelfTeamMembers() async throws - /// Fetch the legalhold for the self user from the server. + /// Fetches the legalhold info for the self user from the server. + /// - returns: The legalhold info. - func fetchSelfLegalhold() async throws -> TeamMemberLegalHold + func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo /// Deletes the member of a team. /// - Parameter userID: The ID of the team member. @@ -60,7 +61,9 @@ public protocol TeamRepositoryProtocol { func storeTeamMemberNeedsBackendUpdate(membershipID: UUID) async throws - func pullSelfLegalHoldStatus() async throws + /// Pulls and stores legalhold info locally. + + func pullSelfLegalholdInfo() async throws } @@ -145,7 +148,7 @@ public final class TeamRepository: TeamRepositoryProtocol { try await storeTeamMembersLocally(teamMembers) } - public func pullSelfLegalHoldStatus() async throws { + public func pullSelfLegalholdInfo() async throws { let (selfUserID, selfClientID) = await context.perform { [userRepository] in let selfUser = userRepository.fetchSelfUser() let selfUserID: UUID = selfUser.remoteIdentifier @@ -154,7 +157,7 @@ public final class TeamRepository: TeamRepositoryProtocol { return (selfUserID, selfClientID) } - let selfUserLegalHold = try await fetchSelfLegalhold() + let selfUserLegalHold = try await fetchSelfLegalholdInfo() switch selfUserLegalHold.status { case .pending: @@ -176,12 +179,12 @@ public final class TeamRepository: TeamRepositoryProtocol { } } - public func fetchSelfLegalhold() async throws -> TeamMemberLegalHold { + public func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo { let selfUserID: UUID = await context.perform { [userRepository] in userRepository.fetchSelfUser().remoteIdentifier } - return try await teamsAPI.getLegalhold( + return try await teamsAPI.getLegalholdInfo( for: selfTeamID, userID: selfUserID ) diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index 9d997cc59cf..a87eb2db8e6 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -117,7 +117,7 @@ final class SyncManager: SyncManagerProtocol { try await conversationsRepository.pullConversations() try await userRepository.pullKnownUsers() try await userRepository.pullSelfUser() - try await teamRepository.pullSelfLegalHoldStatus() + try await teamRepository.pullSelfLegalholdInfo() try await conversationLabelsRepository.pullConversationLabels() try await featureConfigsRepository.pullFeatureConfigs() try await pushSupportedProtocolsUseCase.invoke() diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 14de6abab4a..5863e8f0e56 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -467,26 +467,26 @@ public class MockTeamRepositoryProtocol: TeamRepositoryProtocol { try await mock() } - // MARK: - fetchSelfLegalhold + // MARK: - fetchSelfLegalholdInfo - public var fetchSelfLegalhold_Invocations: [Void] = [] - public var fetchSelfLegalhold_MockError: Error? - public var fetchSelfLegalhold_MockMethod: (() async throws -> TeamMemberLegalHold)? - public var fetchSelfLegalhold_MockValue: TeamMemberLegalHold? + public var fetchSelfLegalholdInfo_Invocations: [Void] = [] + public var fetchSelfLegalholdInfo_MockError: Error? + public var fetchSelfLegalholdInfo_MockMethod: (() async throws -> TeamMemberLegalholdInfo)? + public var fetchSelfLegalholdInfo_MockValue: TeamMemberLegalholdInfo? - public func fetchSelfLegalhold() async throws -> TeamMemberLegalHold { - fetchSelfLegalhold_Invocations.append(()) + public func fetchSelfLegalholdInfo() async throws -> TeamMemberLegalholdInfo { + fetchSelfLegalholdInfo_Invocations.append(()) - if let error = fetchSelfLegalhold_MockError { + if let error = fetchSelfLegalholdInfo_MockError { throw error } - if let mock = fetchSelfLegalhold_MockMethod { + if let mock = fetchSelfLegalholdInfo_MockMethod { return try await mock() - } else if let mock = fetchSelfLegalhold_MockValue { + } else if let mock = fetchSelfLegalholdInfo_MockValue { return mock } else { - fatalError("no mock for `fetchSelfLegalhold`") + fatalError("no mock for `fetchSelfLegalholdInfo`") } } @@ -530,21 +530,21 @@ public class MockTeamRepositoryProtocol: TeamRepositoryProtocol { try await mock(membershipID) } - // MARK: - pullSelfLegalHoldStatus + // MARK: - pullSelfLegalholdInfo - public var pullSelfLegalHoldStatus_Invocations: [Void] = [] - public var pullSelfLegalHoldStatus_MockError: Error? - public var pullSelfLegalHoldStatus_MockMethod: (() async throws -> Void)? + public var pullSelfLegalholdInfo_Invocations: [Void] = [] + public var pullSelfLegalholdInfo_MockError: Error? + public var pullSelfLegalholdInfo_MockMethod: (() async throws -> Void)? - public func pullSelfLegalHoldStatus() async throws { - pullSelfLegalHoldStatus_Invocations.append(()) + public func pullSelfLegalholdInfo() async throws { + pullSelfLegalholdInfo_Invocations.append(()) - if let error = pullSelfLegalHoldStatus_MockError { + if let error = pullSelfLegalholdInfo_MockError { throw error } - guard let mock = pullSelfLegalHoldStatus_MockMethod else { - fatalError("no mock for `pullSelfLegalHoldStatus`") + guard let mock = pullSelfLegalholdInfo_MockMethod else { + fatalError("no mock for `pullSelfLegalholdInfo`") } try await mock() diff --git a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift index 5878fe8c6e9..c94580d7488 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift @@ -239,13 +239,13 @@ final class TeamRepositoryTests: XCTestCase { func testFetchSelfLegalholdStatus() async throws { // Mock - teamsAPI.getLegalholdForUserID_MockValue = Scaffolding.teamMemberLegalHold + teamsAPI.getLegalholdInfoForUserID_MockValue = Scaffolding.teamMemberLegalhold // When - let result = try await sut.fetchSelfLegalhold() + let result = try await sut.fetchSelfLegalholdInfo() // Then - XCTAssertEqual(result, Scaffolding.teamMemberLegalHold) + XCTAssertEqual(result, Scaffolding.teamMemberLegalhold) } } @@ -273,10 +273,10 @@ private enum Scaffolding { static let member2legalholdStatus = LegalholdStatus.pending static let member2Permissions = Permissions.member.rawValue - static let teamMemberLegalHold = TeamMemberLegalHold( + static let teamMemberLegalhold = TeamMemberLegalholdInfo( status: .pending, prekey: prekey ) - static let prekey = LegalHoldPrekey(id: 2_330, base64EncodedKey: "foo") + static let prekey = LegalholdPrekey(id: 2_330, base64EncodedKey: "foo") } diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index 34e0dc931fb..ffeaf69dc81 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -359,7 +359,7 @@ final class SyncManagerTests: XCTestCase { conversationLabelsRepository.pullConversationLabels_MockMethod = {} featureConfigsRepository.pullFeatureConfigs_MockMethod = {} userRepository.pullSelfUser_MockMethod = {} - teamRepository.pullSelfLegalHoldStatus_MockMethod = {} + teamRepository.pullSelfLegalholdInfo_MockMethod = {} pushSupportedProtocolsUseCase.invoke_MockMethod = {} userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } userRepository.fetchUserWithDomain_MockValue = user @@ -384,7 +384,7 @@ final class SyncManagerTests: XCTestCase { XCTAssertEqual(conversationLabelsRepository.pullConversationLabels_Invocations.count, 1) XCTAssertEqual(featureConfigsRepository.pullFeatureConfigs_Invocations.count, 1) XCTAssertEqual(userRepository.pullSelfUser_Invocations.count, 1) - XCTAssertEqual(teamRepository.pullSelfLegalHoldStatus_Invocations.count, 1) + XCTAssertEqual(teamRepository.pullSelfLegalholdInfo_Invocations.count, 1) XCTAssertEqual(pushSupportedProtocolsUseCase.invoke_Invocations.count, 1) } @@ -418,7 +418,7 @@ final class SyncManagerTests: XCTestCase { conversationLabelsRepository.pullConversationLabels_MockMethod = {} featureConfigsRepository.pullFeatureConfigs_MockMethod = {} userRepository.pullSelfUser_MockError = UserRepositoryError.failedToFetchUser(selfUserID) /// throws error - teamRepository.pullSelfLegalHoldStatus_MockMethod = {} + teamRepository.pullSelfLegalholdInfo_MockMethod = {} pushSupportedProtocolsUseCase.invoke_MockMethod = {} userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } userRepository.fetchUserWithDomain_MockValue = user From 25021b3841574291cb480d46d50180fec40f9654 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:43:07 +0200 Subject: [PATCH 13/18] rename OneOnOneResolveError, remove invoke() method, create MLSProvider file, clean up code - WPB-10727 (#1999) --- .../Sources/WireAPI/Models/User/UserID.swift | 2 +- .../APIs/TeamsAPI/TeamsAPITests.swift | 24 +++++------ .../project.pbxproj | 4 ++ .../Repositories/User/UserRepository.swift | 4 +- .../Synchronization/MLSProvider.swift | 32 +++++++++++++++ .../Synchronization/OneOnOneResolver.swift | 22 ++++------ .../Synchronization/SyncManager.swift | 15 +------ .../generated/AutoMockable.generated.swift | 40 +++++++++---------- .../Repositories/TeamRepositoryTests.swift | 2 +- .../Repositories/UserRepositoryTests.swift | 2 +- .../OneOnOneResolverTests.swift | 12 +++--- .../Synchronization/SyncManagerTests.swift | 4 +- 12 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 WireDomain/Sources/WireDomain/Synchronization/MLSProvider.swift diff --git a/WireAPI/Sources/WireAPI/Models/User/UserID.swift b/WireAPI/Sources/WireAPI/Models/User/UserID.swift index db9cc419e44..576a237f690 100644 --- a/WireAPI/Sources/WireAPI/Models/User/UserID.swift +++ b/WireAPI/Sources/WireAPI/Models/User/UserID.swift @@ -18,6 +18,6 @@ import Foundation -/// Fully quallified user identifier. +/// Fully qualified user identifier. public typealias UserID = QualifiedID diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift index 79ab19bacaf..6292efc436d 100644 --- a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/TeamsAPITests.swift @@ -59,7 +59,7 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalholdRequest() async throws { + func testGetLegalholdInfoRequest() async throws { try await apiSnapshotHelper.verifyRequestForAllAPIVersions { sut in _ = try await sut.getLegalholdInfo(for: .mockID1, userID: .mockID2) } @@ -249,7 +249,7 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalhold_SuccessResponse_200_V0() async throws { + func testGetLegalholdInfo_SuccessResponse_200_V0() async throws { // Given let httpClient = try HTTPClientMock( code: .ok, @@ -273,13 +273,13 @@ final class TeamsAPITests: XCTestCase { ) // Then - let expectedPrekey = LegalHoldPrekey(id: 12_345, base64EncodedKey: "foo") + let expectedPrekey = LegalholdPrekey(id: 12_345, base64EncodedKey: "foo") XCTAssertEqual(result.status, .pending) XCTAssertEqual(result.prekey, expectedPrekey) } - func testGetLegalhold_FailureResponse_InvalidRequest_V0() async throws { - try await internalTest_GetLegalhold_Failure( + func testGetLegalholdInfo_FailureResponse_InvalidRequest_V0() async throws { + try await internalTest_GetLegalholdInfo_Failure( expectedError: TeamsAPIError.invalidRequest, for: .v0, code: .notFound, @@ -287,8 +287,8 @@ final class TeamsAPITests: XCTestCase { ) } - func testGetLegalhold_FailureResponse_MemberNotFound_V0() async throws { - try await internalTest_GetLegalhold_Failure( + func testGetLegalholdInfo_FailureResponse_MemberNotFound_V0() async throws { + try await internalTest_GetLegalholdInfo_Failure( expectedError: TeamsAPIError.teamMemberNotFound, for: .v0, code: .notFound, @@ -366,8 +366,8 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalhold_FailureResponse_InvalidRequest_V4() async throws { - try await internalTest_GetLegalhold_Failure( + func testGetLegalholdInfo_FailureResponse_InvalidRequest_V4() async throws { + try await internalTest_GetLegalholdInfo_Failure( expectedError: TeamsAPIError.invalidRequest, for: .v4, code: .badRequest, @@ -389,8 +389,8 @@ final class TeamsAPITests: XCTestCase { } } - func testGetLegalhold_FailureResponse_InvalidRequest_V5() async throws { - try await internalTest_GetLegalhold_Failure( + func testGetLegalholdInfo_FailureResponse_InvalidRequest_V5() async throws { + try await internalTest_GetLegalholdInfo_Failure( expectedError: TeamsAPIError.invalidRequest, for: .v5, code: .notFound, @@ -398,7 +398,7 @@ final class TeamsAPITests: XCTestCase { ) } - private func internalTest_GetLegalhold_Failure( + private func internalTest_GetLegalholdInfo_Failure( expectedError: any Error & Equatable, for apiVersion: APIVersion, code: HTTPStatusCode, diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj index a5c580bdff7..96c8c71c427 100644 --- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ C97C019C2CB66B9E000683C5 /* OneOnOneResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C019B2CB66B9E000683C5 /* OneOnOneResolverTests.swift */; }; C97C019F2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C019E2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift */; }; C97C01A12CB66BFF000683C5 /* OneOnOneResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */; }; + C97C01B02CB96F3C000683C5 /* MLSProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01AF2CB96F3C000683C5 /* MLSProvider.swift */; }; C99322D22C986E3A0065E10F /* TeamRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B22C986E3A0065E10F /* TeamRepository.swift */; }; C99322D32C986E3A0065E10F /* TeamRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */; }; C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99322B52C986E3A0065E10F /* SelfUserProvider.swift */; }; @@ -149,6 +150,7 @@ C97C019B2CB66B9E000683C5 /* OneOnOneResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverTests.swift; sourceTree = ""; }; C97C019E2CB66BC8000683C5 /* PushSupportedProtocolsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCaseTests.swift; sourceTree = ""; }; C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolver.swift; sourceTree = ""; }; + C97C01AF2CB96F3C000683C5 /* MLSProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLSProvider.swift; sourceTree = ""; }; C99322B22C986E3A0065E10F /* TeamRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepository.swift; sourceTree = ""; }; C99322B32C986E3A0065E10F /* TeamRepositoryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamRepositoryError.swift; sourceTree = ""; }; C99322B52C986E3A0065E10F /* SelfUserProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfUserProvider.swift; sourceTree = ""; }; @@ -670,6 +672,7 @@ C97C01A02CB66BFF000683C5 /* OneOnOneResolver.swift */, EECC35A52C2EB6CD00679448 /* SyncManager.swift */, EECC35A72C2EB70400679448 /* SyncState.swift */, + C97C01AF2CB96F3C000683C5 /* MLSProvider.swift */, ); path = Synchronization; sourceTree = ""; @@ -880,6 +883,7 @@ EEAD0A2E2C46B01900CC8658 /* UserUpdateEventProcessor.swift in Sources */, EEAD0A2A2C46AEB600CC8658 /* UserPropertiesDeleteEventProcessor.swift in Sources */, C99322D42C986E3A0065E10F /* SelfUserProvider.swift in Sources */, + C97C01B02CB96F3C000683C5 /* MLSProvider.swift in Sources */, EEAD0A332C46B99800CC8658 /* FederationDeleteEventProcessor.swift in Sources */, C99322E42C986E3A0065E10F /* ConversationRepository.swift in Sources */, C99322E02C986E3A0065E10F /* ConversationLocalStore+Metadata.swift in Sources */, diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift index 0c3faf036ab..24f92517422 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserRepository.swift @@ -116,7 +116,7 @@ public protocol UserRepositoryProtocol { /// Fetches all user IDs that have a one on one conversation /// - returns: A list of users' qualified IDs. - func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] + func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] } @@ -339,7 +339,7 @@ public final class UserRepository: UserRepositoryProtocol { } } - public func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { try await context.perform { [context] in let request = NSFetchRequest(entityName: ZMUser.entityName()) let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation)) diff --git a/WireDomain/Sources/WireDomain/Synchronization/MLSProvider.swift b/WireDomain/Sources/WireDomain/Synchronization/MLSProvider.swift new file mode 100644 index 00000000000..8a592a1b56a --- /dev/null +++ b/WireDomain/Sources/WireDomain/Synchronization/MLSProvider.swift @@ -0,0 +1,32 @@ +// +// Wire +// Copyright (C) 2024 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 WireDataModel + +public struct MLSProvider { + let service: any MLSServiceInterface + let isMLSEnabled: Bool + + public init( + service: any MLSServiceInterface, + isMLSEnabled: Bool + ) { + self.service = service + self.isMLSEnabled = isMLSEnabled + } +} diff --git a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift index a01a67e32c4..ec497388382 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift @@ -23,15 +23,15 @@ import WireDataModel // sourcery: AutoMockable /// Resolves 1:1 conversations protocol OneOnOneResolverProtocol { - func invoke() async throws + func resolveAllOneOnOneConversations() async throws } struct OneOnOneResolver: OneOnOneResolverProtocol { - private enum OneOnOneResolverUseCaseError: Error { + private enum Error: Swift.Error { case failedToActivateConversation case failedToFetchConversation - case failedToEstablishGroup(Error) + case failedToEstablishGroup(Swift.Error) } // MARK: - Properties @@ -55,14 +55,8 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { self.mlsProvider = mlsProvider } - func invoke() async throws { - try await resolveAllOneOnOneConversations() - } - - // MARK: - Private - - private func resolveAllOneOnOneConversations() async throws { - let usersIDs = try await userRepository.fetchAllUserIdsWithOneOnOneConversation() + func resolveAllOneOnOneConversations() async throws { + let usersIDs = try await userRepository.fetchAllUserIDsWithOneOnOneConversation() await withTaskGroup(of: Void.self) { group in for userID in usersIDs { @@ -114,7 +108,7 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") guard let userID = user.qualifiedID else { - throw OneOnOneResolverUseCaseError.failedToActivateConversation + throw Error.failedToActivateConversation } /// Sync the user MLS conversation from backend. @@ -127,7 +121,7 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { let mlsConversation = await conversationsRepository.fetchMLSConversation(with: mlsGroupID) guard let mlsConversation, let groupID = mlsConversation.mlsGroupID else { - throw OneOnOneResolverUseCaseError.failedToFetchConversation + throw Error.failedToFetchConversation } let mlsService = mlsProvider.service @@ -205,7 +199,7 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { } } catch { - throw OneOnOneResolverUseCaseError.failedToEstablishGroup(error) + throw Error.failedToEstablishGroup(error) } } else { try await mlsService.joinGroup(with: groupID) diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index a87eb2db8e6..e9dc395e052 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -38,19 +38,6 @@ protocol SyncManagerProtocol { } -public struct MLSProvider { - let service: any MLSServiceInterface - let isMLSEnabled: Bool - - public init( - service: any MLSServiceInterface, - isMLSEnabled: Bool - ) { - self.service = service - self.isMLSEnabled = isMLSEnabled - } -} - final class SyncManager: SyncManagerProtocol { enum Error: Swift.Error { @@ -122,7 +109,7 @@ final class SyncManager: SyncManagerProtocol { try await featureConfigsRepository.pullFeatureConfigs() try await pushSupportedProtocolsUseCase.invoke() let oneOnOneResolver = makeOneOnOneResolver() - try await oneOnOneResolver.invoke() + try await oneOnOneResolver.resolveAllOneOnOneConversations() } catch { throw Error.failedToPerformSlowSync(error) } diff --git a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift index 5863e8f0e56..81f93bc0be6 100644 --- a/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift +++ b/WireDomain/Sources/WireDomainSupport/Sourcery/generated/AutoMockable.generated.swift @@ -291,21 +291,21 @@ class MockOneOnOneResolverProtocol: OneOnOneResolverProtocol { - // MARK: - invoke + // MARK: - resolveAllOneOnOneConversations - var invoke_Invocations: [Void] = [] - var invoke_MockError: Error? - var invoke_MockMethod: (() async throws -> Void)? + var resolveAllOneOnOneConversations_Invocations: [Void] = [] + var resolveAllOneOnOneConversations_MockError: Error? + var resolveAllOneOnOneConversations_MockMethod: (() async throws -> Void)? - func invoke() async throws { - invoke_Invocations.append(()) + func resolveAllOneOnOneConversations() async throws { + resolveAllOneOnOneConversations_Invocations.append(()) - if let error = invoke_MockError { + if let error = resolveAllOneOnOneConversations_MockError { throw error } - guard let mock = invoke_MockMethod else { - fatalError("no mock for `invoke`") + guard let mock = resolveAllOneOnOneConversations_MockMethod else { + fatalError("no mock for `resolveAllOneOnOneConversations`") } try await mock() @@ -976,26 +976,26 @@ public class MockUserRepositoryProtocol: UserRepositoryProtocol { await mock(user, date) } - // MARK: - fetchAllUserIdsWithOneOnOneConversation + // MARK: - fetchAllUserIDsWithOneOnOneConversation - public var fetchAllUserIdsWithOneOnOneConversation_Invocations: [Void] = [] - public var fetchAllUserIdsWithOneOnOneConversation_MockError: Error? - public var fetchAllUserIdsWithOneOnOneConversation_MockMethod: (() async throws -> [WireDataModel.QualifiedID])? - public var fetchAllUserIdsWithOneOnOneConversation_MockValue: [WireDataModel.QualifiedID]? + public var fetchAllUserIDsWithOneOnOneConversation_Invocations: [Void] = [] + public var fetchAllUserIDsWithOneOnOneConversation_MockError: Error? + public var fetchAllUserIDsWithOneOnOneConversation_MockMethod: (() async throws -> [WireDataModel.QualifiedID])? + public var fetchAllUserIDsWithOneOnOneConversation_MockValue: [WireDataModel.QualifiedID]? - public func fetchAllUserIdsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { - fetchAllUserIdsWithOneOnOneConversation_Invocations.append(()) + public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + fetchAllUserIDsWithOneOnOneConversation_Invocations.append(()) - if let error = fetchAllUserIdsWithOneOnOneConversation_MockError { + if let error = fetchAllUserIDsWithOneOnOneConversation_MockError { throw error } - if let mock = fetchAllUserIdsWithOneOnOneConversation_MockMethod { + if let mock = fetchAllUserIDsWithOneOnOneConversation_MockMethod { return try await mock() - } else if let mock = fetchAllUserIdsWithOneOnOneConversation_MockValue { + } else if let mock = fetchAllUserIDsWithOneOnOneConversation_MockValue { return mock } else { - fatalError("no mock for `fetchAllUserIdsWithOneOnOneConversation`") + fatalError("no mock for `fetchAllUserIDsWithOneOnOneConversation`") } } diff --git a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift index c94580d7488..22b70295fd6 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/TeamRepositoryTests.swift @@ -237,7 +237,7 @@ final class TeamRepositoryTests: XCTestCase { } } - func testFetchSelfLegalholdStatus() async throws { + func testFetchSelfLegalholdInfo() async throws { // Mock teamsAPI.getLegalholdInfoForUserID_MockValue = Scaffolding.teamMemberLegalhold diff --git a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift index d703c81e9b1..05564d5f3d2 100644 --- a/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift +++ b/WireDomain/Tests/WireDomainTests/Repositories/UserRepositoryTests.swift @@ -360,7 +360,7 @@ class UserRepositoryTests: XCTestCase { // When - let userIds = try await sut.fetchAllUserIdsWithOneOnOneConversation() + let userIds = try await sut.fetchAllUserIDsWithOneOnOneConversation() // Then diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index 2ddcd72dff1..d2d4782621c 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -90,7 +90,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -134,7 +134,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -165,7 +165,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -201,7 +201,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -231,7 +231,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -296,7 +296,7 @@ final class OneOnOneResolverTests: XCTestCase { ) { userRepository.fetchUserWithDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser - userRepository.fetchAllUserIdsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] + userRepository.fetchAllUserIDsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = Scaffolding.conversationID.uuidString conversationsRepository.fetchMLSConversationWith_MockValue = mlsOneOnOneConversation diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index ffeaf69dc81..a2cf0a3b324 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -361,7 +361,7 @@ final class SyncManagerTests: XCTestCase { userRepository.pullSelfUser_MockMethod = {} teamRepository.pullSelfLegalholdInfo_MockMethod = {} pushSupportedProtocolsUseCase.invoke_MockMethod = {} - userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } + userRepository.fetchAllUserIDsWithOneOnOneConversation_MockMethod = { [] } userRepository.fetchUserWithDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser mlsService.conversationExistsGroupID_MockValue = true @@ -420,7 +420,7 @@ final class SyncManagerTests: XCTestCase { userRepository.pullSelfUser_MockError = UserRepositoryError.failedToFetchUser(selfUserID) /// throws error teamRepository.pullSelfLegalholdInfo_MockMethod = {} pushSupportedProtocolsUseCase.invoke_MockMethod = {} - userRepository.fetchAllUserIdsWithOneOnOneConversation_MockMethod = { [] } + userRepository.fetchAllUserIDsWithOneOnOneConversation_MockMethod = { [] } userRepository.fetchUserWithDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser mlsService.conversationExistsGroupID_MockValue = true From e151fbc4913b20e97bb8214302a8f3558944479c Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:25:58 +0100 Subject: [PATCH 14/18] rename Error with Failure - WPB-10727 --- WireAPI/Sources/WireAPI/Models/Connection/Connection.swift | 3 ++- .../Sources/WireDomain/Synchronization/SyncManager.swift | 6 +++--- .../Synchronization/OneOnOneResolverTests.swift | 3 ++- .../WireDomainTests/Synchronization/SyncManagerTests.swift | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift index bdf0f765aba..f97f5a2ade7 100644 --- a/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift +++ b/WireAPI/Sources/WireAPI/Models/Connection/Connection.swift @@ -54,7 +54,8 @@ public struct Connection: Equatable, Codable { conversationID: UUID?, qualifiedConversationID: QualifiedID?, lastUpdate: Date, - status: ConnectionStatus) { + status: ConnectionStatus) + { self.senderID = senderID self.receiverID = receiverID self.receiverQualifiedID = receiverQualifiedID diff --git a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift index e9dc395e052..2016c3d6409 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/SyncManager.swift @@ -40,8 +40,8 @@ protocol SyncManagerProtocol { final class SyncManager: SyncManagerProtocol { - enum Error: Swift.Error { - case failedToPerformSlowSync(Swift.Error) + enum Failure: Error { + case failedToPerformSlowSync(Error) } // MARK: - Properties @@ -111,7 +111,7 @@ final class SyncManager: SyncManagerProtocol { let oneOnOneResolver = makeOneOnOneResolver() try await oneOnOneResolver.resolveAllOneOnOneConversations() } catch { - throw Error.failedToPerformSlowSync(error) + throw Failure.failedToPerformSlowSync(error) } } diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index d2d4782621c..834e74f42a1 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -249,7 +249,8 @@ final class OneOnOneResolverTests: XCTestCase { mlsEpoch: UInt64 = 0 ) throws -> (selfUser: ZMUser, user: ZMUser, - mlsConversation: ZMConversation) { + mlsConversation: ZMConversation) + { let user = modelHelper.createUser( id: Scaffolding.receiverQualifiedID.uuid, domain: Scaffolding.receiverQualifiedID.domain, diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift index a2cf0a3b324..cd5c39faf88 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/SyncManagerTests.swift @@ -429,8 +429,9 @@ final class SyncManagerTests: XCTestCase { do { try await sut.performSlowSync() + XCTFail("this test should raise an error") } catch { - let syncError = try XCTUnwrap(error as? SyncManager.Error) + let syncError = try XCTUnwrap(error as? SyncManager.Failure) switch syncError { case .failedToPerformSlowSync(let error): From 4af8a0e162013dbbe64f0904902e83c49ac736b2 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:52:49 +0100 Subject: [PATCH 15/18] fix remaining conflicts, clean up code - WPB-10727 (#1999) --- ...stGetLegalholdInfoRequest.request-0-v0.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v1.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v2.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v3.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v4.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v5.txt | 4 + ...stGetLegalholdInfoRequest.request-0-v6.txt | 4 + ...ationReceiptModeUpdateEventProcessor.swift | 2 +- .../TeamMemberLeaveEventProcessor.swift | 4 +- .../UserConnectionEventProcessor.swift | 2 +- .../Sources/WireDomain/OneOnOneResolver.swift | 335 ------------------ .../Repositories/User/UserLocalStore.swift | 127 ++++++- .../Synchronization/OneOnOneResolver.swift | 82 +++-- .../PushSupportedProtocolsUseCase.swift | 3 +- ...ReceiptModeUpdateEventProcessorTests.swift | 2 +- .../TeamMemberLeaveEventProcessorTests.swift | 4 +- .../OneOnOneResolverTests.swift | 18 +- .../UserConnectionEventProcessorTests.swift | 4 +- .../OneOnOneResolverTests.swift | 3 +- 19 files changed, 206 insertions(+), 408 deletions(-) create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt create mode 100644 WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt delete mode 100644 WireDomain/Sources/WireDomain/OneOnOneResolver.swift diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt new file mode 100644 index 00000000000..f08e8e90296 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v0.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt new file mode 100644 index 00000000000..c2629df16cf --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v1.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v1/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt new file mode 100644 index 00000000000..03509c80054 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v2.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v2/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt new file mode 100644 index 00000000000..f0a8a91c317 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v3.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v3/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt new file mode 100644 index 00000000000..df69c86a56a --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v4.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v4/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt new file mode 100644 index 00000000000..bb7d083563a --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v5.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v5/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt new file mode 100644 index 00000000000..7dca81d74cb --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/TeamsAPI/__Snapshots__/TeamsAPITests/testGetLegalholdInfoRequest.request-0-v6.txt @@ -0,0 +1,4 @@ +- HTTPRequest + - path: "/v6/teams/213248a1-5499-418f-8173-5010d1c1e506/legalhold/302c59b0-037c-4b0f-a3ed-ccdbfb4cfe2c" + - method: get + - body: none diff --git a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift index 7ef16b24f3e..73620335408 100644 --- a/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift +++ b/WireDomain/Sources/WireDomain/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessor.swift @@ -40,7 +40,7 @@ struct ConversationReceiptModeUpdateEventProcessor: ConversationReceiptModeUpdat func processEvent(_ event: ConversationReceiptModeUpdateEvent) async throws { let senderID = event.senderID let conversationID = event.conversationID - let isEnabled = event.newRecieptMode == 1 + let isEnabled = event.newReceiptMode == 1 let sender = try await userRepository.fetchUser( id: senderID.uuid, diff --git a/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift index ef7f131dd05..2f4ea790162 100644 --- a/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift +++ b/WireDomain/Sources/WireDomain/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessor.swift @@ -37,9 +37,9 @@ struct TeamMemberLeaveEventProcessor: TeamMemberLeaveEventProcessorProtocol { func processEvent(_ event: TeamMemberLeaveEvent) async throws { try await repository.deleteMembership( - for: event.userID, + userID: event.userID, domain: nil, - at: event.time + date: event.time ) } diff --git a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift index 2d50ea45361..40cf367e47c 100644 --- a/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift +++ b/WireDomain/Sources/WireDomain/Event Processing/UserEventProcessor/UserConnectionEventProcessor.swift @@ -42,7 +42,7 @@ struct UserConnectionEventProcessor: UserConnectionEventProcessorProtocol { try await connectionsRepository.updateConnection(connection) if connection.status == .accepted { - try await oneOnOneResolver.invoke() + try await oneOnOneResolver.resolveAllOneOnOneConversations() } } } diff --git a/WireDomain/Sources/WireDomain/OneOnOneResolver.swift b/WireDomain/Sources/WireDomain/OneOnOneResolver.swift deleted file mode 100644 index 10811074a4f..00000000000 --- a/WireDomain/Sources/WireDomain/OneOnOneResolver.swift +++ /dev/null @@ -1,335 +0,0 @@ -// -// Wire -// Copyright (C) 2024 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 CoreData -import WireAPI -import WireDataModel - -// sourcery: AutoMockable -/// Resolves 1:1 conversations -public protocol OneOnOneResolverProtocol { - func invoke() async throws -} - -struct OneOnOneResolver: OneOnOneResolverProtocol { - - /// The target to resolve 1:1 conversation for. - enum Target { - case user(id: WireAPI.QualifiedID) - case allUsers - } - - private enum Error: Swift.Error { - case failedToActivateConversation - case failedToFetchConversation - case failedToEstablishGroup(Swift.Error) - } - - // MARK: - Properties - - private let context: NSManagedObjectContext - private let target: Target - private let userRepository: any UserRepositoryProtocol - private let conversationsRepository: any ConversationRepositoryProtocol - private let mlsService: any MLSServiceInterface - private let isMLSEnabled: Bool - - // MARK: - Object lifecycle - - init( - context: NSManagedObjectContext, - userRepository: any UserRepositoryProtocol, - conversationsRepository: any ConversationRepositoryProtocol, - mlsService: any MLSServiceInterface, - isMLSEnabled: Bool, - target: Target - ) { - self.context = context - self.userRepository = userRepository - self.conversationsRepository = conversationsRepository - self.mlsService = mlsService - self.isMLSEnabled = isMLSEnabled - self.target = target - } - - // MARK: - Public - - public func invoke() async throws { - switch target { - case .user(let id): - try await resolveOneOnOneConversation(with: id.toDomainModel()) - case .allUsers: - // TODO: [WPB-10727] resolve all users 1:1 conversations - break - } - } - - // MARK: - Private - - private func resolveOneOnOneConversation( - with userID: WireDataModel.QualifiedID - ) async throws { - let user = try await userRepository.fetchUser( - id: userID.uuid, - domain: userID.domain - ) - - let selfUser = await userRepository.fetchSelfUser() - let commonProtocol = await getCommonProtocol(between: selfUser, and: user) - - if isMLSEnabled, commonProtocol == .mls { - try await resolveMLSConversation( - for: user - ) - } - - if isMLSEnabled, commonProtocol == nil { - await resolveNoCommonProtocolConversation( - between: selfUser, - and: user - ) - } - - if commonProtocol == .proteus { - await resolveProteusConversation( - for: user - ) - } - } - - private func resolveMLSConversation(for user: ZMUser) async throws { - WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") - - let userID = await context.perform { - user.qualifiedID - } - - guard let userID else { - throw Error.failedToActivateConversation - } - - /// Sync the user MLS conversation from backend. - let mlsGroupID = try await conversationsRepository.pullMLSOneToOneConversation( - userID: userID.uuid.uuidString, - userDomain: userID.domain - ) - - /// Then, fetch the synced MLS conversation. - let mlsConversation = await conversationsRepository.fetchMLSConversation( - groupID: mlsGroupID - ) - - let localMLSGroupID = await context.perform { - mlsConversation?.mlsGroupID - } - - guard let mlsConversation, let localMLSGroupID else { - throw Error.failedToFetchConversation - } - - /// If conversation already exists, there is no need to perform a migration. - let needsMLSMigration = try await mlsService.conversationExists( - groupID: localMLSGroupID - ) == false - - if needsMLSMigration { - await migrateToMLS( - mlsConversation: mlsConversation, - mlsGroupID: localMLSGroupID, - user: user, - userID: userID - ) - } - } - - private func migrateToMLS( - mlsConversation: ZMConversation, - mlsGroupID: MLSGroupID, - user: ZMUser, - userID: WireDataModel.QualifiedID - ) async { - do { - try await setupMLSGroup( - mlsConversation: mlsConversation, - groupID: mlsGroupID, - userID: userID - ) - } catch { - await context.perform { - let userOneOnOneConversation = user.oneOnOneConversation - userOneOnOneConversation?.isForcedReadOnly = true - } - - return WireLogger.conversation.error( - "Failed to setup MLS group with ID: \(mlsGroupID.safeForLoggingDescription)" - ) - } - - await switchLocalConversationToMLS( - mlsConversation: mlsConversation, - for: user - ) - } - - /// Establish a new MLS group (when epoch is 0) or join an existing group. - /// - parameters: - /// - mlsConversation: The 1:1 MLS conversation. - /// - groupID: The MLS group ID. - /// - userID: The user ID that will be part of the MLS group. - - private func setupMLSGroup( - mlsConversation: ZMConversation, - groupID: MLSGroupID, - userID: WireDataModel.QualifiedID - ) async throws { - let epoch = await context.perform { - mlsConversation.epoch - } - - if epoch == 0 { - let users = [MLSUser(userID)] - - do { - let ciphersuite = try await mlsService.establishGroup( - for: groupID, - with: users, - removalKeys: nil - ) - - await context.perform { - mlsConversation.ciphersuite = ciphersuite - mlsConversation.mlsStatus = .ready - } - - } catch { - throw Error.failedToEstablishGroup(error) - } - } else { - try await mlsService.joinGroup(with: groupID) - } - } - - /// Migrates Proteus messages to the MLS conversation and sets the MLS conversation for the user. - /// - Parameter mlsConversation: The MLS conversation. - /// - Parameter user: The user to set the MLS conversation for. - - private func switchLocalConversationToMLS( - mlsConversation: ZMConversation, - for user: ZMUser - ) async { - await context.perform { - /// Move local messages from proteus conversation if it exists - if let proteusConversation = user.oneOnOneConversation { - /// Since ZMMessages only have a single conversation connected, - /// forming this union also removes the relationship to the proteus conversation. - mlsConversation.mutableMessages.union(proteusConversation.allMessages) - mlsConversation.isForcedReadOnly = false - mlsConversation.needsToBeUpdatedFromBackend = true - } - - /// Switch active conversation - user.oneOnOneConversation = mlsConversation - } - } - - /// Resolves a Proteus 1:1 conversation. - /// - Parameter user: The user to resolve the conversation for. - - private func resolveProteusConversation( - for user: ZMUser - ) async { - WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") - - let conversation = await context.perform { - user.oneOnOneConversation - } - - guard let conversation else { - return WireLogger.conversation.warn( - "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } - - await context.perform { - conversation.isForcedReadOnly = false - } - } - - /// Resolves a 1:1 conversation with no common protocols between self user and user. - /// - Parameter selfUser: The self user. - /// - Parameter user: The other user. - /// - /// When no common protocols are found, the 1:1 conversation is marked as read only and a system - /// message is append to the conversation to inform the self user or the user. - - private func resolveNoCommonProtocolConversation( - between selfUser: ZMUser, - and user: ZMUser - ) async { - WireLogger.conversation.debug("No common protocols found") - - let conversation = await context.perform { - user.oneOnOneConversation - } - - guard let conversation else { - return WireLogger.conversation.warn( - "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } - - let isForcedReadOnly = await context.perform { - conversation.isForcedReadOnly - } - - if !isForcedReadOnly { - await context.perform { - if !selfUser.supportedProtocols.contains(.mls) { - conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser) - } else if !user.supportedProtocols.contains(.mls) { - conversation.appendMLSMigrationMLSNotSupportedForOtherUser(user: user) - } - - conversation.isForcedReadOnly = true - } - } - } - - private func getCommonProtocol( - between selfUser: ZMUser, - and otherUser: ZMUser - ) async -> ConversationMessageProtocol? { - let selfUserProtocols = await context.perform { - selfUser.supportedProtocols - } - - let otherUserProtocols = await context.perform { - otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols - } - - let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) - - if commonProtocols.contains(.mls) { - return .mls - } else if commonProtocols.contains(.proteus) { - return .proteus - } else { - return nil - } - } -} diff --git a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift index 1461500c998..e020344718d 100644 --- a/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift +++ b/WireDomain/Sources/WireDomain/Repositories/User/UserLocalStore.swift @@ -133,7 +133,13 @@ public protocol UserLocalStoreProtocol { func markAccountAsDeleted(for user: ZMUser) async - // TODO: [WPB-10727] Merge these two methods into a single method (also no API objects should be passed to local store) + /// Fetches all user IDs that have a one on one conversation + /// - returns: A list of users' qualified IDs. + + func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] + + // TODO: [WPB-10727] Merge these methods into a single method (also no API objects should be passed to local store) + func persistSelfUser(from selfUser: WireAPI.SelfUser) async func persistUser(from user: WireAPI.User) async func updateUser(from event: UserUpdateEvent) async } @@ -195,6 +201,26 @@ public final class UserLocalStore: UserLocalStoreProtocol { } } + public func fetchAllUserIDsWithOneOnOneConversation() async throws -> [WireDataModel.QualifiedID] { + try await context.perform { [context] in + let request = NSFetchRequest(entityName: ZMUser.entityName()) + let predicate = NSPredicate(format: "%K != nil", #keyPath(ZMUser.oneOnOneConversation)) + request.predicate = predicate + + return try context + .fetch(request) + .compactMap { user in + guard let userID = user.qualifiedID else { + WireLogger.conversation.error( + "Missing user's qualifiedID" + ) + return nil + } + return userID + } + } + } + public func fetchUsersQualifiedIDs() async throws -> [WireDataModel.QualifiedID] { try await context.perform { let fetchRequest = NSFetchRequest(entityName: ZMUser.entityName()) @@ -300,23 +326,53 @@ public final class UserLocalStore: UserLocalStoreProtocol { domain: user.id.domain ) - await context.perform { - guard user.deleted == false else { - return persistedUser.markAccountAsDeleted(at: Date()) - } + let previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key + let completeProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key + + await updateUserMetadata( + persistedUser, + deleted: user.deleted == true, + name: user.name, + handle: user.handle, + teamID: user.teamID, + accentID: user.accentID, + previewProfileAssetIdentifier: previewProfileAssetIdentifier, + completeProfileAssetIdentifier: completeProfileAssetIdentifier, + email: user.email, + expiresAt: user.expiresAt, + serviceIdentifier: user.service?.id.transportString(), + providerIdentifier: user.service?.provider.transportString(), + supportedProtocols: user.supportedProtocols?.toDomainModel() ?? [.proteus] + ) + } - persistedUser.name = user.name - persistedUser.handle = user.handle - persistedUser.teamIdentifier = user.teamID - persistedUser.accentColorValue = Int16(user.accentID) - persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .preview })?.key - persistedUser.previewProfileAssetIdentifier = user.assets.first(where: { $0.size == .complete })?.key - persistedUser.emailAddress = user.email - persistedUser.expiresAt = user.expiresAt - persistedUser.serviceIdentifier = user.service?.id.transportString() - persistedUser.providerIdentifier = user.service?.provider.transportString() - persistedUser.supportedProtocols = user.supportedProtocols?.toDomainModel() ?? [.proteus] - persistedUser.needsToBeUpdatedFromBackend = false + public func persistSelfUser( + from selfUser: WireAPI.SelfUser + ) async { + let persistedSelfUser = await fetchSelfUser() + let previewProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .preview })?.key + let completeProfileAssetIdentifier = selfUser.assets?.first(where: { $0.size == .complete })?.key + + await updateUserMetadata( + persistedSelfUser, + deleted: selfUser.deleted == true, + name: selfUser.name, + handle: selfUser.handle, + teamID: selfUser.teamID, + accentID: selfUser.accentID, + previewProfileAssetIdentifier: previewProfileAssetIdentifier, + completeProfileAssetIdentifier: completeProfileAssetIdentifier, + email: selfUser.email, + expiresAt: selfUser.expiresAt, + serviceIdentifier: selfUser.service?.id.transportString(), + providerIdentifier: selfUser.service?.provider.transportString(), + supportedProtocols: selfUser.supportedProtocols?.toDomainModel() ?? [.proteus] + ) + + await context.perform { + persistedSelfUser.remoteIdentifier = selfUser.qualifiedID.uuid + persistedSelfUser.domain = selfUser.qualifiedID.domain + persistedSelfUser.managedBy = selfUser.managedBy?.rawValue } } @@ -432,4 +488,41 @@ public final class UserLocalStore: UserLocalStoreProtocol { } } + // MARK: - Private + + private func updateUserMetadata( + _ user: ZMUser, + deleted: Bool, + name: String, + handle: String?, + teamID: UUID?, + accentID: Int, + previewProfileAssetIdentifier: String?, + completeProfileAssetIdentifier: String?, + email: String?, + expiresAt: Date?, + serviceIdentifier: String?, + providerIdentifier: String?, + supportedProtocols: Set + ) async { + await context.perform { + guard deleted == false else { + return user.markAccountAsDeleted(at: .now) + } + + user.name = name + user.handle = handle + user.teamIdentifier = teamID + user.accentColorValue = Int16(accentID) + user.previewProfileAssetIdentifier = previewProfileAssetIdentifier + user.completeProfileAssetIdentifier = completeProfileAssetIdentifier + user.emailAddress = email + user.expiresAt = expiresAt + user.serviceIdentifier = serviceIdentifier + user.providerIdentifier = providerIdentifier + user.supportedProtocols = supportedProtocols + user.needsToBeUpdatedFromBackend = false + } + } + } diff --git a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift index ec497388382..584dfa79b83 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift @@ -22,7 +22,7 @@ import WireDataModel // sourcery: AutoMockable /// Resolves 1:1 conversations -protocol OneOnOneResolverProtocol { +public protocol OneOnOneResolverProtocol { func resolveAllOneOnOneConversations() async throws } @@ -78,11 +78,11 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { with userID: WireDataModel.QualifiedID ) async throws { let user = try await userRepository.fetchUser( - with: userID.uuid, domain: userID.domain + id: userID.uuid, domain: userID.domain ) - let selfUser = userRepository.fetchSelfUser() - let commonProtocol = getCommonProtocol(between: selfUser, and: user) + let selfUser = await userRepository.fetchSelfUser() + let commonProtocol = await getCommonProtocol(between: selfUser, and: user) if mlsProvider.isMLSEnabled, commonProtocol == .mls { try await resolveMLSConversation( @@ -107,20 +107,28 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { private func resolveMLSConversation(for user: ZMUser) async throws { WireLogger.conversation.debug("Should resolve to mls 1-1 conversation") - guard let userID = user.qualifiedID else { + let userID = await context.perform { + user.qualifiedID + } + + guard let userID else { throw Error.failedToActivateConversation } /// Sync the user MLS conversation from backend. let mlsGroupID = try await conversationsRepository.pullMLSOneToOneConversation( userID: userID.uuid.uuidString, - domain: userID.domain + userDomain: userID.domain ) /// Then, fetch the synced MLS conversation. - let mlsConversation = await conversationsRepository.fetchMLSConversation(with: mlsGroupID) + let mlsConversation = await conversationsRepository.fetchMLSConversation(groupID: mlsGroupID) + + let groupID = await context.perform { + mlsConversation?.mlsGroupID + } - guard let mlsConversation, let groupID = mlsConversation.mlsGroupID else { + guard let mlsConversation, let groupID else { throw Error.failedToFetchConversation } @@ -183,7 +191,11 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { ) async throws { let mlsService = mlsProvider.service - if mlsConversation.epoch == 0 { + let epoch = await context.perform { + mlsConversation.epoch + } + + if epoch == 0 { let users = [MLSUser(userID)] do { @@ -235,15 +247,15 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { private func resolveProteusConversation( for user: ZMUser ) async { - WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") + await context.perform { + WireLogger.conversation.debug("Should resolve to Proteus 1-1 conversation") - guard let conversation = user.oneOnOneConversation else { - return WireLogger.conversation.warn( - "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve Proteus conversation: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } - await context.perform { conversation.isForcedReadOnly = false } } @@ -259,16 +271,16 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { between selfUser: ZMUser, and user: ZMUser ) async { - WireLogger.conversation.debug("No common protocols found") + await context.perform { + WireLogger.conversation.debug("No common protocols found") - guard let conversation = user.oneOnOneConversation else { - return WireLogger.conversation.warn( - "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" - ) - } + guard let conversation = user.oneOnOneConversation else { + return WireLogger.conversation.warn( + "Failed to resolve 1:1 conversation with no common protocol: missing 1:1 conversation for user with id \(user.remoteIdentifier.safeForLoggingDescription)" + ) + } - if !conversation.isForcedReadOnly { - await context.perform { + if !conversation.isForcedReadOnly { if !selfUser.supportedProtocols.contains(.mls) { conversation.appendMLSMigrationMLSNotSupportedForSelfUser(user: selfUser) } else if !user.supportedProtocols.contains(.mls) { @@ -283,18 +295,20 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { private func getCommonProtocol( between selfUser: ZMUser, and otherUser: ZMUser - ) -> ConversationMessageProtocol? { - let selfUserProtocols = selfUser.supportedProtocols - let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols /// default to Proteus if empty. + ) async -> ConversationMessageProtocol? { + await context.perform { + let selfUserProtocols = selfUser.supportedProtocols + let otherUserProtocols = otherUser.supportedProtocols.isEmpty ? [.proteus] : otherUser.supportedProtocols /// default to Proteus if empty. - let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) + let commonProtocols = selfUserProtocols.intersection(otherUserProtocols) - if commonProtocols.contains(.mls) { - return .mls - } else if commonProtocols.contains(.proteus) { - return .proteus - } else { - return nil + if commonProtocols.contains(.mls) { + return .mls + } else if commonProtocols.contains(.proteus) { + return .proteus + } else { + return nil + } } } } diff --git a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift index a44f1a7b4fc..9cd169f36c5 100644 --- a/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift +++ b/WireDomain/Sources/WireDomain/UseCases/PushSupportedProtocolsUseCase.swift @@ -20,7 +20,6 @@ import WireAPI import WireDataModel import WireSystem - // sourcery: AutoMockable /// Calculates and pushes the supported protocols to the backend public protocol PushSupportedProtocolsUseCaseProtocol { @@ -35,7 +34,7 @@ public struct PushSupportedProtocolsUseCase: PushSupportedProtocolsUseCaseProtoc case ongoing case finalised } - + let featureConfigRepository: any FeatureConfigRepositoryProtocol let userRepository: any UserRepositoryProtocol let userClientsRepository: any UserClientsRepositoryProtocol diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift index 0a53bd9ad4e..41b4a4dd472 100644 --- a/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift +++ b/WireDomain/Tests/WireDomainTests/Event Processing/ConversationEventProcessor/ConversationReceiptModeUpdateEventProcessorTests.swift @@ -105,7 +105,7 @@ final class ConversationReceiptModeUpdateEventProcessorTests: XCTestCase { static let event = ConversationReceiptModeUpdateEvent( conversationID: ConversationID(uuid: id, domain: domain), senderID: UserID(uuid: id, domain: domain), - newRecieptMode: 1 + newReceiptMode: 1 ) } } diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift index bbf662be9c5..4466ad1c031 100644 --- a/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift +++ b/WireDomain/Tests/WireDomainTests/Event Processing/TeamEventProcessor/TeamMemberLeaveEventProcessorTests.swift @@ -45,7 +45,7 @@ final class TeamMemberLeaveEventProcessorTests: XCTestCase { func testProcessEvent_It_Invokes_Delete_Team_Membership_Repo_Method() async throws { // Mock - teamRepository.deleteMembershipForDomainAt_MockMethod = { _, _, _ in } + teamRepository.deleteMembershipUserIDDomainDate_MockMethod = { _, _, _ in } // When @@ -53,7 +53,7 @@ final class TeamMemberLeaveEventProcessorTests: XCTestCase { // Then - XCTAssertEqual(teamRepository.deleteMembershipForDomainAt_Invocations.count, 1) + XCTAssertEqual(teamRepository.deleteMembershipUserIDDomainDate_Invocations.count, 1) } private enum Scaffolding { diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift index dc97cf07aa9..0517930c775 100644 --- a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift @@ -49,9 +49,7 @@ final class OneOnOneResolverTests: XCTestCase { context: context, userRepository: userRepository, conversationsRepository: conversationsRepository, - mlsService: mlsService, - isMLSEnabled: true, - target: .user(id: Scaffolding.receiverQualifiedID) + mlsProvider: .init(service: mlsService, isMLSEnabled: true) ) DeveloperFlag.storage = UserDefaults(suiteName: Scaffolding.defaultsSuiteName)! @@ -93,12 +91,13 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then XCTAssert(mlsService.establishGroupForWithRemovalKeys_Invocations.isEmpty) XCTAssert(mlsService.joinGroupWith_Invocations.isEmpty) + XCTAssertEqual(userRepository.fetchAllUserIDsWithOneOnOneConversation_Invocations.count, 1) } func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Zero() async throws { @@ -123,7 +122,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -170,7 +169,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -204,7 +203,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -243,7 +242,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -277,7 +276,7 @@ final class OneOnOneResolverTests: XCTestCase { // When - try await sut.invoke() + try await sut.resolveAllOneOnOneConversations() // Then @@ -346,6 +345,7 @@ final class OneOnOneResolverTests: XCTestCase { ) { userRepository.fetchUserIdDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser + userRepository.fetchAllUserIDsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] conversationsRepository.pullMLSOneToOneConversationUserIDUserDomain_MockValue = Scaffolding.conversationID.uuidString conversationsRepository.fetchMLSConversationGroupID_MockValue = mlsOneOnOneConversation diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift index 8233ea7ed65..e9a98e26cc5 100644 --- a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift +++ b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/UserConnectionEventProcessorTests.swift @@ -57,7 +57,7 @@ final class UserConnectionEventProcessorTests: XCTestCase { // Mock connectionsRepository.updateConnection_MockMethod = { _ in } - oneOnOneResolver.invoke_MockMethod = {} + oneOnOneResolver.resolveAllOneOnOneConversations_MockMethod = {} // When @@ -66,7 +66,7 @@ final class UserConnectionEventProcessorTests: XCTestCase { // Then XCTAssertEqual(connectionsRepository.updateConnection_Invocations, [event.connection]) - XCTAssertEqual(oneOnOneResolver.invoke_Invocations.count, 1) + XCTAssertEqual(oneOnOneResolver.resolveAllOneOnOneConversations_Invocations.count, 1) } private enum Scaffolding { diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index 834e74f42a1..d2d4782621c 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -249,8 +249,7 @@ final class OneOnOneResolverTests: XCTestCase { mlsEpoch: UInt64 = 0 ) throws -> (selfUser: ZMUser, user: ZMUser, - mlsConversation: ZMConversation) - { + mlsConversation: ZMConversation) { let user = modelHelper.createUser( id: Scaffolding.receiverQualifiedID.uuid, domain: Scaffolding.receiverQualifiedID.domain, From d6014c14e032e515364796e7518881173f986fe1 Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:02:37 +0100 Subject: [PATCH 16/18] fix OneOnOneResolver tests - WPB-10727 (#1999) --- .../project.pbxproj | 8 +- .../OneOnOneResolverTests.swift | 376 ------------------ .../OneOnOneResolverTests.swift | 124 +++--- 3 files changed, 78 insertions(+), 430 deletions(-) delete mode 100644 WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift diff --git a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj index 899d602075b..942da8c12ed 100644 --- a/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj +++ b/WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ C96B75482CDBA10F003A85EB /* SystemMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96B75462CDBA10F003A85EB /* SystemMessage.swift */; }; C97BCCAA2C98704B004F2D0D /* WireDomain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01D0DCA62C1C8C870076CB1C /* WireDomain.framework */; }; C97C013D2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */; }; - C97C01502CB01BDF000683C5 /* OneOnOneResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */; }; C97C01542CB04626000683C5 /* UserDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01532CB04626000683C5 /* UserDeleteEventProcessorTests.swift */; }; C97C01592CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01562CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift */; }; C97C015A2CB40010000683C5 /* FederationDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C01572CB40010000683C5 /* FederationDeleteEventProcessorTests.swift */; }; @@ -78,6 +77,7 @@ C9C102472CE78AEA00EA273F /* MessageLocalStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C102452CE78AEA00EA273F /* MessageLocalStoreTests.swift */; }; C9C1024A2CE792C300EA273F /* PushSupportedProtocolsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C102492CE792C300EA273F /* PushSupportedProtocolsUseCase.swift */; }; C9C1024C2CE7935600EA273F /* UserConnectionEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C1024B2CE7935600EA273F /* UserConnectionEventProcessorTests.swift */; }; + C9C1024E2CE7984300EA273F /* OneOnOneResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C1024D2CE7984300EA273F /* OneOnOneResolverTests.swift */; }; C9C8FDCE2C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC32C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift */; }; C9C8FDCF2C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC52C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift */; }; C9C8FDD02C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C8FDC62C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift */; }; @@ -186,7 +186,6 @@ C96B75462CDBA10F003A85EB /* SystemMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMessage.swift; sourceTree = ""; }; C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesDeleteEventProcessorTests.swift; sourceTree = ""; }; C97C014A2CB00F92000683C5 /* OneOnOneResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OneOnOneResolver.swift; path = ../../OneOnOneResolver.swift; sourceTree = ""; }; - C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverTests.swift; sourceTree = ""; }; C97C01532CB04626000683C5 /* UserDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDeleteEventProcessorTests.swift; sourceTree = ""; }; C97C01562CB40010000683C5 /* FederationConnectionRemovedEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationConnectionRemovedEventProcessorTests.swift; sourceTree = ""; }; C97C01572CB40010000683C5 /* FederationDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederationDeleteEventProcessorTests.swift; sourceTree = ""; }; @@ -228,6 +227,7 @@ C9C102452CE78AEA00EA273F /* MessageLocalStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLocalStoreTests.swift; sourceTree = ""; }; C9C102492CE792C300EA273F /* PushSupportedProtocolsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSupportedProtocolsUseCase.swift; sourceTree = ""; }; C9C1024B2CE7935600EA273F /* UserConnectionEventProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConnectionEventProcessorTests.swift; sourceTree = ""; }; + C9C1024D2CE7984300EA273F /* OneOnOneResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOnOneResolverTests.swift; sourceTree = ""; }; C9C8FDC32C9DBE0E00702B91 /* FeatureConfigUpdateEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfigUpdateEventProcessorTests.swift; sourceTree = ""; }; C9C8FDC52C9DBE0E00702B91 /* TeamDeleteEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamDeleteEventProcessorTests.swift; sourceTree = ""; }; C9C8FDC62C9DBE0E00702B91 /* TeamMemberLeaveEventProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamMemberLeaveEventProcessorTests.swift; sourceTree = ""; }; @@ -656,7 +656,6 @@ isa = PBXGroup; children = ( C97C01BA2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift */, - C97C014F2CB01BDF000683C5 /* OneOnOneResolverTests.swift */, C9C1024B2CE7935600EA273F /* UserConnectionEventProcessorTests.swift */, C97C015B2CB40038000683C5 /* UserPropertiesSetEventProcessorTests.swift */, C97C013C2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift */, @@ -807,6 +806,7 @@ EEC410252C60D48900E89394 /* Synchronization */ = { isa = PBXGroup; children = ( + C9C1024D2CE7984300EA273F /* OneOnOneResolverTests.swift */, EEC410242C60D48900E89394 /* SyncManagerTests.swift */, ); path = Synchronization; @@ -1124,8 +1124,8 @@ C9E8A3AE2C73878B0093DD5C /* ConnectionsRepositoryTests.swift in Sources */, C9C102472CE78AEA00EA273F /* MessageLocalStoreTests.swift in Sources */, C97C01BB2CBE5E65000683C5 /* UserUpdateEventProcessorTests.swift in Sources */, + C9C1024E2CE7984300EA273F /* OneOnOneResolverTests.swift in Sources */, C97C013D2CAD7D69000683C5 /* UserPropertiesDeleteEventProcessorTests.swift in Sources */, - C97C01502CB01BDF000683C5 /* OneOnOneResolverTests.swift in Sources */, C9C8FDD22C9DBE0E00702B91 /* UserClientAddEventProcessorTests.swift in Sources */, C9C8FDD12C9DBE0E00702B91 /* TeamMemberUpdateEventProcessorTests.swift in Sources */, C9C1024C2CE7935600EA273F /* UserConnectionEventProcessorTests.swift in Sources */, diff --git a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift deleted file mode 100644 index 0517930c775..00000000000 --- a/WireDomain/Tests/WireDomainTests/Event Processing/UserEventProcessor/OneOnOneResolverTests.swift +++ /dev/null @@ -1,376 +0,0 @@ -// -// Wire -// Copyright (C) 2024 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 WireAPI -import WireDataModel -import WireDataModelSupport -@testable import WireDomain -import WireDomainSupport -import XCTest - -final class OneOnOneResolverTests: XCTestCase { - - private var sut: WireDomain.OneOnOneResolver! - private var coreDataStack: CoreDataStack! - private var coreDataStackHelper: CoreDataStackHelper! - private var modelHelper: ModelHelper! - private var userRepository: MockUserRepositoryProtocol! - private var conversationsRepository: MockConversationRepositoryProtocol! - private var mlsService: MockMLSServiceInterface! - - var context: NSManagedObjectContext { - coreDataStack.syncContext - } - - override func setUp() async throws { - try await super.setUp() - coreDataStackHelper = CoreDataStackHelper() - modelHelper = ModelHelper() - coreDataStack = try await coreDataStackHelper.createStack() - userRepository = MockUserRepositoryProtocol() - conversationsRepository = MockConversationRepositoryProtocol() - mlsService = MockMLSServiceInterface() - sut = WireDomain.OneOnOneResolver( - context: context, - userRepository: userRepository, - conversationsRepository: conversationsRepository, - mlsProvider: .init(service: mlsService, isMLSEnabled: true) - ) - - DeveloperFlag.storage = UserDefaults(suiteName: Scaffolding.defaultsSuiteName)! - var flag = DeveloperFlag.enableMLSSupport - flag.isOn = true - } - - override func tearDown() async throws { - try await super.tearDown() - coreDataStack = nil - sut = nil - modelHelper = nil - try coreDataStackHelper.cleanupDirectory() - DeveloperFlag.storage.removePersistentDomain(forName: Scaffolding.defaultsSuiteName) - coreDataStackHelper = nil - } - - // MARK: - Tests - - func testProcessEvent_It_Does_Not_Migrate_MLS_Conversation() async throws { - // Given - - let commonProtocol = WireDataModel.MessageProtocol.mls - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol - ) - - let mlsConversationExists = true /// should not migrate MLS conversation - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation, - mlsConversationExists: mlsConversationExists - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - XCTAssert(mlsService.establishGroupForWithRemovalKeys_Invocations.isEmpty) - XCTAssert(mlsService.joinGroupWith_Invocations.isEmpty) - XCTAssertEqual(userRepository.fetchAllUserIDsWithOneOnOneConversation_Invocations.count, 1) - } - - func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Zero() async throws { - // Given - - let commonProtocol = WireDataModel.MessageProtocol.mls - let mlsEpoch: UInt64 = 0 - - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol, - mlsEpoch: mlsEpoch - ) - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - XCTAssertEqual(mlsService.establishGroupForWithRemovalKeys_Invocations.count, 1) - let createGroupInvocation = try XCTUnwrap( - mlsService.establishGroupForWithRemovalKeys_Invocations.first - ) - - XCTAssertEqual(createGroupInvocation.groupID, Scaffolding.mlsGroupID) - XCTAssertEqual( - createGroupInvocation.users, - [MLSUser(Scaffolding.receiverQualifiedID.toDomainModel())] - ) - - await context.perform { - XCTAssertEqual(mlsOneOnOneConversation.ciphersuite, Scaffolding.ciphersuite) - XCTAssertEqual(mlsOneOnOneConversation.mlsStatus, .ready) - XCTAssertEqual(mlsOneOnOneConversation.isForcedReadOnly, false) - XCTAssertEqual(mlsOneOnOneConversation.needsToBeUpdatedFromBackend, true) - XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) - XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) - } - } - - func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Not_Zero() async throws { - // Given - - let commonProtocol = WireDataModel.MessageProtocol.mls - let mlsEpoch: UInt64 = 1 - - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol, - mlsEpoch: mlsEpoch - ) - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - XCTAssertEqual(mlsService.joinGroupWith_Invocations.count, 1) - let invokedMLSGroupID = try XCTUnwrap(mlsService.joinGroupWith_Invocations.first) - XCTAssertEqual(invokedMLSGroupID, Scaffolding.mlsGroupID) - - await context.perform { - XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) - XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) - } - } - - func testProcessEvent_It_Migrates_Proteus_Messages_To_MLS_Conversation() async throws { - // Given - - let commonProtocol = WireDataModel.MessageProtocol.mls - - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol - ) - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - let migratedMessagesTexts = await context.perform { - mlsOneOnOneConversation.allMessages - .compactMap(\.textMessageData) - .compactMap(\.messageText) - .sorted() - } - - /// Ensuring proteus messages were migrated to MLS conversation. - XCTAssertEqual(migratedMessagesTexts.first, "Hello") - XCTAssertEqual(migratedMessagesTexts.last, "World!") - } - - func testProcessEvent_It_Resolves_Proteus_Conversation() async throws { - // Given - - let commonProtocol = WireDataModel.MessageProtocol.proteus - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol - ) - - await context.perform { - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) - } - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - await context.perform { - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) - } - } - - func testProcessEvent_It_Resolves_Conversation_With_No_Common_Protocol() async throws { - // Given - - let forcedReadOnly = false - - let (selfUser, user, mlsOneOnOneConversation) = try await setupManagedObjects( - selfUserProtocol: .mls, - userProtocol: .proteus, - forcedReadOnly: forcedReadOnly - ) - - await context.perform { - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) - } - - // Mock - - setupMock( - selfUser: selfUser, - user: user, - mlsOneOnOneConversation: mlsOneOnOneConversation - ) - - // When - - try await sut.resolveAllOneOnOneConversations() - - // Then - - try await context.perform { - let lastMessage = try XCTUnwrap(user.oneOnOneConversation?.lastMessage as? ZMSystemMessage) - XCTAssertEqual(lastMessage.systemMessageType, .mlsNotSupportedOtherUser) - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) - } - } - - // MARK: - Setup - - typealias ManagedObjects = (selfUser: ZMUser, user: ZMUser, mlsConversation: ZMConversation) - - private func setupManagedObjects( - selfUserProtocol: WireDataModel.MessageProtocol, - userProtocol: WireDataModel.MessageProtocol, - forcedReadOnly: Bool = true, - mlsEpoch: UInt64 = 0 - ) async throws -> ManagedObjects { - try await context.perform { [self] in - let user = modelHelper.createUser( - id: Scaffolding.receiverQualifiedID.uuid, - domain: Scaffolding.receiverQualifiedID.domain, - in: context - ) - - user.supportedProtocols = [userProtocol] - - let selfUser = modelHelper.createSelfUser( - id: UUID(), - domain: nil, - in: context - ) - - selfUser.supportedProtocols = [selfUserProtocol] - - let proteusConversation = modelHelper.createOneOnOne( - with: selfUser, - in: context - ) - - proteusConversation.isForcedReadOnly = forcedReadOnly - user.oneOnOneConversation = proteusConversation - - try proteusConversation.appendText(content: "Hello") - try proteusConversation.appendText(content: "World!") - - let mlsOneOnOneConversation = modelHelper.createMLSConversation( - mlsGroupID: Scaffolding.mlsGroupID, - mlsStatus: .pendingJoin, - conversationType: .oneOnOne, - epoch: mlsEpoch, - in: context - ) - - return (selfUser, user, mlsOneOnOneConversation) - } - } - - private func setupMock( - selfUser: ZMUser, - user: ZMUser, - mlsOneOnOneConversation: ZMConversation, - mlsConversationExists: Bool = false - ) { - userRepository.fetchUserIdDomain_MockValue = user - userRepository.fetchSelfUser_MockValue = selfUser - userRepository.fetchAllUserIDsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] - - conversationsRepository.pullMLSOneToOneConversationUserIDUserDomain_MockValue = Scaffolding.conversationID.uuidString - conversationsRepository.fetchMLSConversationGroupID_MockValue = mlsOneOnOneConversation - - mlsService.establishGroupForWithRemovalKeys_MockValue = Scaffolding.ciphersuite - mlsService.conversationExistsGroupID_MockValue = mlsConversationExists - mlsService.joinGroupWith_MockMethod = { _ in } - } - - private enum Scaffolding { - static let receiverID = UUID() - static let receiverQualifiedID = WireAPI.QualifiedID( - uuid: receiverID, - domain: "domain.com" - ) - static let conversationID = UUID() - - static let base64EncodedString = "pQABARn//wKhAFggHsa0CszLXYLFcOzg8AA//E1+Dl1rDHQ5iuk44X0/PNYDoQChAFgg309rkhG6SglemG6kWae81P1HtQPx9lyb6wExTovhU4cE9g==" - - nonisolated(unsafe) static let ciphersuite = WireDataModel.MLSCipherSuite.MLS_256_DHKEMP521_AES256GCM_SHA512_P521 - - nonisolated(unsafe) static let mlsGroupID = WireDataModel.MLSGroupID( - base64Encoded: base64EncodedString - )! - - static let defaultsSuiteName = UUID().uuidString - } -} diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index d2d4782621c..b6b9f3081e6 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -74,11 +74,12 @@ final class OneOnOneResolverTests: XCTestCase { let commonProtocol = WireDataModel.MessageProtocol.mls let mlsEpoch: UInt64 = 0 - let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol, - mlsEpoch: mlsEpoch - ) + let (selfUser, user, mlsOneOnOneConversation) = try await context.perform { [self] in + try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol + ) + } // Mock @@ -104,12 +105,15 @@ final class OneOnOneResolverTests: XCTestCase { createGroupInvocation.users, [MLSUser(Scaffolding.receiverQualifiedID.toDomainModel())] ) - XCTAssertEqual(mlsOneOnOneConversation.ciphersuite, Scaffolding.ciphersuite) - XCTAssertEqual(mlsOneOnOneConversation.mlsStatus, .ready) - XCTAssertEqual(mlsOneOnOneConversation.isForcedReadOnly, false) - XCTAssertEqual(mlsOneOnOneConversation.needsToBeUpdatedFromBackend, true) - XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) - XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + + await context.perform { + XCTAssertEqual(mlsOneOnOneConversation.ciphersuite, Scaffolding.ciphersuite) + XCTAssertEqual(mlsOneOnOneConversation.mlsStatus, .ready) + XCTAssertEqual(mlsOneOnOneConversation.isForcedReadOnly, false) + XCTAssertEqual(mlsOneOnOneConversation.needsToBeUpdatedFromBackend, true) + XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) + XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + } } func testProcessEvent_It_Resolves_MLS_Conversation_Epoch_Not_Zero() async throws { @@ -118,11 +122,13 @@ final class OneOnOneResolverTests: XCTestCase { let commonProtocol = WireDataModel.MessageProtocol.mls let mlsEpoch: UInt64 = 1 - let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol, - mlsEpoch: mlsEpoch - ) + let (selfUser, user, mlsOneOnOneConversation) = try await context.perform { [self] in + try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol, + mlsEpoch: mlsEpoch + ) + } // Mock @@ -138,11 +144,13 @@ final class OneOnOneResolverTests: XCTestCase { // Then - XCTAssertEqual(mlsService.joinGroupWith_Invocations.count, 1) - let invokedMLSGroupID = try XCTUnwrap(mlsService.joinGroupWith_Invocations.first) - XCTAssertEqual(invokedMLSGroupID, Scaffolding.mlsGroupID) - XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) - XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + try await context.perform { [self] in + XCTAssertEqual(mlsService.joinGroupWith_Invocations.count, 1) + let invokedMLSGroupID = try XCTUnwrap(mlsService.joinGroupWith_Invocations.first) + XCTAssertEqual(invokedMLSGroupID, Scaffolding.mlsGroupID) + XCTAssertEqual(user.oneOnOneConversation, mlsOneOnOneConversation) + XCTAssertEqual(mlsOneOnOneConversation.oneOnOneUser, user) + } } func testProcessEvent_It_Migrates_Proteus_Messages_To_MLS_Conversation() async throws { @@ -150,10 +158,12 @@ final class OneOnOneResolverTests: XCTestCase { let commonProtocol = WireDataModel.MessageProtocol.mls - let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol - ) + let (selfUser, user, mlsOneOnOneConversation) = try await context.perform { [self] in + try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol + ) + } // Mock @@ -169,14 +179,16 @@ final class OneOnOneResolverTests: XCTestCase { // Then - let migratedMessagesTexts = mlsOneOnOneConversation.allMessages - .compactMap(\.textMessageData) - .compactMap(\.messageText) - .sorted() + await context.perform { + let migratedMessagesTexts = mlsOneOnOneConversation.allMessages + .compactMap(\.textMessageData) + .compactMap(\.messageText) + .sorted() - /// Ensuring proteus messages were migrated to MLS conversation. - XCTAssertEqual(migratedMessagesTexts.first, "Hello") - XCTAssertEqual(migratedMessagesTexts.last, "World!") + /// Ensuring proteus messages were migrated to MLS conversation. + XCTAssertEqual(migratedMessagesTexts.first, "Hello") + XCTAssertEqual(migratedMessagesTexts.last, "World!") + } } func testProcessEvent_It_Resolves_Proteus_Conversation() async throws { @@ -184,12 +196,16 @@ final class OneOnOneResolverTests: XCTestCase { let commonProtocol = WireDataModel.MessageProtocol.proteus - let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( - selfUserProtocol: commonProtocol, - userProtocol: commonProtocol - ) + let (selfUser, user, mlsOneOnOneConversation) = try await context.perform { [self] in + try setupManagedObjects( + selfUserProtocol: commonProtocol, + userProtocol: commonProtocol + ) + } - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + await context.perform { + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + } // Mock @@ -205,7 +221,9 @@ final class OneOnOneResolverTests: XCTestCase { // Then - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + await context.perform { + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + } } func testProcessEvent_It_Resolves_Conversation_With_No_Common_Protocol() async throws { @@ -213,13 +231,17 @@ final class OneOnOneResolverTests: XCTestCase { let forcedReadOnly = false - let (selfUser, user, mlsOneOnOneConversation) = try setupManagedObjects( - selfUserProtocol: .mls, - userProtocol: .proteus, - forcedReadOnly: forcedReadOnly - ) + let (selfUser, user, mlsOneOnOneConversation) = try await context.perform { [self] in + try setupManagedObjects( + selfUserProtocol: .mls, + userProtocol: .proteus, + forcedReadOnly: forcedReadOnly + ) + } - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + await context.perform { + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, false) + } // Mock @@ -235,9 +257,11 @@ final class OneOnOneResolverTests: XCTestCase { // Then - let lastMessage = try XCTUnwrap(user.oneOnOneConversation?.lastMessage as? ZMSystemMessage) - XCTAssertEqual(lastMessage.systemMessageType, .mlsNotSupportedOtherUser) - XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + try await context.perform { + let lastMessage = try XCTUnwrap(user.oneOnOneConversation?.lastMessage as? ZMSystemMessage) + XCTAssertEqual(lastMessage.systemMessageType, .mlsNotSupportedOtherUser) + XCTAssertEqual(user.oneOnOneConversation?.isForcedReadOnly, true) + } } // MARK: - Setup @@ -294,12 +318,12 @@ final class OneOnOneResolverTests: XCTestCase { mlsOneOnOneConversation: ZMConversation, mlsConversationExists: Bool = false ) { - userRepository.fetchUserWithDomain_MockValue = user + userRepository.fetchUserIdDomain_MockValue = user userRepository.fetchSelfUser_MockValue = selfUser userRepository.fetchAllUserIDsWithOneOnOneConversation_MockValue = [Scaffolding.receiverQualifiedID.toDomainModel()] - conversationsRepository.pullMLSOneToOneConversationUserIDDomain_MockValue = Scaffolding.conversationID.uuidString - conversationsRepository.fetchMLSConversationWith_MockValue = mlsOneOnOneConversation + conversationsRepository.pullMLSOneToOneConversationUserIDUserDomain_MockValue = Scaffolding.conversationID.uuidString + conversationsRepository.fetchMLSConversationGroupID_MockValue = mlsOneOnOneConversation mlsService.establishGroupForWithRemovalKeys_MockValue = Scaffolding.ciphersuite mlsService.conversationExistsGroupID_MockValue = mlsConversationExists From 43ad84d82dbc601b203b4ac7f2984f18cb41149e Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:03:49 +0100 Subject: [PATCH 17/18] lint and format - WPB-10727 (#1999) --- .../Synchronization/OneOnOneResolverTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index b6b9f3081e6..243231aeb87 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -266,14 +266,14 @@ final class OneOnOneResolverTests: XCTestCase { // MARK: - Setup + typealias ManagedObjects = (selfUser: ZMUser, user: ZMUser, mlsConversation: ZMConversation) + private func setupManagedObjects( selfUserProtocol: WireDataModel.MessageProtocol, userProtocol: WireDataModel.MessageProtocol, forcedReadOnly: Bool = true, mlsEpoch: UInt64 = 0 - ) throws -> (selfUser: ZMUser, - user: ZMUser, - mlsConversation: ZMConversation) { + ) throws -> ManagedObjects { let user = modelHelper.createUser( id: Scaffolding.receiverQualifiedID.uuid, domain: Scaffolding.receiverQualifiedID.domain, From f5820cb2d18018fb4e8b6e2ba649cabc717db17c Mon Sep 17 00:00:00 2001 From: Jullian Mercier <31648126+jullianm@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:08:00 +0100 Subject: [PATCH 18/18] Trigger Build