From 7e9c9339db25136c6956ccd86385cbfed17a96bf Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:35:28 +1000 Subject: [PATCH 01/21] Point SDK source back to AvdLee Master --- Package.resolved | 2 +- Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 1eea750a..0c29905c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,7 +3,7 @@ "pins": [ { "package": "AppStoreConnect-Swift-SDK", - "repositoryURL": "https://github.com/ittybittyapps/appstoreconnect-swift-sdk.git", + "repositoryURL": "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", "state": { "branch": "master", "revision": "65d95d0979734597e7fb7d2d30028c659594ac53", diff --git a/Package.swift b/Package.swift index f89119a3..18e8e694 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( from: "0.0.2" ), .package( - url: "https://github.com/ittybittyapps/appstoreconnect-swift-sdk.git", + url: "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", .branch("master") ), .package( From 73ce2cc04589c985105cb06faa4cbc8c3a515df5 Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:37:46 +1000 Subject: [PATCH 02/21] update fileContent() -> Data to return FileContent --- .../Certificates/CertificateProcessor.swift | 4 ++-- Sources/FileSystem/FileProvider.swift | 18 ++++++++++++++++++ .../FileSystem/Profile/ProfileProcessor.swift | 4 ++-- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 Sources/FileSystem/FileProvider.swift diff --git a/Sources/FileSystem/Certificates/CertificateProcessor.swift b/Sources/FileSystem/Certificates/CertificateProcessor.swift index 89d9e92f..e52d7b64 100644 --- a/Sources/FileSystem/Certificates/CertificateProcessor.swift +++ b/Sources/FileSystem/Certificates/CertificateProcessor.swift @@ -36,7 +36,7 @@ extension Certificate: FileProvider { } } - func fileContent() throws -> Data { + func fileContent() throws -> FileContent { guard let content = content, let data = Data(base64Encoded: content) @@ -44,7 +44,7 @@ extension Certificate: FileProvider { throw Error.noContent } - return data + return .data(data) } var fileName: String { diff --git a/Sources/FileSystem/FileProvider.swift b/Sources/FileSystem/FileProvider.swift new file mode 100644 index 00000000..a2b085f2 --- /dev/null +++ b/Sources/FileSystem/FileProvider.swift @@ -0,0 +1,18 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Foundation + +protocol FileProvider: FileNameProvider, FileContentProvider { } + +protocol FileNameProvider { + var fileName: String { get } +} + +enum FileContent { + case data(Data) + case string(String) +} + +protocol FileContentProvider { + func fileContent() throws -> FileContent +} diff --git a/Sources/FileSystem/Profile/ProfileProcessor.swift b/Sources/FileSystem/Profile/ProfileProcessor.swift index 310dcc8c..eb34912c 100644 --- a/Sources/FileSystem/Profile/ProfileProcessor.swift +++ b/Sources/FileSystem/Profile/ProfileProcessor.swift @@ -38,14 +38,14 @@ extension Profile: FileProvider { } } - func fileContent() throws -> Data { + func fileContent() throws -> FileContent { guard let content = profileContent, let data = Data(base64Encoded: content) else { throw Error.noContent } - return data + return .data(data) } var fileName: String { From a9b95c751457d429af7518fd6a3640fb60fdc0df Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:40:53 +1000 Subject: [PATCH 03/21] Add id to beta group, make group name non-nil --- .../AppStoreConnectCLI/Model/BetaGroup.swift | 22 ++++++++++++++++--- Sources/Model/BetaGroup.swift | 12 +++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift index 979f1982..935b23ba 100755 --- a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift @@ -29,7 +29,7 @@ extension BetaGroup: TableInfoProvider, ResultRenderable { app.id, app.bundleId ?? "", app.name ?? "", - groupName ?? "", + groupName, isInternal ?? "", publicLink ?? "", publicLinkEnabled ?? "", @@ -41,13 +41,29 @@ extension BetaGroup: TableInfoProvider, ResultRenderable { } extension BetaGroup { + enum Error: LocalizedError { + case invalidName + + var errorDescription: String? { + switch self { + case .invalidName: + return "Beta group doesn't have a valid group name." + } + } + } + init( _ apiApp: AppStoreConnect_Swift_SDK.App, _ apiBetaGroup: AppStoreConnect_Swift_SDK.BetaGroup - ) { + ) throws { + guard let groupName = apiBetaGroup.attributes?.name else { + throw Error.invalidName + } + self.init( app: App(apiApp), - groupName: apiBetaGroup.attributes?.name, + id: apiBetaGroup.id, + groupName: groupName, isInternal: apiBetaGroup.attributes?.isInternalGroup, publicLink: apiBetaGroup.attributes?.publicLink, publicLinkEnabled: apiBetaGroup.attributes?.publicLinkEnabled, diff --git a/Sources/Model/BetaGroup.swift b/Sources/Model/BetaGroup.swift index 13cfc074..d92f55d8 100644 --- a/Sources/Model/BetaGroup.swift +++ b/Sources/Model/BetaGroup.swift @@ -4,25 +4,30 @@ import Foundation public struct BetaGroup: Codable, Equatable { public let app: App - public let groupName: String? + public let id: String? + public let groupName: String public let isInternal: Bool? public let publicLink: String? public let publicLinkEnabled: Bool? public let publicLinkLimit: Int? public let publicLinkLimitEnabled: Bool? public let creationDate: String? + public var testers: String? // tester csv file path public init( app: App, - groupName: String?, + id: String?, + groupName: String, isInternal: Bool?, publicLink: String?, publicLinkEnabled: Bool?, publicLinkLimit: Int?, publicLinkLimitEnabled: Bool?, - creationDate: String? + creationDate: String?, + testers: String? = nil ) { self.app = app + self.id = id self.groupName = groupName self.isInternal = isInternal self.publicLink = publicLink @@ -30,5 +35,6 @@ public struct BetaGroup: Codable, Equatable { self.publicLinkLimit = publicLinkLimit self.publicLinkLimitEnabled = publicLinkLimitEnabled self.creationDate = creationDate + self.testers = testers } } From 9e564bdffa1f5ea940b732ca6abb050193696c5b Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:42:04 +1000 Subject: [PATCH 04/21] Add delete to processor with some tweaks, create betagroup processor --- .../Beta Group/BetaGroupProcessor.swift | 44 ++++++++++++++++ Sources/FileSystem/ResourceProcessor.swift | 52 ++++++++++++------- 2 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 Sources/FileSystem/Beta Group/BetaGroupProcessor.swift diff --git a/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift new file mode 100644 index 00000000..391cf4ae --- /dev/null +++ b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift @@ -0,0 +1,44 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Files +import Foundation +import Model +import Yams + +public struct BetaGroupProcessor: ResourceProcessor { + + public init(path: ResourcePath) { + self.path = path + } + + func write(_: [BetaGroup]) throws -> [File] { + fatalError() + } + + var path: ResourcePath + + func write(_ betaGroup: BetaGroup) throws -> File { + try writeFile(betaGroup) + } + + func read() throws -> [BetaGroup] { + fatalError() + } + + public func write(groupsWithTesters: [(betaGroup: BetaGroup, testers: [BetaTester])]) throws { + deleteFile() + + try groupsWithTesters.map { try write($0.betaGroup) } + } + +} + +extension BetaGroup: FileProvider { + var fileName: String { + "\(app.id)_\(groupName).yml" + } + + func fileContent() throws -> FileContent { + .string(try YAMLEncoder().encode(self)) + } +} diff --git a/Sources/FileSystem/ResourceProcessor.swift b/Sources/FileSystem/ResourceProcessor.swift index db675b8f..c42c1d04 100644 --- a/Sources/FileSystem/ResourceProcessor.swift +++ b/Sources/FileSystem/ResourceProcessor.swift @@ -37,33 +37,47 @@ extension ResourceWriter { func writeFile(_ resource: FileProvider) throws -> File { var file: File + var folder: Folder + var fileName: String switch path { case .file(let path): let standardizedPath = path as NSString - file = try Folder(path: standardizedPath.deletingLastPathComponent) - .createFile( - named: standardizedPath.lastPathComponent, - contents: resource.fileContent() - ) + folder = try Folder(path: "").createSubfolderIfNeeded(at: standardizedPath.deletingLastPathComponent) + fileName = standardizedPath.lastPathComponent case .folder(let folderPath): - file = try Folder(path: folderPath) - .createFile( - named: resource.fileName, - contents: resource.fileContent() - ) + fileName = resource.fileName + folder = try Folder(path: "").createSubfolderIfNeeded(at: folderPath) + } + + switch try resource.fileContent() { + case .data(let data): + file = try folder.createFileIfNeeded(withName: fileName, contents: data) + case .string(let string): + file = try folder.createFileIfNeeded(at: fileName) + try file.write(string) } return file } -} - -protocol FileProvider: FileNameProvider, FileContentProvider { } -protocol FileNameProvider { - var fileName: String { get } -} - -protocol FileContentProvider { - func fileContent() throws -> Data + func deleteFile() { + do { + switch path { + case .file(let filePath): + let standardizedPath = filePath as NSString + try Folder(path: standardizedPath.deletingLastPathComponent) + .files.forEach { + if $0.name == standardizedPath.lastPathComponent { + try $0.delete() + } + } + case .folder(let folderPath): + try Folder(path: folderPath) + .files.forEach { try $0.delete() } + } + } catch { + print("\(error)") + } + } } From 5226dfc3a7f6038aba82f2816fed4558482faec4 Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:42:44 +1000 Subject: [PATCH 05/21] Remove not found error in ListBetaTesterOperation. make options optional --- .../Operations/ListBetaTestersOperation.swift | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Services/Operations/ListBetaTestersOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/ListBetaTestersOperation.swift index 1f246525..7943901f 100644 --- a/Sources/AppStoreConnectCLI/Services/Operations/ListBetaTestersOperation.swift +++ b/Sources/AppStoreConnectCLI/Services/Operations/ListBetaTestersOperation.swift @@ -7,26 +7,15 @@ import Foundation struct ListBetaTestersOperation: APIOperation { struct Options { - let email: String? - let firstName: String? - let lastName: String? - let inviteType: BetaInviteType? - let appIds: [String]? - let groupIds: [String]? - let sort: ListBetaTesters.Sort? - let limit: Int? - let relatedResourcesLimit: Int? - } - - enum Error: LocalizedError { - case notFound - - var errorDescription: String? { - switch self { - case .notFound: - return "Beta testers with provided filters not found." - } - } + var email: String? + var firstName: String? + var lastName: String? + var inviteType: BetaInviteType? + var appIds: [String]? + var groupIds: [String]? + var sort: ListBetaTesters.Sort? + var limit: Int? + var relatedResourcesLimit: Int? } private let options: Options @@ -107,13 +96,9 @@ struct ListBetaTestersOperation: APIOperation { next: $0 ) } - .tryMap { (responses: [BetaTestersResponse]) throws -> Output in - try responses.flatMap { (response: BetaTestersResponse) -> Output in - guard !response.data.isEmpty else { - throw Error.notFound - } - - return response.data.map { + .map { + $0.flatMap { (response: BetaTestersResponse) -> Output in + response.data.map { .init(betaTester: $0, includes: response.included) } } From 265826f17239d17eb72777e7de44c6b5c66a7fe6 Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:43:03 +1000 Subject: [PATCH 06/21] Create service function pullBetaGroups --- .../Services/AppStoreConnectService.swift | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index ae24a37d..cca93ed3 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -410,7 +410,7 @@ class AppStoreConnectService { .execute(with: requestor) .await() - return Model.BetaGroup(app, betaGroup) + return try Model.BetaGroup(app, betaGroup) } func createBetaGroup( @@ -432,7 +432,7 @@ class AppStoreConnectService { ) let betaGroupResponse = createBetaGroupOperation.execute(with: requestor) - return try betaGroupResponse.map(Model.BetaGroup.init).await() + return try betaGroupResponse.tryMap(Model.BetaGroup.init).await() } func deleteBetaGroup(appBundleId: String, betaGroupName: String) throws { @@ -507,7 +507,29 @@ class AppStoreConnectService { let modifyBetaGroupOperation = ModifyBetaGroupOperation(options: modifyBetaGroupOptions) let modifiedBetaGroup = try modifyBetaGroupOperation.execute(with: requestor).await() - return Model.BetaGroup(app, modifiedBetaGroup) + return try Model.BetaGroup(app, modifiedBetaGroup) + } + + func pullBetaGroups() throws -> [(betaGroup: Model.BetaGroup, testers: [Model.BetaTester])] { + let groupOutputs = try ListBetaGroupsOperation(options: .init(appIds: [], names: [], sort: nil)).execute(with: requestor).await() + + return try groupOutputs.map { + let testers = try ListBetaTestersOperation( + options: .init(groupIds: [$0.betaGroup.id]) + ) + .execute(with: requestor) + .await() + .map(BetaTester.init) + + return (try BetaGroup($0.app, $0.betaGroup), testers) + } + } + + func updateBetaGroup(betaGroup: Model.BetaGroup) throws { + // TODO +// _ = try UpdateBetaGroupOperation(options: .init(betaGroup: betaGroup)) +// .execute(with: requestor) +// .await() } func readBuild(bundleId: String, buildNumber: String, preReleaseVersion: String) throws -> Model.Build { From 701f95e9109115cf6c2d709217043136f1f9f47e Mon Sep 17 00:00:00 2001 From: Decheng Date: Thu, 11 Jun 2020 17:43:32 +1000 Subject: [PATCH 07/21] Implement basic pull beta groups command --- .../BetaGroups/BetaGroupCommand.swift | 1 + .../Sync/PullBetaGroupsCommand.swift | 30 +++++++++++++++++++ .../Sync/PushBetaGroupsCommand.swift | 28 +++++++++++++++++ .../Sync/SyncBetaGroupsCommand.swift | 17 +++++++++++ 4 files changed, 76 insertions(+) create mode 100644 Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift create mode 100644 Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift create mode 100644 Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/BetaGroupCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/BetaGroupCommand.swift index f677c506..405af792 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/BetaGroupCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/BetaGroupCommand.swift @@ -18,6 +18,7 @@ struct TestFlightBetaGroupCommand: ParsableCommand { ReadBetaGroupCommand.self, RemoveTestersFromGroupCommand.self, AddTestersToGroupCommand.self, + SyncBetaGroupsCommand.self, ], defaultSubcommand: ListBetaGroupsCommand.self ) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift new file mode 100644 index 00000000..4a0849d4 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift @@ -0,0 +1,30 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import ArgumentParser +import FileSystem + +struct PullBetaGroupsCommand: CommonParsableCommand { + + static var configuration = CommandConfiguration( + commandName: "pull", + abstract: "Pull down server beta groups, refresh local beta group config files" + ) + + @OptionGroup() + var common: CommonOptions + + @Option( + default: "./config/betagroups", + help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')" + ) var outputPath: String + + func run() throws { + let service = try makeService() + + let betaGroupWithTesters = try service.pullBetaGroups() + + try BetaGroupProcessor(path: .folder(path: outputPath)) + .write(groupsWithTesters: betaGroupWithTesters) + } + +} diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift new file mode 100644 index 00000000..ce4e03a8 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift @@ -0,0 +1,28 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import ArgumentParser +import struct Model.BetaGroup + +struct PushBetaGroupsCommand: CommonParsableCommand { + + static var configuration = CommandConfiguration( + commandName: "push", + abstract: "Push local beta group config files to server, update server beta groups" + ) + + @OptionGroup() + var common: CommonOptions + + @Option( + default: "./config/betagroups", + help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')" + ) var inputPath: String + + @Flag(help: "Perform a dry run.") + var dryRun: Bool + + func run() throws { + + } + +} diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift new file mode 100644 index 00000000..8c4166b2 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift @@ -0,0 +1,17 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import ArgumentParser + +struct SyncBetaGroupsCommand: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "sync", + abstract: """ + Sync information about beta groups with provided configuration file. + """, + subcommands: [ + PullBetaGroupsCommand.self, + PushBetaGroupsCommand.self, + ], + defaultSubcommand: PullBetaGroupsCommand.self + ) +} From 4688745de62922576536cf035ef39db4dbedea08 Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 11:42:03 +1000 Subject: [PATCH 08/21] Create BetaTesterProcessor for rendering CSV --- Package.resolved | 2 +- Package.swift | 2 +- .../Beta Tester/BetaTesterProcessor.swift | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 Sources/FileSystem/Beta Tester/BetaTesterProcessor.swift diff --git a/Package.resolved b/Package.resolved index 0c29905c..1eea750a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,7 +3,7 @@ "pins": [ { "package": "AppStoreConnect-Swift-SDK", - "repositoryURL": "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", + "repositoryURL": "https://github.com/ittybittyapps/appstoreconnect-swift-sdk.git", "state": { "branch": "master", "revision": "65d95d0979734597e7fb7d2d30028c659594ac53", diff --git a/Package.swift b/Package.swift index 18e8e694..f89119a3 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( from: "0.0.2" ), .package( - url: "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", + url: "https://github.com/ittybittyapps/appstoreconnect-swift-sdk.git", .branch("master") ), .package( diff --git a/Sources/FileSystem/Beta Tester/BetaTesterProcessor.swift b/Sources/FileSystem/Beta Tester/BetaTesterProcessor.swift new file mode 100644 index 00000000..f547d404 --- /dev/null +++ b/Sources/FileSystem/Beta Tester/BetaTesterProcessor.swift @@ -0,0 +1,45 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import CodableCSV +import Files +import Foundation +import Model + +struct BetaTesterProcessor { + + let folder: Folder + + typealias FilePath = String + + func write(group: BetaGroup, testers: [BetaTester]) throws -> FilePath { + let file = try folder.createFile(named: "\(group.app.bundleId ?? "")_\(group.groupName)_beta-testers.csv") + + try file.write(testers.renderAsCSV()) + + return file.name + } +} + +// TODO: merge this with ResultRenderable in main module +protocol CSVRenderable: Codable { + var headers: [String] { get } + var rows: [[String]] { get } +} + +extension CSVRenderable { + func renderAsCSV() -> String { + let wholeTable = [headers] + rows + + return try! CSVWriter.encode(rows: wholeTable, into: String.self) // swiftlint:disable:this force_try + } +} + +extension Array: CSVRenderable where Element == BetaTester { + var headers: [String] { + ["Email", "First Name", "Last Name", "Invite Type"] + } + + var rows: [[String]] { + self.map { [$0.email, $0.firstName, $0.lastName, $0.inviteType].compactMap { $0 } } + } +} From 077f78911245c3f8ca2dd5ca0c51b58d3b20b8ea Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 11:42:46 +1000 Subject: [PATCH 09/21] Wire BetaGroupProcessor up with TesterProcessor --- .../Beta Group/BetaGroupProcessor.swift | 17 +++++++++++-- Sources/FileSystem/ResourceProcessor.swift | 24 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift index 391cf4ae..32eebc2c 100644 --- a/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift +++ b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift @@ -17,6 +17,7 @@ public struct BetaGroupProcessor: ResourceProcessor { var path: ResourcePath + @discardableResult func write(_ betaGroup: BetaGroup) throws -> File { try writeFile(betaGroup) } @@ -28,14 +29,26 @@ public struct BetaGroupProcessor: ResourceProcessor { public func write(groupsWithTesters: [(betaGroup: BetaGroup, testers: [BetaTester])]) throws { deleteFile() - try groupsWithTesters.map { try write($0.betaGroup) } + let betagroups = try groupsWithTesters + .map { try write(betaTesters: $0.testers, into: $0.betaGroup) } + + try betagroups.forEach { try write($0) } + } + + private func write(betaTesters: [BetaTester], into betaGroup: BetaGroup) throws -> BetaGroup { + let testerProcessor = BetaTesterProcessor(folder: try getFolder()) + + var group = betaGroup + group.testers = try testerProcessor.write(group: group, testers: betaTesters) + + return group } } extension BetaGroup: FileProvider { var fileName: String { - "\(app.id)_\(groupName).yml" + "\(app.bundleId ?? "")_\(groupName).yml" } func fileContent() throws -> FileContent { diff --git a/Sources/FileSystem/ResourceProcessor.swift b/Sources/FileSystem/ResourceProcessor.swift index c42c1d04..07f9a17c 100644 --- a/Sources/FileSystem/ResourceProcessor.swift +++ b/Sources/FileSystem/ResourceProcessor.swift @@ -33,23 +33,37 @@ protocol PathProvider { var path: ResourcePath { get } } +extension PathProvider { + func getFolder() throws -> Folder { + var folder: Folder + switch path { + case .file(let path): + let standardizedPath = path as NSString + folder = try Folder(path: "").createSubfolderIfNeeded(at: standardizedPath.deletingLastPathComponent) + case .folder(let folderPath): + folder = try Folder(path: "").createSubfolderIfNeeded(at: folderPath) + } + + return folder + } +} + extension ResourceWriter { func writeFile(_ resource: FileProvider) throws -> File { var file: File - var folder: Folder var fileName: String switch path { case .file(let path): let standardizedPath = path as NSString - folder = try Folder(path: "").createSubfolderIfNeeded(at: standardizedPath.deletingLastPathComponent) fileName = standardizedPath.lastPathComponent - case .folder(let folderPath): + case .folder(_): fileName = resource.fileName - folder = try Folder(path: "").createSubfolderIfNeeded(at: folderPath) } + let folder: Folder = try getFolder() + switch try resource.fileContent() { case .data(let data): file = try folder.createFileIfNeeded(withName: fileName, contents: data) @@ -77,7 +91,7 @@ extension ResourceWriter { .files.forEach { try $0.delete() } } } catch { - print("\(error)") + // Skip delete failed error, if folder is missing. } } } From 4b74c09fd9b58bd1667919a710aae6213599dde3 Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:17:21 +1000 Subject: [PATCH 10/21] Model.BetaGroup + Hashable, Equatable --- Sources/Model/BetaGroup.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/Model/BetaGroup.swift b/Sources/Model/BetaGroup.swift index d92f55d8..c360094c 100644 --- a/Sources/Model/BetaGroup.swift +++ b/Sources/Model/BetaGroup.swift @@ -38,3 +38,21 @@ public struct BetaGroup: Codable, Equatable { self.testers = testers } } + +extension BetaGroup: Hashable { + public static func == (lhs: BetaGroup, rhs: BetaGroup) -> Bool { + return lhs.id == rhs.id && + lhs.groupName == rhs.groupName && + lhs.publicLinkEnabled == rhs.publicLinkEnabled && + lhs.publicLinkLimit == rhs.publicLinkLimit && + lhs.publicLinkLimitEnabled == rhs.publicLinkLimitEnabled + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(groupName) + hasher.combine(publicLinkEnabled) + hasher.combine(publicLinkLimit) + hasher.combine(publicLinkLimitEnabled) + } +} From 94e03c8052e402e7056dd0ce3164d1d283ebfa6b Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:17:52 +1000 Subject: [PATCH 11/21] Add read() to BetaGroupProcessor --- .../Beta Group/BetaGroupProcessor.swift | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift index 32eebc2c..cb989ae5 100644 --- a/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift +++ b/Sources/FileSystem/Beta Group/BetaGroupProcessor.swift @@ -7,23 +7,22 @@ import Yams public struct BetaGroupProcessor: ResourceProcessor { + var path: ResourcePath + public init(path: ResourcePath) { self.path = path } - func write(_: [BetaGroup]) throws -> [File] { - fatalError() - } - - var path: ResourcePath - - @discardableResult - func write(_ betaGroup: BetaGroup) throws -> File { - try writeFile(betaGroup) - } + public func read() throws -> [BetaGroup] { + try getFolder().files.compactMap { (file: File) -> BetaGroup? in + if file.extension == "yml" { + return Readers + .FileReader(format: .yaml) + .read(filePath: file.path) + } - func read() throws -> [BetaGroup] { - fatalError() + return nil + } } public func write(groupsWithTesters: [(betaGroup: BetaGroup, testers: [BetaTester])]) throws { @@ -32,14 +31,24 @@ public struct BetaGroupProcessor: ResourceProcessor { let betagroups = try groupsWithTesters .map { try write(betaTesters: $0.testers, into: $0.betaGroup) } - try betagroups.forEach { try write($0) } + try write(betagroups) + } + + @discardableResult + func write(_ betaGroups: [BetaGroup]) throws -> [File] { + try betaGroups.map { try write($0) } + } + + @discardableResult + func write(_ betaGroup: BetaGroup) throws -> File { + try writeFile(betaGroup) } private func write(betaTesters: [BetaTester], into betaGroup: BetaGroup) throws -> BetaGroup { - let testerProcessor = BetaTesterProcessor(folder: try getFolder()) var group = betaGroup - group.testers = try testerProcessor.write(group: group, testers: betaTesters) + group.testers = try BetaTesterProcessor(folder: try getFolder()) + .write(group: group, testers: betaTesters) return group } From 1c50987956957af5433a148519a4c612079e6288 Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:18:56 +1000 Subject: [PATCH 12/21] Implement UpdateBetaGroupOperation, add update and delete service func --- .../Services/AppStoreConnectService.swift | 13 ++++--- .../Operations/UpdateBetaGroupOperation.swift | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 Sources/AppStoreConnectCLI/Services/Operations/UpdateBetaGroupOperation.swift diff --git a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift index cca93ed3..0ec2253f 100644 --- a/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift +++ b/Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift @@ -526,10 +526,15 @@ class AppStoreConnectService { } func updateBetaGroup(betaGroup: Model.BetaGroup) throws { - // TODO -// _ = try UpdateBetaGroupOperation(options: .init(betaGroup: betaGroup)) -// .execute(with: requestor) -// .await() + _ = try UpdateBetaGroupOperation(options: .init(betaGroup: betaGroup)) + .execute(with: requestor) + .await() + } + + func deleteBetaGroup(with id: String) throws { + try DeleteBetaGroupOperation(options: .init(betaGroupId: id)) + .execute(with: requestor) + .await() } func readBuild(bundleId: String, buildNumber: String, preReleaseVersion: String) throws -> Model.Build { diff --git a/Sources/AppStoreConnectCLI/Services/Operations/UpdateBetaGroupOperation.swift b/Sources/AppStoreConnectCLI/Services/Operations/UpdateBetaGroupOperation.swift new file mode 100644 index 00000000..abe72722 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Services/Operations/UpdateBetaGroupOperation.swift @@ -0,0 +1,34 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import AppStoreConnect_Swift_SDK +import Combine +import Foundation +import struct Model.BetaGroup + +struct UpdateBetaGroupOperation: APIOperation { + + struct Options { + let betaGroup: BetaGroup + } + + private let options: Options + + init(options: Options) { + self.options = options + } + + func execute(with requestor: EndpointRequestor) throws -> AnyPublisher { + let betaGroup = options.betaGroup + + let endpoint = APIEndpoint.modify( + betaGroupWithId: betaGroup.id!, + name: betaGroup.groupName, + publicLinkEnabled: betaGroup.publicLinkEnabled, + publicLinkLimit: betaGroup.publicLinkLimit, + publicLinkLimitEnabled: betaGroup.publicLinkLimitEnabled + ) + + return requestor.request(endpoint).eraseToAnyPublisher() + } + +} From ede1d0fd228d1f5e81a95b3a908193d77247c870 Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:20:40 +1000 Subject: [PATCH 13/21] Create SyncStrategy, SyncResultRenderable, SyncResultRenderer --- .../Readers and Renderers/Renderers.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift index bf37fcdf..0a09dc8c 100644 --- a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift +++ b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift @@ -113,3 +113,42 @@ extension ResultRenderable where Self: TableInfoProvider { return table.render() } } + +protocol SyncResultRenderable { + var syncResultText: String { get } +} + +enum SyncStrategy { + case delete(T) + case create(T) + case update(T) +} + +extension Renderers { + + struct SyncResultRenderer { + + func render(_ strategy: [SyncStrategy], isDryRun: Bool) { + strategy.forEach { renderResultText($0, isDryRun) } + } + + func render(_ strategy: SyncStrategy, isDryRun: Bool) { + renderResultText(strategy, isDryRun) + } + + private func renderResultText(_ strategy: SyncStrategy, _ isDryRun: Bool) { + let resultText: String + switch strategy { + case .create(let input): + resultText = "➕ \(input.syncResultText)" + case .delete(let input): + resultText = "➖ \(input.syncResultText)" + case .update(let input): + resultText = "⬆️ \(input.syncResultText)" + } + + print("\(isDryRun ? "" : "✅") \(resultText)") + } + } + +} From b7d3cce6bf74afe352fc3cce969c6dc8531f6a34 Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:23:26 +1000 Subject: [PATCH 14/21] Wire up PushBetaGroupCommand with sync funcs and strategies --- .../Sync/PushBetaGroupsCommand.swift | 61 +++++++++++++++++++ .../AppStoreConnectCLI/Model/BetaGroup.swift | 6 ++ 2 files changed, 67 insertions(+) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift index ce4e03a8..10a1e37f 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift @@ -1,6 +1,8 @@ // Copyright 2020 Itty Bitty Apps Pty Ltd import ArgumentParser +import FileSystem +import Foundation import struct Model.BetaGroup struct PushBetaGroupsCommand: CommonParsableCommand { @@ -22,7 +24,66 @@ struct PushBetaGroupsCommand: CommonParsableCommand { var dryRun: Bool func run() throws { + let service = try makeService() + let resourceProcessor = BetaGroupProcessor(path: .folder(path: inputPath)) + + let serverGroups = Set(try service.pullBetaGroups().map{ $0.betaGroup }) + let localGroups = Set(try resourceProcessor.read()) + + let strategies = compareGroups( + localGroups: localGroups, + serverGroups: serverGroups + ) + + let renderer = Renderers.SyncResultRenderer() + + if dryRun { + renderer.render(strategies, isDryRun: true) + } else { + try strategies.forEach { (strategy: SyncStrategy) in + switch strategy { + case .create(let group): + _ = try service.createBetaGroup( + appBundleId: group.app.bundleId!, + groupName: group.groupName, + publicLinkEnabled: group.publicLinkEnabled ?? false, + publicLinkLimit: group.publicLinkLimit + ) + case .delete(let group): + try service.deleteBetaGroup(with: group.id!) + case .update(let group): + try service.updateBetaGroup(betaGroup: group) + } + + renderer.render(strategy, isDryRun: false) + } + + let betaGroupWithTesters = try service.pullBetaGroups() + + try resourceProcessor.write(groupsWithTesters: betaGroupWithTesters) + } + } + + func compareGroups(localGroups: Set, serverGroups: Set) -> [SyncStrategy] { + var strategies: [SyncStrategy] = [] + + let groupToCreate = localGroups.subtracting(serverGroups) + let groupToDelete = serverGroups.subtracting(localGroups) + + groupToDelete.forEach { group in + if !localGroups.contains(where: { group.id == $0.id }) { + strategies.append(.delete(group)) + } + } + + groupToCreate.forEach { group in + serverGroups.contains(where: { group.id == $0.id }) + ? strategies.append(.update(group)) + : strategies.append(.create(group)) + } + + return strategies } } diff --git a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift index 935b23ba..e29e452a 100755 --- a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift @@ -73,3 +73,9 @@ extension BetaGroup { ) } } + +extension BetaGroup: SyncResultRenderable { + var syncResultText: String { + "\(app.bundleId ?? "" )_\(groupName)" + } +} From bc1f122e312610c5126cc91e85844e891d70890c Mon Sep 17 00:00:00 2001 From: Decheng Date: Fri, 12 Jun 2020 14:34:54 +1000 Subject: [PATCH 15/21] Reformat files for linting get passed --- .../TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift | 2 +- .../AppStoreConnectCLI/Readers and Renderers/Renderers.swift | 2 +- Sources/FileSystem/ResourceProcessor.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift index 10a1e37f..8b6b561d 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift @@ -28,7 +28,7 @@ struct PushBetaGroupsCommand: CommonParsableCommand { let resourceProcessor = BetaGroupProcessor(path: .folder(path: inputPath)) - let serverGroups = Set(try service.pullBetaGroups().map{ $0.betaGroup }) + let serverGroups = Set(try service.pullBetaGroups().map { $0.betaGroup }) let localGroups = Set(try resourceProcessor.read()) let strategies = compareGroups( diff --git a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift index 0a09dc8c..eb4857f7 100644 --- a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift +++ b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift @@ -150,5 +150,5 @@ extension Renderers { print("\(isDryRun ? "" : "✅") \(resultText)") } } - + } diff --git a/Sources/FileSystem/ResourceProcessor.swift b/Sources/FileSystem/ResourceProcessor.swift index 07f9a17c..418e29ef 100644 --- a/Sources/FileSystem/ResourceProcessor.swift +++ b/Sources/FileSystem/ResourceProcessor.swift @@ -58,7 +58,7 @@ extension ResourceWriter { case .file(let path): let standardizedPath = path as NSString fileName = standardizedPath.lastPathComponent - case .folder(_): + case .folder: fileName = resource.fileName } From 49e4ecbcd325ed04493657eacb152ba8f0641415 Mon Sep 17 00:00:00 2001 From: Decheng Date: Mon, 15 Jun 2020 09:36:57 +1000 Subject: [PATCH 16/21] Introduce SyncResourceComparator, --- .../Services/SyncService.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Sources/AppStoreConnectCLI/Services/SyncService.swift diff --git a/Sources/AppStoreConnectCLI/Services/SyncService.swift b/Sources/AppStoreConnectCLI/Services/SyncService.swift new file mode 100644 index 00000000..ad1067d6 --- /dev/null +++ b/Sources/AppStoreConnectCLI/Services/SyncService.swift @@ -0,0 +1,42 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +import Foundation + +protocol SyncResourceProcessable: SyncResourceComparable, SyncResultRenderable { } + +protocol SyncResourceComparable: Hashable { + associatedtype T: Comparable + + var compareIdentity: T? { get } +} + +struct SyncResourceComparator { + + let localResources: [T] + let serverResources: [T] + + private var localResourcesSet: Set { Set(localResources) } + private var serverResourcesSet: Set { Set(serverResources) } + + func compare() -> [SyncStrategy] { + serverResourcesSet + .subtracting(localResourcesSet) + .compactMap { resource -> SyncStrategy? in + localResources + .contains(where: { resource.compareIdentity == $0.compareIdentity }) + ? nil + : .delete(resource) + } + + + localResourcesSet + .subtracting(serverResourcesSet) + .compactMap { resource -> SyncStrategy? in + serverResourcesSet + .contains( + where: { resource.compareIdentity == $0.compareIdentity } + ) + ? .update(resource) + : .create(resource) + } + } +} From aa393dd51ad36d3173492617c39ba342d74a570b Mon Sep 17 00:00:00 2001 From: Decheng Date: Mon, 15 Jun 2020 09:37:35 +1000 Subject: [PATCH 17/21] Apply new SyncResourceComparator to sync betagroup command --- .../Sync/PushBetaGroupsCommand.swift | 34 ++++--------------- Sources/AppStoreConnectCLI/Model/App.swift | 2 +- .../AppStoreConnectCLI/Model/BetaGroup.swift | 8 ++++- .../Readers and Renderers/Renderers.swift | 2 +- Sources/Model/App.swift | 4 +-- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift index 8b6b561d..25f1bf45 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift @@ -28,13 +28,14 @@ struct PushBetaGroupsCommand: CommonParsableCommand { let resourceProcessor = BetaGroupProcessor(path: .folder(path: inputPath)) - let serverGroups = Set(try service.pullBetaGroups().map { $0.betaGroup }) - let localGroups = Set(try resourceProcessor.read()) + let serverGroups = try service.pullBetaGroups().map { $0.betaGroup } + let localGroups = try resourceProcessor.read() - let strategies = compareGroups( - localGroups: localGroups, - serverGroups: serverGroups - ) + let strategies = SyncResourceComparator( + localResources: localGroups, + serverResources: serverGroups + ) + .compare() let renderer = Renderers.SyncResultRenderer() @@ -65,25 +66,4 @@ struct PushBetaGroupsCommand: CommonParsableCommand { } } - func compareGroups(localGroups: Set, serverGroups: Set) -> [SyncStrategy] { - var strategies: [SyncStrategy] = [] - - let groupToCreate = localGroups.subtracting(serverGroups) - let groupToDelete = serverGroups.subtracting(localGroups) - - groupToDelete.forEach { group in - if !localGroups.contains(where: { group.id == $0.id }) { - strategies.append(.delete(group)) - } - } - - groupToCreate.forEach { group in - serverGroups.contains(where: { group.id == $0.id }) - ? strategies.append(.update(group)) - : strategies.append(.create(group)) - } - - return strategies - } - } diff --git a/Sources/AppStoreConnectCLI/Model/App.swift b/Sources/AppStoreConnectCLI/Model/App.swift index 9b12fab9..946fc576 100644 --- a/Sources/AppStoreConnectCLI/Model/App.swift +++ b/Sources/AppStoreConnectCLI/Model/App.swift @@ -38,7 +38,7 @@ extension App: TableInfoProvider { var tableRow: [CustomStringConvertible] { return [ - id, + id ?? "", bundleId ?? "", name ?? "", primaryLocale ?? "", diff --git a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift index e29e452a..54d91949 100755 --- a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift @@ -26,7 +26,7 @@ extension BetaGroup: TableInfoProvider, ResultRenderable { var tableRow: [CustomStringConvertible] { [ - app.id, + app.id ?? "", app.bundleId ?? "", app.name ?? "", groupName, @@ -79,3 +79,9 @@ extension BetaGroup: SyncResultRenderable { "\(app.bundleId ?? "" )_\(groupName)" } } + +extension BetaGroup: SyncResourceProcessable { + var compareIdentity: String? { + id + } +} diff --git a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift index eb4857f7..2dafbecf 100644 --- a/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift +++ b/Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift @@ -114,7 +114,7 @@ extension ResultRenderable where Self: TableInfoProvider { } } -protocol SyncResultRenderable { +protocol SyncResultRenderable: Equatable { var syncResultText: String { get } } diff --git a/Sources/Model/App.swift b/Sources/Model/App.swift index b68cd27e..474425e8 100644 --- a/Sources/Model/App.swift +++ b/Sources/Model/App.swift @@ -3,14 +3,14 @@ import Foundation public struct App: Codable, Equatable { - public let id: String + public let id: String? public var bundleId: String? public var name: String? public var primaryLocale: String? public var sku: String? public init( - id: String, + id: String?, bundleId: String?, name: String?, primaryLocale: String?, From 4495c847ff6d3c9968f1c37cfcb504ddd0cd79b2 Mon Sep 17 00:00:00 2001 From: Decheng Date: Mon, 15 Jun 2020 09:37:57 +1000 Subject: [PATCH 18/21] Add tests to SyncResourceComparator --- .../SyncResourceComparatorTests.swift | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift diff --git a/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift b/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift new file mode 100644 index 00000000..df57727c --- /dev/null +++ b/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift @@ -0,0 +1,107 @@ +// Copyright 2020 Itty Bitty Apps Pty Ltd + +@testable import AppStoreConnectCLI +import Model +import Foundation +import XCTest + +final class SyncResourceComparatorTests: XCTestCase { + func testCompare_returnCreateStrategies() throws { + let localGroups = [generateGroup(id: "123")] + + let strategies = SyncResourceComparator( + localResources: localGroups, + serverResources: [] + ) + .compare() + + XCTAssertEqual(strategies.count, 1) + + XCTAssertEqual(strategies.first!, .create(localGroups.first!)) + } + + func testCompare_returnUpdateStrategies() throws { + let localGroups = [generateGroup(id: "123", name: "foo")] + let serverGroups = [generateGroup(id: "123", name: "bar")] + + let strategies = SyncResourceComparator( + localResources: localGroups, + serverResources: serverGroups + ) + .compare() + + XCTAssertEqual(strategies.count, 1) + XCTAssertEqual(strategies.first!, .update(localGroups.first!)) + } + + func testCompare_returnDelete() { + let serverGroups = [generateGroup(id: "123"), generateGroup(id: "456")] + + let strategies = SyncResourceComparator( + localResources: [], + serverResources: serverGroups + ) + .compare() + + XCTAssertEqual(strategies.count, 2) + XCTAssertEqual(strategies.contains(.delete(serverGroups.first!)), true) + XCTAssertEqual(strategies.contains(.delete(serverGroups[1])), true) + XCTAssertNotEqual(strategies.contains(.update(generateGroup(id: "1234"))), true) + } + + func testCompare_returnDeleteAndUpdateAndCreate() { + let localGroups = [generateGroup(id: "1", publicLinkEnabled: true), generateGroup(name: "hi")] + let serverGroups = [generateGroup(id: "1", publicLinkEnabled: false), generateGroup(id: "3", name: "there")] + + let strategies = SyncResourceComparator( + localResources: localGroups, + serverResources: serverGroups + ) + .compare() + + XCTAssertEqual(strategies.count, 3) + + XCTAssertEqual(strategies.contains(.delete(serverGroups[1])), true) + XCTAssertEqual(strategies.contains(.create(localGroups[1])), true) + XCTAssertEqual(strategies.contains(.update(localGroups[0])), true) + } +} + +private extension SyncResourceComparatorTests { + func generateGroup( + id: String? = nil, + name: String = "foo", + isInternal: Bool = false, + publicLinkEnabled: Bool = false, + publicLinkLimit: Int = 10, + publicLinkLimitEnabled: Bool = false + ) -> BetaGroup { + BetaGroup( + app: App(id: "", bundleId: "com.example.foo", name: "foo", primaryLocale: "", sku: ""), + id: id, + groupName: name, + isInternal: isInternal, + publicLink: "", + publicLinkEnabled: publicLinkEnabled, + publicLinkLimit: publicLinkLimit, + publicLinkLimitEnabled: publicLinkLimitEnabled, + creationDate: "", + testers: "" + ) + } +} + +extension SyncStrategy: Equatable { + public static func == (lhs: SyncStrategy, rhs: SyncStrategy) -> Bool { + switch (lhs, rhs) { + case (let .create(lhsItem), let .create(rhsItem)): + return lhsItem == rhsItem + case (let .update(lhsItem), let .update(rhsItem)): + return lhsItem == rhsItem + case (let .delete(lhsItem), let .delete(rhsItem)): + return lhsItem == rhsItem + default: + return false + } + } +} From 1679e6fb36177e35bca3a8b7f94d685f06f2fb74 Mon Sep 17 00:00:00 2001 From: Decheng Date: Mon, 15 Jun 2020 10:43:14 +1000 Subject: [PATCH 19/21] PushBetaGroupsCommand syncBetaGroup function code separation --- .../Sync/PushBetaGroupsCommand.swift | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift index 25f1bf45..ef321b36 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PushBetaGroupsCommand.swift @@ -43,20 +43,7 @@ struct PushBetaGroupsCommand: CommonParsableCommand { renderer.render(strategies, isDryRun: true) } else { try strategies.forEach { (strategy: SyncStrategy) in - switch strategy { - case .create(let group): - _ = try service.createBetaGroup( - appBundleId: group.app.bundleId!, - groupName: group.groupName, - publicLinkEnabled: group.publicLinkEnabled ?? false, - publicLinkLimit: group.publicLinkLimit - ) - case .delete(let group): - try service.deleteBetaGroup(with: group.id!) - case .update(let group): - try service.updateBetaGroup(betaGroup: group) - } - + try syncBetaGroup(strategy: strategy, with: service) renderer.render(strategy, isDryRun: false) } @@ -66,4 +53,23 @@ struct PushBetaGroupsCommand: CommonParsableCommand { } } + func syncBetaGroup( + strategy: SyncStrategy, + with service: AppStoreConnectService + ) throws { + switch strategy { + case .create(let group): + _ = try service.createBetaGroup( + appBundleId: group.app.bundleId!, + groupName: group.groupName, + publicLinkEnabled: group.publicLinkEnabled ?? false, + publicLinkLimit: group.publicLinkLimit + ) + case .delete(let group): + try service.deleteBetaGroup(with: group.id!) + case .update(let group): + try service.updateBetaGroup(betaGroup: group) + } + } + } From cb7cc950e6b3eb39eb14031e095e366dfc59bd39 Mon Sep 17 00:00:00 2001 From: Decheng Date: Mon, 15 Jun 2020 12:01:38 +1000 Subject: [PATCH 20/21] mark compareIdentity non-optional in ResourceComparable, tweak sync test --- Sources/AppStoreConnectCLI/Model/BetaGroup.swift | 4 ++-- Sources/AppStoreConnectCLI/Services/SyncService.swift | 2 +- .../Serivces/SyncResourceComparatorTests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift index 54d91949..b3785ce5 100755 --- a/Sources/AppStoreConnectCLI/Model/BetaGroup.swift +++ b/Sources/AppStoreConnectCLI/Model/BetaGroup.swift @@ -81,7 +81,7 @@ extension BetaGroup: SyncResultRenderable { } extension BetaGroup: SyncResourceProcessable { - var compareIdentity: String? { - id + var compareIdentity: String { + id ?? "" } } diff --git a/Sources/AppStoreConnectCLI/Services/SyncService.swift b/Sources/AppStoreConnectCLI/Services/SyncService.swift index ad1067d6..c4ccdc9e 100644 --- a/Sources/AppStoreConnectCLI/Services/SyncService.swift +++ b/Sources/AppStoreConnectCLI/Services/SyncService.swift @@ -7,7 +7,7 @@ protocol SyncResourceProcessable: SyncResourceComparable, SyncResultRenderable { protocol SyncResourceComparable: Hashable { associatedtype T: Comparable - var compareIdentity: T? { get } + var compareIdentity: T { get } } struct SyncResourceComparator { diff --git a/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift b/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift index df57727c..3c7fdc93 100644 --- a/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift +++ b/Tests/appstoreconnect-cliTests/Serivces/SyncResourceComparatorTests.swift @@ -7,7 +7,7 @@ import XCTest final class SyncResourceComparatorTests: XCTestCase { func testCompare_returnCreateStrategies() throws { - let localGroups = [generateGroup(id: "123")] + let localGroups = [generateGroup(name: "a new group")] let strategies = SyncResourceComparator( localResources: localGroups, From 0e381c05e4003de5f640c99b0bec790b216a90a1 Mon Sep 17 00:00:00 2001 From: Decheng Date: Wed, 1 Jul 2020 10:10:31 +1000 Subject: [PATCH 21/21] Reformat Pull betagroup command desc, remove default command for sync --- .../TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift | 4 ++-- .../TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift index 4a0849d4..f560d13f 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/PullBetaGroupsCommand.swift @@ -7,7 +7,7 @@ struct PullBetaGroupsCommand: CommonParsableCommand { static var configuration = CommandConfiguration( commandName: "pull", - abstract: "Pull down server beta groups, refresh local beta group config files" + abstract: "Pull down existing beta groups, refreshing local beta group config files" ) @OptionGroup() @@ -15,7 +15,7 @@ struct PullBetaGroupsCommand: CommonParsableCommand { @Option( default: "./config/betagroups", - help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')" + help: "Path to the Folder containing the information about beta groups." ) var outputPath: String func run() throws { diff --git a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift index 8c4166b2..2f41e4af 100644 --- a/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift +++ b/Sources/AppStoreConnectCLI/Commands/TestFlight/BetaGroups/Sync/SyncBetaGroupsCommand.swift @@ -5,13 +5,10 @@ import ArgumentParser struct SyncBetaGroupsCommand: ParsableCommand { static var configuration = CommandConfiguration( commandName: "sync", - abstract: """ - Sync information about beta groups with provided configuration file. - """, + abstract: "Sync information about beta groups with provided configuration file.", subcommands: [ PullBetaGroupsCommand.self, PushBetaGroupsCommand.self, - ], - defaultSubcommand: PullBetaGroupsCommand.self + ] ) }