Skip to content

Commit

Permalink
fix: misc individual to team fixes - WPB-14957 (#2284)
Browse files Browse the repository at this point in the history
Co-authored-by: John Nguyen <[email protected]>
Co-authored-by: François Benaiteau <[email protected]>
Co-authored-by: Christoph Aldrian <[email protected]>
  • Loading branch information
4 people authored Dec 13, 2024
1 parent af0e0b5 commit 4c6a8fb
Show file tree
Hide file tree
Showing 24 changed files with 338 additions and 79 deletions.
1 change: 1 addition & 0 deletions WireAPI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ let package = Package(
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
resources: [
.process("APIs/AccountsAPI/Resources"),
.process("APIs/BackendInfoAPI/Resources"),
.process("APIs/ConnectionsAPI/Resources"),
.process("APIs/ConversationsAPI/Resources"),
Expand Down
5 changes: 4 additions & 1 deletion WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ class AccountsAPIV7: AccountsAPIV6 {
requiringAccessToken: true
)

return try ResponseParser()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

return try ResponseParser(decoder: decoder)
.success(code: .ok, type: UpgradeToTeamResponseV7.self)
.failure(code: .forbidden, label: "user-already-in-a-team", error: AccountsAPIError.userAlreadyInATeam)
.failure(code: .notFound, label: "not-found", error: AccountsAPIError.userNotFound)
Expand Down
124 changes: 124 additions & 0 deletions WireAPI/Tests/WireAPITests/APIs/AccountsAPI/AccountsAPITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// 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 XCTest

@testable import WireAPI
@testable import WireAPISupport

final class AccountsAPITests: XCTestCase {

private var apiSnapshotHelper: APIServiceSnapshotHelper<any AccountsAPI>!

// MARK: - Setup

override func setUp() {
apiSnapshotHelper = APIServiceSnapshotHelper<any AccountsAPI> { apiService, apiVersion in
AccountsAPIBuilder(apiService: apiService)
.makeAPI(for: apiVersion)

}
}

override func tearDown() {
apiSnapshotHelper = nil
}

// MARK: - Request generation

func testUpgradeToTeam_V0_To_V6() async throws {
// Given
let apiService = MockAPIServiceProtocol()
let builder = AccountsAPIBuilder(apiService: apiService)

for apiVersion in [APIVersion.v0, .v1, .v2, .v3, .v4, .v5, .v6] {
let sut = builder.makeAPI(for: apiVersion)

// Then
await XCTAssertThrowsErrorAsync(AccountsAPIError.unsupportedEndpointForAPIVersion) {
try await sut.upgradeToTeam(teamName: Scaffolding.teamName)
}
}
}

func testUpgradeToTeam_Request_Generation_V7_Onwards() async throws {
// Given
let apiVersions = APIVersion.v7.andNextVersions

// Then
try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in
// When
_ = try await sut.upgradeToTeam(teamName: Scaffolding.teamName)
}
}

// MARK: - Response handling

func testUpgradeToTeam_Response_Handling_V7_Success() async throws {
// Given
let apiService = MockAPIServiceProtocol.withResponses([
(.ok, "UpgradeToTeamSuccessResponse")
])

let sut = AccountsAPIV7(apiService: apiService)

// When
let response = try await sut.upgradeToTeam(teamName: Scaffolding.teamName)

// Then
XCTAssertEqual(response, UpgradedAccountTeam(teamId: Scaffolding.teamID, teamName: Scaffolding.teamName))
}

func testUpgradeToTeam_Response_Handling_V7_User_Already_In_A_Team() async throws {
// Given
let apiService = MockAPIServiceProtocol.withResponses([
(.forbidden, "UpgradeToTeamErrorResponse_UserAlreadyInATeam")
])

let sut = AccountsAPIV7(apiService: apiService)

// Then
await XCTAssertThrowsErrorAsync(AccountsAPIError.userAlreadyInATeam) {
// When
try await sut.upgradeToTeam(teamName: Scaffolding.teamName)
}
}

func testUpgradeToTeam_Response_Handling_V7_User_Not_Found() async throws {
// Given
let apiService = MockAPIServiceProtocol.withResponses([
(.notFound, "UpgradeToTeamErrorResponse_UserNotFound")
])

let sut = AccountsAPIV7(apiService: apiService)

// Then
await XCTAssertThrowsErrorAsync(AccountsAPIError.userNotFound) {
// When
try await sut.upgradeToTeam(teamName: Scaffolding.teamName)
}
}

}

private enum Scaffolding {

static let teamName = "iOS Team"
static let teamID = UUID(uuidString: "66dc3593-4c3a-49e8-b5c3-d3d908bd7403")!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"code": 403,
"label": "user-already-in-a-team",
"message": "Switching teams is not allowed"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"code": 404,
"label": "not-found",
"message": "User not found"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"team_id": "66dc3593-4c3a-49e8-b5c3-d3d908bd7403",
"team_name": "iOS Team"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--request POST \
--header "Content-Type: application/json" \
--data "{\"name\":\"iOS Team\",\"icon\":\"default\"}" \
"upgrade-personal-to-team"
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,35 @@

import Foundation
@preconcurrency import WireAPI
@preconcurrency import WireDataModel
import WireDomainAPI
import WireLogging
import WireSystem

public struct IndividualToTeamMigrationUseCaseImplementation: IndividualToTeamMigrationUseCase {

private let accountsAPI: AccountsAPI
private let context: NSManagedObjectContext
private let logger: WireLogger = .individualToTeamMigration

public init(apiService: APIServiceProtocol, apiVersion: APIVersion) {
self.accountsAPI = AccountsAPIBuilder(apiService: apiService).makeAPI(for: apiVersion)
public init(
accountsAPI: AccountsAPI,
context: NSManagedObjectContext
) {
self.accountsAPI = accountsAPI
self.context = context
}

public func invoke(teamName: String) async throws -> IndividualToTeamMigrationResult {
logger.debug("Migrating individual account to team account")
do {
let upgradeResult = try await accountsAPI.upgradeToTeam(teamName: teamName)
logger.debug("Individual account migrated to team account")
logger.debug("Individual account migrated to team account, storing team locally...")
try await createTeamLocally(id: upgradeResult.teamId, name: upgradeResult.teamName)
logger.debug("Individual to team migration completed successfully")
return IndividualToTeamMigrationResult(teamID: upgradeResult.teamId, teamName: upgradeResult.teamName)
} catch {
logger.error("Failed to migrate individual account to team account")
logger.error("Failed to migrate individual account to team account: \(error.localizedDescription)")
switch error {
case AccountsAPIError.userAlreadyInATeam:
throw IndividualToTeamMigrationError.userAlreadyInTeam
Expand All @@ -46,4 +55,34 @@ public struct IndividualToTeamMigrationUseCaseImplementation: IndividualToTeamMi
}
}
}

private func createTeamLocally(
id: UUID,
name: String
) async throws {
try await context.perform { [context] in
let team = Team.fetchOrCreate(
with: id,
in: context
)

team.name = name
team.needsToBeUpdatedFromBackend = true

// Probably there's nothing to fetch since it's a brand
// new team, but just in case.
team.needsToDownloadRoles = true
team.needsToRedownloadMembers = true

let selfUser = ZMUser.selfUser(in: context)

_ = Member.getOrUpdateMember(
for: selfUser,
in: team,
context: context
)

try context.save()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ public enum KeychainQueryItem: Hashable, Equatable, Sendable {
case let .account(string):
(kSecAttrAccount, string)

case let .itemClass(itemClass):
(kSecClass, itemClass)
case .itemClass(.genericPassword):
(kSecClass, kSecClassGenericPassword)

case .accessible(.afterFirstUnlock):
(kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock)
Expand Down
6 changes: 4 additions & 2 deletions WireUI/Sources/WireDesign/Colors/ColorTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public enum ColorTheme {
public static let onHighlight = UIColor(light: .black, dark: .black)

public static let secondaryText = UIColor(light: .gray70, dark: .gray60)

public static let requiredField = UIColor(light: .red500Light, dark: .red500Dark)
}

public enum Backgrounds {
Expand Down Expand Up @@ -124,8 +126,8 @@ public enum ColorTheme {
}

public enum Checkbox {
public static let enabled = UIColor(light: .blue500Light, dark: .blue500Dark)
public static let disabled = UIColor(light: .gray50, dark: .gray80)
public static let enabled = UIColor(light: .gray50, dark: .gray80)
public static let selected = UIColor(light: .blue500Light, dark: .blue500Dark)
}

public enum Strokes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,9 @@ struct Checkbox: View {
}, label: {
Image(systemName: isChecked ? "checkmark.square.fill" : "square")
.font(.system(size: 24))
.backgroundStyle(Color(
uiColor: isChecked ? ColorTheme.Checkbox.enabled : ColorTheme.Checkbox
.disabled
))
})
.buttonStyle(.plain)
.foregroundStyle(isChecked ? ColorTheme.Checkbox.selected.color : ColorTheme.Checkbox.enabled.color)
Text(title)
.wireTextStyle(.subline1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,25 @@ struct PageContainer<Content: View>: View {
}

var body: some View {
ScrollView {
VStack {
Text(String.formated(key: "individualToTeam.progressCount", bundle: .module, step, stepCount))
.wireTextStyle(.subline1)
.foregroundStyle(Color(uiColor: ColorTheme.Base.secondaryText))
Spacer()
.frame(height: 12)
Text(stepTitle)
.wireTextStyle(.h2)
Spacer(minLength: 36)
content
GeometryReader { proxy in
ScrollView {
VStack {
Text(String.formated(key: "individualToTeam.progressCount", bundle: .module, step, stepCount))
.wireTextStyle(.subline1)
.foregroundStyle(Color(uiColor: ColorTheme.Base.secondaryText))
Spacer()
.frame(height: 12)
Text(stepTitle)
.wireTextStyle(.h2)
Spacer(minLength: 36)
content
}
.frame(
minHeight: proxy.size.height - 24
)
}
.padding(.horizontal, 16)
.padding(.bottom, 24)
}
.padding(.horizontal, 16)
.padding(.bottom, 24)
}
}
Loading

0 comments on commit 4c6a8fb

Please sign in to comment.