diff --git a/README.md b/README.md index c8366e5..66e08fb 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,7 @@ OPTIONS: OpenSSL documentation for this flag (https://www.openssl.org/docs/manmaster/man1/openssl-req.html): -Sets - subject name for new request or supersedes the + Sets subject name for new request or supersedes the subject name when processing a certificate request. The arg must be formatted as @@ -157,6 +156,9 @@ Sets specify the members of the set. Example: /DC=org/DC=OpenSSL/DC=users/UID=123456+CN=JohnDoe + --auto-regenerate + Defines if the profile should be regenerated in case + it already exists (optional) -h, --help Show help information. diff --git a/Sources/SignHereLibrary/Commands/CreateProvisioningProfileCommand.swift b/Sources/SignHereLibrary/Commands/CreateProvisioningProfileCommand.swift index ed373fa..0eb986f 100644 --- a/Sources/SignHereLibrary/Commands/CreateProvisioningProfileCommand.swift +++ b/Sources/SignHereLibrary/Commands/CreateProvisioningProfileCommand.swift @@ -38,6 +38,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { ) case unableToCreateCSR(output: ShellOutput) case unableToImportIntermediaryAppleCertificate(certificate: String, output: ShellOutput) + case profileNameMissing var description: String { switch self { @@ -101,6 +102,8 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { - Output: \(output.outputString) - Error: \(output.errorString) """ + case .profileNameMissing: + return "--auto-regenerate flag requires that you include a profile name using the argument --profile-name" } } } @@ -122,6 +125,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { case intermediaryAppleCertificates = "intermediaryAppleCertificates" case certificateSigningRequestSubject = "certificateSigningRequestSubject" case profileName = "profileName" + case autoRegenerate = "autoRegenerate" } @Option(help: "The key identifier of the private key (https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests)") @@ -182,6 +186,9 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { """) internal var certificateSigningRequestSubject: String + @Flag(help: "Defines if the profile should be regenerated in case it already exists (optional)") + internal var autoRegenerate = false + private let files: Files private let log: Log private let shell: Shell @@ -228,7 +235,8 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { certificateSigningRequestSubject: String, bundleIdentifierName: String?, platform: String, - profileName: String? + profileName: String?, + autoRegenerate: Bool ) { self.files = files self.log = log @@ -252,6 +260,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { self.bundleIdentifierName = bundleIdentifierName self.platform = platform self.profileName = profileName + self.autoRegenerate = autoRegenerate } internal init(from decoder: Decoder) throws { @@ -286,18 +295,36 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { certificateSigningRequestSubject: try container.decode(String.self, forKey: .certificateSigningRequestSubject), bundleIdentifierName: try container.decodeIfPresent(String.self, forKey: .bundleIdentifierName), platform: try container.decode(String.self, forKey: .platform), - profileName: try container.decodeIfPresent(String.self, forKey: .profileName) + profileName: try container.decodeIfPresent(String.self, forKey: .profileName), + autoRegenerate: try container.decode(Bool.self, forKey: .autoRegenerate) ) } internal func run() throws { - let privateKey: Path = .init(privateKeyPath) - let csr: Path = try createCSR(privateKey: privateKey) let jsonWebToken: String = try jsonWebTokenService.createToken( keyIdentifier: keyIdentifier, issuerID: issuerID, secretKey: try files.read(Path(itunesConnectKeyPath)) ) + let deviceIDs: Set = try iTunesConnectService.fetchITCDeviceIDs(jsonWebToken: jsonWebToken) + guard let profileName, let profile = try? fetchProvisioningProfile(jsonWebToken: jsonWebToken, name: profileName) + else { + try createProvisioningProfile(jsonWebToken: jsonWebToken, deviceIDs: deviceIDs) + return + } + guard autoRegenerate, shouldRegenerate(profile: profile, with: deviceIDs) + else { + try save(profile: profile) + log.append("The profile already exists") + return + } + try deleteProvisioningProfile(jsonWebToken: jsonWebToken, id: profile.id) + try createProvisioningProfile(jsonWebToken: jsonWebToken, deviceIDs: deviceIDs) + } + + private func createProvisioningProfile(jsonWebToken: String, deviceIDs: Set) throws { + let privateKey: Path = .init(privateKeyPath) + let csr: Path = try createCSR(privateKey: privateKey) let tuple: (cer: Path, certificateId: String) = try fetchOrCreateCertificate(jsonWebToken: jsonWebToken, csr: csr) let cer: Path = tuple.cer let certificateId: String = tuple.certificateId @@ -311,7 +338,6 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { try importP12IdentityIntoKeychain(p12Identity: p12Identity, identityPassword: identityPassword) try importIntermediaryAppleCertificates() try updateKeychainPartitionList() - let deviceIDs: Set = try iTunesConnectService.fetchITCDeviceIDs(jsonWebToken: jsonWebToken) let profileResponse: CreateProfileResponse = try iTunesConnectService.createProfile( jsonWebToken: jsonWebToken, bundleId: try iTunesConnectService.determineBundleIdITCId( @@ -325,11 +351,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { profileType: profileType, profileName: profileName ) - guard let profileData: Data = .init(base64Encoded: profileResponse.data.attributes.profileContent) - else { - throw Error.unableToBase64DecodeProfile(name: profileResponse.data.attributes.name) - } - try files.write(profileData, to: .init(outputPath)) + try save(profile: profileResponse.data) log.append(profileResponse.data.id) } @@ -496,4 +518,44 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand { ) } } + + private func fetchProvisioningProfile(jsonWebToken: String, name: String) throws -> ProfileResponseData? { + try iTunesConnectService.fetchProvisioningProfile( + jsonWebToken: jsonWebToken, + name: name + ).first(where: { $0.attributes.name == name }) + } + + private func deleteProvisioningProfile(jsonWebToken: String, id: String) throws { + try iTunesConnectService.deleteProvisioningProfile( + jsonWebToken: jsonWebToken, + id: id + ) + log.append("Deleted profile with id: \(id)") + } + + private func save(profile: ProfileResponseData) throws { + guard let profileData: Data = .init(base64Encoded: profile.attributes.profileContent) + else { + throw Error.unableToBase64DecodeProfile(name: profile.attributes.name) + } + try files.write(profileData, to: .init(outputPath)) + } + + private func shouldRegenerate(profile: ProfileResponseData, with deviceIDs: Set) -> Bool { + guard ProfileType(rawValue: profileType).usesDevices else { return false } + let profileDevices = Set(profile.relationships.devices.data.map { $0.id }) + let shouldRegenerate = deviceIDs != profileDevices + if shouldRegenerate { + let missingDevices = deviceIDs.subtracting(profileDevices) + log.append("The profile will be regenerated because it is missing the device(s): \(missingDevices.joined(separator: ", "))") + } + return shouldRegenerate + } + + mutating internal func validate() throws { + if autoRegenerate, profileName == nil { + throw Error.profileNameMissing + } + } } diff --git a/Sources/SignHereLibrary/Models/CreateProfileResponse.swift b/Sources/SignHereLibrary/Models/CreateProfileResponse.swift index b00ce82..279c92e 100644 --- a/Sources/SignHereLibrary/Models/CreateProfileResponse.swift +++ b/Sources/SignHereLibrary/Models/CreateProfileResponse.swift @@ -8,20 +8,5 @@ import Foundation internal struct CreateProfileResponse: Codable { - struct CreateProfileResponseData: Codable { - struct Attributes: Codable { - var profileContent: String - var uuid: String - var name: String - var platform: String - var createdDate: Date - var profileState: String - var profileType: String - var expirationDate: Date - } - var id: String - var type: String - var attributes: Attributes - } - var data: CreateProfileResponseData + var data: ProfileResponseData } diff --git a/Sources/SignHereLibrary/Models/GetProfilesResponse.swift b/Sources/SignHereLibrary/Models/GetProfilesResponse.swift new file mode 100644 index 0000000..397e146 --- /dev/null +++ b/Sources/SignHereLibrary/Models/GetProfilesResponse.swift @@ -0,0 +1,12 @@ +// +// GetProfilesResponse.swift +// Models +// +// Created by Omar Zuniga on 29/05/24. +// + +import Foundation + +internal struct GetProfilesResponse: Codable { + var data: [ProfileResponseData] +} diff --git a/Sources/SignHereLibrary/Models/ProfileResponseData.swift b/Sources/SignHereLibrary/Models/ProfileResponseData.swift new file mode 100644 index 0000000..48e2ce2 --- /dev/null +++ b/Sources/SignHereLibrary/Models/ProfileResponseData.swift @@ -0,0 +1,36 @@ +// +// CreateProfileResponse.swift +// Models +// +// Created by Omar Zuniga on 29/05/24. +// + +import Foundation + +struct ProfileResponseData: Codable { + struct Attributes: Codable { + var profileContent: String + var uuid: String + var name: String + var platform: String + var createdDate: Date + var profileState: String + var profileType: String + var expirationDate: Date + } + struct Relationships: Codable { + struct Devices: Codable { + struct Data: Codable { + var id: String + var type: String + } + + var data: [Data] + } + var devices: Devices + } + var id: String + var type: String + var attributes: Attributes + var relationships: Relationships +} diff --git a/Sources/SignHereLibrary/Models/ProfileType.swift b/Sources/SignHereLibrary/Models/ProfileType.swift new file mode 100644 index 0000000..abdd8fd --- /dev/null +++ b/Sources/SignHereLibrary/Models/ProfileType.swift @@ -0,0 +1,35 @@ +// +// ProfileType.swift +// Models +// +// Created by Omar Zuniga on 29/05/24. +// + +import Foundation + +enum ProfileType { + case development + case adHoc + case appStore + case inHouse + case direct + case unknown + + init(rawValue: String) { + switch rawValue { + case let str where str.hasSuffix("_APP_DEVELOPMENT"): self = .development + case let str where str.hasSuffix("_APP_ADHOC"): self = .adHoc + case let str where str.hasSuffix("_APP_STORE"): self = .appStore + case let str where str.hasSuffix("_APP_INHOUSE"): self = .inHouse + case let str where str.hasSuffix("_APP_DIRECT"): self = .direct + default: self = .unknown + } + } + + var usesDevices: Bool { + switch self { + case .appStore: return false + default: return true + } + } +} diff --git a/Sources/SignHereLibrary/Services/iTunesConnectService.swift b/Sources/SignHereLibrary/Services/iTunesConnectService.swift index 781d27b..97def27 100644 --- a/Sources/SignHereLibrary/Services/iTunesConnectService.swift +++ b/Sources/SignHereLibrary/Services/iTunesConnectService.swift @@ -42,6 +42,10 @@ internal protocol iTunesConnectService { jsonWebToken: String, id: String ) throws + func fetchProvisioningProfile( + jsonWebToken: String, + name: String + ) throws -> [ProfileResponseData] } internal class iTunesConnectServiceImp: iTunesConnectService { @@ -368,7 +372,7 @@ internal class iTunesConnectServiceImp: iTunesConnectService { let profileName = profileName ?? "\(certificateId)_\(profileType)_\(clock.now().timeIntervalSince1970)" var devices: CreateProfileRequest.CreateProfileRequestData.Relationships.Devices? = nil // ME: App Store profiles cannot use UDIDs - if !["IOS_APP_STORE", "MAC_APP_STORE", "TVOS_APP_STORE", "MAC_CATALYST_APP_STORE"].contains(profileType) { + if ProfileType(rawValue: profileType).usesDevices { devices = .init( data: deviceIDs.sorted().map { CreateProfileRequest.CreateProfileRequestData.Relationships.Devices.DevicesData( @@ -440,6 +444,39 @@ internal class iTunesConnectServiceImp: iTunesConnectService { } } + func fetchProvisioningProfile( + jsonWebToken: String, + name: String + ) throws -> [ProfileResponseData] { + var urlComponents: URLComponents = .init() + urlComponents.scheme = Constants.httpsScheme + urlComponents.host = Constants.itcHost + urlComponents.path = "/v1/profiles" + urlComponents.queryItems = [ + .init(name: "filter[name]", value: name), + .init(name: "include", value: "devices") + ] + guard let url: URL = urlComponents.url + else { + throw Error.unableToCreateURL(urlComponents: urlComponents) + } + var request: URLRequest = .init(url: url) + request.setValue("Bearer \(jsonWebToken)", forHTTPHeaderField: "Authorization") + request.setValue(Constants.applicationJSONHeaderValue, forHTTPHeaderField: "Accept") + request.setValue(Constants.applicationJSONHeaderValue, forHTTPHeaderField: Constants.contentTypeHeaderName) + request.httpMethod = "GET" + let jsonDecoder: JSONDecoder = createITCApiJSONDecoder() + let data: Data = try network.execute(request: request) + do { + return try jsonDecoder.decode( + GetProfilesResponse.self, + from: data + ).data + } catch let decodingError as DecodingError { + throw Error.unableToDecodeResponse(responseData: data, decodingError: decodingError) + } + } + private func createITCApiJSONDecoder() -> JSONDecoder { let jsonDecoder: JSONDecoder = .init() let dateFormatter: DateFormatter = .init() diff --git a/Tests/SignHereLibraryTests/CreateProvisioningProfileCommandTests.swift b/Tests/SignHereLibraryTests/CreateProvisioningProfileCommandTests.swift index de10048..b8c2cfb 100644 --- a/Tests/SignHereLibraryTests/CreateProvisioningProfileCommandTests.swift +++ b/Tests/SignHereLibraryTests/CreateProvisioningProfileCommandTests.swift @@ -57,7 +57,8 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { certificateSigningRequestSubject: "certificateSigningRequestSubject", bundleIdentifierName: "bundleIdentifierName", platform: "platform", - profileName: "profileName" + profileName: "profileName", + autoRegenerate: false ) isRecording = false } @@ -142,6 +143,10 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { ).description, as: .lines ) + assertSnapshot( + matching: CreateProvisioningProfileCommand.Error.profileNameMissing.description, + as: .lines + ) } func test_initDecoder() throws { @@ -162,7 +167,8 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { "certificateSigningRequestSubject": "certificateSigningRequestSubject", "bundleIdentifierName": "bundleIdentifierName", "platform": "platform", - "profileName": "profileName" + "profileName": "profileName", + "autoRegenerate": false } """.utf8) @@ -183,6 +189,7 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { XCTAssertEqual(subject.bundleIdentifierName, "bundleIdentifierName") XCTAssertEqual(subject.platform, "platform") XCTAssertEqual(subject.profileName, "profileName") + XCTAssertEqual(subject.autoRegenerate, false) } func test_execute_alreadyActiveCertificate() throws { @@ -216,7 +223,6 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { iTunesConnectService.createProfileHandler = { _, _, _, _, _, _ in self.createCreateProfileResponse() } - // WHEN try subject.run() @@ -229,6 +235,9 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { matching: log.appendArgValues, as: .dump ) + + XCTAssertEqual(executeLaunchPaths.count, 0) + XCTAssertEqual(fileDataReads.count, 0) } func test_execute_noActiveCertificates() throws { @@ -272,6 +281,167 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { matching: log.appendArgValues, as: .dump ) + + XCTAssertEqual(executeLaunchPaths.count, 0) + XCTAssertEqual(fileDataReads.count, 0) + } + + func test_execute_profileAlreadyExists() throws { + // GIVEN + var executeLaunchPaths: [ShellOutput] = [] + let responseObject = createCreateProfileResponse().data + + shell.executeLaunchPathHandler = { _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return executeLaunchPaths.removeFirst() + } + var fileDataReads: [Data] = [ + Data("iTunesConnectAPIKey".utf8), + ] + files.readPathHandler = { _ in + fileDataReads.removeFirst() + } + iTunesConnectService.fetchActiveCertificatesHandler = { _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return [] + } + iTunesConnectService.createCertificateHandler = { _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return self.createCreateCertificateResponse() + } + iTunesConnectService.createProfileHandler = { _, _, _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return self.createCreateProfileResponse() + } + iTunesConnectService.fetchProvisioningProfileHandler = { _, _ in + return [responseObject] + } + // WHEN + subject.profileName = responseObject.attributes.name + try subject.run() + + // THEN + assertSnapshot( + matching: shell.executeLaunchPathArgValues, + as: .dump + ) + assertSnapshot( + matching: log.appendArgValues, + as: .dump + ) + + XCTAssertEqual(fileDataReads.count, 0) + } + + func test_execute_profileShouldRegenerateWithNewDevices() throws { + // GIVEN + var previousProfileWasDeleted = false + let responseObject = createCreateProfileResponse().data + + files.uniqueTemporaryPathHandler = { + Path("/unique_temporary_path_\(self.files.uniqueTemporaryPathCallCount)") + } + var executeLaunchPaths: [ShellOutput] = [ + .init(status: 0, data: .init("createCSR".utf8), errorData: .init()), + .init(status: 0, data: .init("createPEM".utf8), errorData: .init()), + .init(status: 0, data: .init("createP12Identity".utf8), errorData: .init()), + .init(status: 0, data: .init("importP12IdentityIntoKeychain".utf8), errorData: .init()), + .init(status: 0, data: .init("importIntermediateAppleCertificate".utf8), errorData: .init()), + .init(status: 0, data: .init("updateKeychainPartitionList".utf8), errorData: .init()) + ] + shell.executeLaunchPathHandler = { _, _, _, _ in + executeLaunchPaths.removeFirst() + } + var fileDataReads: [Data] = [ + Data("iTunesConnectAPIKey".utf8) + ] + files.readPathHandler = { _ in + fileDataReads.removeFirst() + } + iTunesConnectService.fetchActiveCertificatesHandler = { _, _, _, _ in + self.createDownloadCertificateResponse().data + } + iTunesConnectService.createCertificateHandler = { _, _, _ in + self.createCreateCertificateResponse() + } + iTunesConnectService.createProfileHandler = { _, _, _, _, _, _ in + self.createCreateProfileResponse() + } + iTunesConnectService.fetchITCDeviceIDsHandler = { _ in + Set(["deviceID"]) + } + iTunesConnectService.deleteProvisioningProfileHandler = { _, _ in + previousProfileWasDeleted = true + } + iTunesConnectService.fetchProvisioningProfileHandler = { _, _ in + return [responseObject] + } + // WHEN + subject.profileName = responseObject.attributes.name + subject.autoRegenerate = true + try subject.run() + + // THEN + assertSnapshot( + matching: shell.executeLaunchPathArgValues, + as: .dump + ) + assertSnapshot( + matching: log.appendArgValues, + as: .dump + ) + + XCTAssertEqual(executeLaunchPaths.count, 0) + XCTAssertEqual(fileDataReads.count, 0) + XCTAssertTrue(previousProfileWasDeleted) + } + + func test_execute_profileShouldNotRegenerateWithSameDevices() throws { + // GIVEN + var executeLaunchPaths: [ShellOutput] = [] + let responseObject = createCreateProfileResponse().data + + shell.executeLaunchPathHandler = { _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return executeLaunchPaths.removeFirst() + } + var fileDataReads: [Data] = [ + Data("iTunesConnectAPIKey".utf8), + ] + files.readPathHandler = { _ in + fileDataReads.removeFirst() + } + iTunesConnectService.fetchActiveCertificatesHandler = { _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return [] + } + iTunesConnectService.createCertificateHandler = { _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return self.createCreateCertificateResponse() + } + iTunesConnectService.createProfileHandler = { _, _, _, _, _, _ in + XCTAssert(false, "Shouldn't be executed") + return self.createCreateProfileResponse() + } + iTunesConnectService.fetchProvisioningProfileHandler = { _, _ in + return [responseObject] + } + // WHEN + subject.profileName = responseObject.attributes.name + subject.autoRegenerate = true + try subject.run() + + // THEN + assertSnapshot( + matching: shell.executeLaunchPathArgValues, + as: .dump + ) + assertSnapshot( + matching: log.appendArgValues, + as: .dump + ) + + XCTAssertEqual(fileDataReads.count, 0) } private func createDownloadCertificateResponse() -> DownloadCertificateResponse { @@ -321,10 +491,10 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { private func createCreateProfileResponse() -> CreateProfileResponse { .init( - data: CreateProfileResponse.CreateProfileResponseData( + data: ProfileResponseData( id: "createdProfileITCID", type: "type", - attributes: CreateProfileResponse.CreateProfileResponseData.Attributes( + attributes: ProfileResponseData.Attributes( profileContent: "dGVzdAo=", uuid: "uuid", name: "createdProfileName", @@ -333,6 +503,11 @@ final class CreateProvisioningProfileCommandTests: XCTestCase { profileState: "profileState", profileType: "profileType", expirationDate: .init(timeIntervalSince1970: 100) + ), + relationships: ProfileResponseData.Relationships( + devices: ProfileResponseData.Relationships.Devices( + data: [] + ) ) ) ) diff --git a/Tests/SignHereLibraryTests/ProfileTypeModelTests.swift b/Tests/SignHereLibraryTests/ProfileTypeModelTests.swift new file mode 100644 index 0000000..d66a30c --- /dev/null +++ b/Tests/SignHereLibraryTests/ProfileTypeModelTests.swift @@ -0,0 +1,114 @@ +// +// CreateProvisioningProfileCommandTests.swift +// SignHereLibraryTests +// +// Created by Omar Zuniga on 29/05/24. +// + +import ArgumentParser +import CoreLibrary +import CoreLibrary_GeneratedMocks +import CoreLibraryTestKit +import PathKit +import XCTest + +@testable import SignHereLibrary +@testable import SignHereLibrary_GeneratedMocks + +final class ProfileTypeModelTests: XCTestCase { + + func testDevelopmentType() { + // GIVEN + let prefixes = [ + "IOS", + "MAC", + "TVOS", + "MAC_CATALYST" + ] + let suffix = "_APP_DEVELOPMENT" + // WHEN + let profileTypes = prefixes.map { + ProfileType(rawValue: "\($0)\(suffix)") + } + // THEN + profileTypes.forEach { profileType in + XCTAssertEqual(profileType, .development) + XCTAssertEqual(profileType.usesDevices, true) + } + } + + func testAdHocType() { + // GIVEN + let prefixes = [ + "IOS", + "TVOS" + ] + let suffix = "_APP_ADHOC" + // WHEN + let profileTypes = prefixes.map { + ProfileType(rawValue: "\($0)\(suffix)") + } + // THEN + profileTypes.forEach { profileType in + XCTAssertEqual(profileType, .adHoc) + XCTAssertEqual(profileType.usesDevices, true) + } + } + + func testAppStoreType() { + // GIVEN + let prefixes = [ + "IOS", + "MAC", + "TVOS", + "MAC_CATALYST" + ] + let suffix = "_APP_STORE" + // WHEN + let profileTypes = prefixes.map { + ProfileType(rawValue: "\($0)\(suffix)") + } + // THEN + profileTypes.forEach { profileType in + XCTAssertEqual(profileType, .appStore) + XCTAssertEqual(profileType.usesDevices, false) + } + } + + + func testInHouseType() { + // GIVEN + let prefixes = [ + "IOS", + "TVOS" + ] + let suffix = "_APP_INHOUSE" + // WHEN + let profileTypes = prefixes.map { + ProfileType(rawValue: "\($0)\(suffix)") + } + // THEN + profileTypes.forEach { profileType in + XCTAssertEqual(profileType, .inHouse) + XCTAssertEqual(profileType.usesDevices, true) + } + } + + func testDirectType() { + // GIVEN + let prefixes = [ + "MAC", + "MAC_CATALYST" + ] + let suffix = "_APP_DIRECT" + // WHEN + let profileTypes = prefixes.map { + ProfileType(rawValue: "\($0)\(suffix)") + } + // THEN + profileTypes.forEach { profileType in + XCTAssertEqual(profileType, .direct) + XCTAssertEqual(profileType.usesDevices, true) + } + } +} diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_testErrors.10.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_testErrors.10.txt new file mode 100644 index 0000000..40e4768 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_testErrors.10.txt @@ -0,0 +1 @@ +--auto-regenerate flag requires that you include a profile name using the argument --profile-name \ No newline at end of file diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.1.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.1.txt new file mode 100644 index 0000000..a6eff20 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.1.txt @@ -0,0 +1 @@ +- 0 elements diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.2.txt new file mode 100644 index 0000000..3fd8a7e --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileAlreadyExists.2.txt @@ -0,0 +1,4 @@ +▿ 1 element + ▿ (2 elements) + - .0: "The profile already exists" + - .1: "\n" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.1.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.1.txt new file mode 100644 index 0000000..a6eff20 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.1.txt @@ -0,0 +1 @@ +- 0 elements diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.2.txt new file mode 100644 index 0000000..3fd8a7e --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldNotRegenerateWithSameDevices.2.txt @@ -0,0 +1,4 @@ +▿ 1 element + ▿ (2 elements) + - .0: "The profile already exists" + - .1: "\n" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.1.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.1.txt new file mode 100644 index 0000000..79fb71a --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.1.txt @@ -0,0 +1,91 @@ +▿ 6 elements + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 9 elements + - "/opensslPath" + - "req" + - "-new" + - "-key" + - "privateKeyPath" + - "-out" + - "/unique_temporary_path_1/certificate_request.csr" + - "-subj" + - "certificateSigningRequestSubject" + - .2: Optional>.none + - .3: Optional.none + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 10 elements + - "/opensslPath" + - "x509" + - "-inform" + - "DER" + - "-outform" + - "PEM" + - "-in" + - "/unique_temporary_path_2/activeCertID.cer" + - "-out" + - "/unique_temporary_path_3/certificate.pem" + - .2: Optional>.none + - .3: Optional.none + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 17 elements + - "/opensslPath" + - "pkcs12" + - "-export" + - "-macalg" + - "sha1" + - "-keypbe" + - "PBE-SHA1-3DES" + - "-certpbe" + - "PBE-SHA1-3DES" + - "-inkey" + - "privateKeyPath" + - "-in" + - "/unique_temporary_path_3/certificate.pem" + - "-passout" + - "pass:uuid_1" + - "-out" + - "/unique_temporary_path_4/identity.p12" + - .2: Optional>.none + - .3: Optional.none + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 9 elements + - "security" + - "import" + - "/unique_temporary_path_4/identity.p12" + - "-k" + - "keychainName" + - "-P" + - "uuid_1" + - "-T" + - "/usr/bin/codesign" + - .2: Optional>.none + - .3: Optional.none + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 7 elements + - "security" + - "import" + - "/intermediaryAppleCertificate" + - "-k" + - "keychainName" + - "-T" + - "/usr/bin/codesign" + - .2: Optional>.none + - .3: Optional.none + ▿ (4 elements) + - .0: "/usr/bin/env" + ▿ .1: 8 elements + - "security" + - "set-key-partition-list" + - "-S" + - "apple-tool:,apple:,codesign:" + - "-s" + - "-k" + - "keychainPassword" + - "keychainName" + - .2: Optional>.none + - .3: Optional.none diff --git a/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.2.txt new file mode 100644 index 0000000..ef0a376 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/CreateProvisioningProfileCommandTests/CreateProvisioningProfileCommandTests_test_execute_profileShouldRegenerateWithNewDevices.2.txt @@ -0,0 +1,10 @@ +▿ 3 elements + ▿ (2 elements) + - .0: "The profile will be regenerated because it is missing the device(s): deviceID" + - .1: "\n" + ▿ (2 elements) + - .0: "Deleted profile with id: createdProfileITCID" + - .1: "\n" + ▿ (2 elements) + - .0: "createdProfileITCID" + - .1: "\n" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile.2.txt index f9a01d1..69c2fb1 100644 --- a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile.2.txt +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile.2.txt @@ -1,5 +1,5 @@ ▿ CreateProfileResponse - ▿ data: CreateProfileResponseData + ▿ data: ProfileResponseData ▿ attributes: Attributes - createdDate: 1970-01-01T00:00:00Z - expirationDate: 1970-01-01T00:01:40Z @@ -10,4 +10,7 @@ - profileType: "profileType" - uuid: "uuid" - id: "createdProfileITCID" + ▿ relationships: Relationships + ▿ devices: Devices + - data: 0 elements - type: "type" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_iosAppStoreProfile.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_iosAppStoreProfile.2.txt index f9a01d1..69c2fb1 100644 --- a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_iosAppStoreProfile.2.txt +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_iosAppStoreProfile.2.txt @@ -1,5 +1,5 @@ ▿ CreateProfileResponse - ▿ data: CreateProfileResponseData + ▿ data: ProfileResponseData ▿ attributes: Attributes - createdDate: 1970-01-01T00:00:00Z - expirationDate: 1970-01-01T00:01:40Z @@ -10,4 +10,7 @@ - profileType: "profileType" - uuid: "uuid" - id: "createdProfileITCID" + ▿ relationships: Relationships + ▿ devices: Devices + - data: 0 elements - type: "type" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_withProfileName.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_withProfileName.2.txt index f9a01d1..69c2fb1 100644 --- a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_withProfileName.2.txt +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_createProfile_withProfileName.2.txt @@ -1,5 +1,5 @@ ▿ CreateProfileResponse - ▿ data: CreateProfileResponseData + ▿ data: ProfileResponseData ▿ attributes: Attributes - createdDate: 1970-01-01T00:00:00Z - expirationDate: 1970-01-01T00:01:40Z @@ -10,4 +10,7 @@ - profileType: "profileType" - uuid: "uuid" - id: "createdProfileITCID" + ▿ relationships: Relationships + ▿ devices: Devices + - data: 0 elements - type: "type" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.1.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.1.txt new file mode 100644 index 0000000..12b94a5 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.1.txt @@ -0,0 +1,5 @@ +curl \ + --header "Accept: application/json" \ + --header "Authorization: Bearer jsonWebToken" \ + --header "Content-Type: application/json" \ + "https://api.appstoreconnect.apple.com/v1/profiles?filter%5Bname%5D=Test&include=devices" \ No newline at end of file diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.2.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.2.txt new file mode 100644 index 0000000..1b169aa --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile.2.txt @@ -0,0 +1,19 @@ +▿ Optional + ▿ some: ProfileResponseData + ▿ attributes: Attributes + - createdDate: 1970-01-01T00:00:00Z + - expirationDate: 1970-01-01T00:01:40Z + - name: "getProfileName" + - platform: "platform" + - profileContent: "dGVzdAo=" + - profileState: "profileState" + - profileType: "profileType" + - uuid: "uuid" + - id: "getProfileITCID" + ▿ relationships: Relationships + ▿ devices: Devices + ▿ data: 1 element + ▿ Data + - id: "1234" + - type: "device" + - type: "type" diff --git a/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile_decodeError.1.txt b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile_decodeError.1.txt new file mode 100644 index 0000000..12b94a5 --- /dev/null +++ b/Tests/SignHereLibraryTests/__Snapshots__/iTunesConnectServiceTests/iTunesConnectServiceTests_test_fetchProfile_decodeError.1.txt @@ -0,0 +1,5 @@ +curl \ + --header "Accept: application/json" \ + --header "Authorization: Bearer jsonWebToken" \ + --header "Content-Type: application/json" \ + "https://api.appstoreconnect.apple.com/v1/profiles?filter%5Bname%5D=Test&include=devices" \ No newline at end of file diff --git a/Tests/SignHereLibraryTests/iTunesConnectServiceTests.swift b/Tests/SignHereLibraryTests/iTunesConnectServiceTests.swift index 47fc880..7a8e362 100644 --- a/Tests/SignHereLibraryTests/iTunesConnectServiceTests.swift +++ b/Tests/SignHereLibraryTests/iTunesConnectServiceTests.swift @@ -713,6 +713,59 @@ final class iTunesConnectServiceTests: XCTestCase { } } + func test_fetchProfile() throws { + // GIVEN + let jsonEncoder: JSONEncoder = createJSONEncoder() + var networkExecutes: [Data] = [ + try jsonEncoder.encode(createFetchProfileResponse()), + ] + network.executeHandler = { _ in + networkExecutes.removeFirst() + } + + // WHEN + let value: ProfileResponseData? = try subject.fetchProvisioningProfile( + jsonWebToken: "jsonWebToken", + name: "Test" + ).first + + // THEN + for argValue in network.executeArgValues { + assertSnapshot(matching: argValue, as: .curl) + } + + assertSnapshot( + matching: value, + as: .dump + ) + } + + func test_fetchProfile_decodeError() throws { + // GIVEN + var networkExecutes: [Data] = [ + .init() + ] + network.executeHandler = { _ in + networkExecutes.removeFirst() + } + + // WHEN + XCTAssertThrowsError(try subject.fetchProvisioningProfile( + jsonWebToken: "jsonWebToken", + name: "Test" + ).first) { + if case iTunesConnectServiceImp.Error.unableToDecodeResponse = $0 { + return + } + XCTFail($0.localizedDescription) + } + + // THEN + for argValue in network.executeArgValues { + assertSnapshot(matching: argValue, as: .curl) + } + } + private func createJSONEncoder() -> JSONEncoder { let jsonEncoder: JSONEncoder = .init() let dateFormatter: DateFormatter = .init() @@ -808,10 +861,10 @@ final class iTunesConnectServiceTests: XCTestCase { private func createCreateProfileResponse() -> CreateProfileResponse { .init( - data: CreateProfileResponse.CreateProfileResponseData( + data: ProfileResponseData( id: "createdProfileITCID", type: "type", - attributes: CreateProfileResponse.CreateProfileResponseData.Attributes( + attributes: ProfileResponseData.Attributes( profileContent: "dGVzdAo=", uuid: "uuid", name: "createdProfileName", @@ -820,8 +873,42 @@ final class iTunesConnectServiceTests: XCTestCase { profileState: "profileState", profileType: "profileType", expirationDate: .init(timeIntervalSince1970: 100) + ), + relationships: ProfileResponseData.Relationships( + devices: ProfileResponseData.Relationships.Devices( + data: [] + ) ) ) ) } + + private func createFetchProfileResponse() -> GetProfilesResponse { + .init( + data: [ProfileResponseData( + id: "getProfileITCID", + type: "type", + attributes: ProfileResponseData.Attributes( + profileContent: "dGVzdAo=", + uuid: "uuid", + name: "getProfileName", + platform: "platform", + createdDate: .init(timeIntervalSince1970: 0), + profileState: "profileState", + profileType: "profileType", + expirationDate: .init(timeIntervalSince1970: 100) + ), + relationships: ProfileResponseData.Relationships( + devices: ProfileResponseData.Relationships.Devices( + data: [ + ProfileResponseData.Relationships.Devices.Data( + id: "1234", + type: "device" + ) + ] + ) + ) + )] + ) + } }