diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb91ff..92f2678 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,5 +7,7 @@ jobs: - uses: actions/checkout@v2.3.3 - name: Run tests env: - DEVELOPER_DIR: /Applications/Xcode_11.6.app - run: swift test + DEVELOPER_DIR: /Applications/Xcode_12.app + # Normally would use `swift test` but there's a bug related to SPM package resources, so using xcodebuild as a workaround + # https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051/10 + run: xcodebuild test -scheme xcodes diff --git a/.swift-version b/.swift-version index ef425ca..d346e2a 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.2 +5.3 diff --git a/Apple.paw b/Apple.paw index 50dc39c..f1efcf4 100644 Binary files a/Apple.paw and b/Apple.paw differ diff --git a/Package.swift b/Package.swift index 36bad7f..c4f7bc1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.3 import PackageDescription let package = Package( @@ -19,7 +19,7 @@ let package = Package( .package(name: "PMKFoundation", url: "https://github.com/PromiseKit/Foundation.git", .upToNextMinor(from: "3.3.1")), .package(url: "https://github.com/scinfu/SwiftSoup.git", .upToNextMinor(from: "2.0.0")), .package(url: "https://github.com/mxcl/LegibleError.git", .upToNextMinor(from: "1.0.1")), - .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMinor(from: "3.2.0")) + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMinor(from: "3.2.0")), ], targets: [ .target( @@ -39,6 +39,9 @@ let package = Package( name: "XcodesKitTests", dependencies: [ "XcodesKit", "Version" + ], + resources: [ + .copy("Fixtures"), ]), .target( name: "AppleAPI", @@ -47,6 +50,9 @@ let package = Package( ]), .testTarget( name: "AppleAPITests", - dependencies: ["AppleAPI"]), + dependencies: ["AppleAPI"], + resources: [ + .copy("Fixtures"), + ]), ] ) diff --git a/README.md b/README.md index 677228c..c669af2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ mint install RobotsAndPencils/xcodes
Build from source -Building from source requires Xcode 11.0 or later, so it's not an option for setting up a computer from scratch. +Building from source requires Xcode 12.0 or later, so it's not an option for setting up a computer from scratch. ```sh git clone https://github.com/RobotsAndPencils/xcodes @@ -98,7 +98,7 @@ Xcode 11.2.0 has been installed to /Applications/Xcode-11.2.0.app ## Development -You'll need Xcode 11 in order to build and run xcodes. +You'll need Xcode 12 in order to build and run xcodes.
Using Xcode diff --git a/Sources/AppleAPI/Client.swift b/Sources/AppleAPI/Client.swift index b3cc7c1..ba47128 100644 --- a/Sources/AppleAPI/Client.swift +++ b/Sources/AppleAPI/Client.swift @@ -3,16 +3,18 @@ import PromiseKit import PMKFoundation public class Client { - private(set) public var session = URLSession.shared private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"] public init() {} - public enum Error: Swift.Error, LocalizedError { + public enum Error: Swift.Error, LocalizedError, Equatable { case invalidSession case invalidUsernameOrPassword(username: String) + case invalidPhoneNumberIndex(min: Int, max: Int, given: String?) + case incorrectSecurityCode case unexpectedSignInResponse(statusCode: Int, message: String?) case appleIDAndPrivacyAcknowledgementRequired + case noTrustedPhoneNumbers public var errorDescription: String? { switch self { @@ -20,6 +22,10 @@ public class Client { return "Invalid username and password combination. Attempted to sign in with username \(username)." case .appleIDAndPrivacyAcknowledgementRequired: return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement." + case .invalidPhoneNumberIndex(let min, let max, let given): + return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")." + case .noTrustedPhoneNumbers: + return "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915." default: return String(describing: self) } @@ -28,7 +34,7 @@ public class Client { /// Use the olympus session endpoint to see if the existing session is still valid public func validateSession() -> Promise { - return session.dataTask(.promise, with: URLRequest.olympusSession) + return Current.network.dataTask(with: URLRequest.olympusSession) .done { data, response in guard let jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any], @@ -41,7 +47,7 @@ public class Client { var serviceKey: String! return firstly { () -> Promise<(data: Data, response: URLResponse)> in - self.session.dataTask(.promise, with: URLRequest.itcServiceKey) + Current.network.dataTask(with: URLRequest.itcServiceKey) } .then { (data, _) -> Promise<(data: Data, response: URLResponse)> in struct ServiceKeyResponse: Decodable { @@ -51,7 +57,7 @@ public class Client { let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data) serviceKey = response.authServiceKey - return self.session.dataTask(.promise, with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password)) + return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password)) } .then { (data, response) -> Promise in struct SignInResponse: Decodable { @@ -73,11 +79,11 @@ public class Client { switch httpResponse.statusCode { case 200: - return self.session.dataTask(.promise, with: URLRequest.olympusSession).asVoid() + return Current.network.dataTask(with: URLRequest.olympusSession).asVoid() case 401: throw Error.invalidUsernameOrPassword(username: accountName) case 409: - return self.handleTwoFactor(data: data, response: response, serviceKey: serviceKey) + return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey) case 412 where Client.authTypes.contains(responseBody.authType ?? ""): throw Error.appleIDAndPrivacyAcknowledgementRequired default: @@ -87,24 +93,215 @@ public class Client { } } - public func handleTwoFactor(data: Data, response: URLResponse, serviceKey: String) -> Promise { + func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> Promise { let httpResponse = response as! HTTPURLResponse let sessionID = (httpResponse.allHeaderFields["X-Apple-ID-Session-Id"] as! String) let scnt = (httpResponse.allHeaderFields["scnt"] as! String) - return firstly { () -> Promise<(data: Data, response: URLResponse)> in - self.session.dataTask(.promise, with: URLRequest.authOptions(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)) + return firstly { () -> Promise in + return Current.network.dataTask(with: URLRequest.authOptions(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)) + .map { try JSONDecoder().decode(AuthOptionsResponse.self, from: $0.data) } } - .then { (data, response) -> Promise<(data: Data, response: URLResponse)> in - print("Enter the code: ") - let code = readLine() ?? "" - return self.session.dataTask(.promise, with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code)) + .then { authOptions -> Promise in + switch authOptions.kind { + case .twoStep: + Current.logging.log("Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or open an issue on GitHub explaining why this isn't an option for you here: https://github.com/RobotsAndPencils/xcodes/issues/new") + return Promise.value(()) + case .twoFactor: + return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions) + case .unknown: + Current.logging.log("Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response:") + String(data: data, encoding: .utf8).map { Current.logging.log($0) } + return Promise.value(()) + } } - .then { (data, response) -> Promise<(data: Data, response: URLResponse)> in - self.session.dataTask(.promise, with: URLRequest.trust(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)) + } + + func handleTwoFactor(serviceKey: String, sessionID: String, scnt: String, authOptions: AuthOptionsResponse) -> Promise { + Current.logging.log("Two-factor authentication is enabled for this account.\n") + + // SMS was sent automatically + if authOptions.smsAutomaticallySent { + return firstly { () throws -> Promise<(data: Data, response: URLResponse)> in + let code = self.promptForSMSSecurityCode(length: authOptions.securityCode.length, for: authOptions.trustedPhoneNumbers!.first!) + return Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code)) + .validateSecurityCodeResponse() + } + .then { (data, response) -> Promise in + self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + } + // SMS wasn't sent automatically because user needs to choose a phone to send to + } else if authOptions.canFallBackToSMS { + return handleWithPhoneNumberSelection(authOptions: authOptions, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + // Code is shown on trusted devices + } else { + let code = Current.shell.readLine(""" + Enter "sms" without quotes to exit this prompt and choose a phone number to send an SMS security code to. + Enter the \(authOptions.securityCode.length) digit code from one of your trusted devices: + """) ?? "" + + if code == "sms" { + return handleWithPhoneNumberSelection(authOptions: authOptions, serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + } + + return firstly { + Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: .device(code: code))) + .validateSecurityCodeResponse() + + } + .then { (data, response) -> Promise in + self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + } } - .then { (data, response) -> Promise in - self.session.dataTask(.promise, with: URLRequest.olympusSession).asVoid() + } + + func updateSession(serviceKey: String, sessionID: String, scnt: String) -> Promise { + return Current.network.dataTask(with: URLRequest.trust(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)) + .then { (data, response) -> Promise in + Current.network.dataTask(with: URLRequest.olympusSession).asVoid() + } + } + + func selectPhoneNumberInteractively(from trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]) -> Promise { + return firstly { () throws -> Guarantee in + Current.logging.log("Trusted phone numbers:") + trustedPhoneNumbers.enumerated().forEach { (index, phoneNumber) in + Current.logging.log("\(index + 1): \(phoneNumber.numberWithDialCode)") + } + + let possibleSelectionNumberString = Current.shell.readLine("Select a trusted phone number to receive a code via SMS: ") + guard + let selectionNumberString = possibleSelectionNumberString, + let selectionNumber = Int(selectionNumberString) , + trustedPhoneNumbers.indices.contains(selectionNumber - 1) + else { + throw Error.invalidPhoneNumberIndex(min: 1, max: trustedPhoneNumbers.count, given: possibleSelectionNumberString) + } + + return .value(trustedPhoneNumbers[selectionNumber - 1]) + } + .recover { error throws -> Promise in + guard case Error.invalidPhoneNumberIndex = error else { throw error } + Current.logging.log("\(error.localizedDescription)\n") + return self.selectPhoneNumberInteractively(from: trustedPhoneNumbers) + } + } + + func promptForSMSSecurityCode(length: Int, for trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber) -> SecurityCode { + let code = Current.shell.readLine("Enter the \(length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ") ?? "" + return .sms(code: code, phoneNumberId: trustedPhoneNumber.id) + } + + func handleWithPhoneNumberSelection(authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) -> Promise { + return firstly { () throws -> Promise in + // I don't think this should ever be nil or empty, because 2FA requires at least one trusted phone number, + // but if it is nil or empty it's better to inform the user so they can try to address it instead of crashing. + guard let trustedPhoneNumbers = authOptions.trustedPhoneNumbers, trustedPhoneNumbers.isEmpty == false else { + throw Error.noTrustedPhoneNumbers + } + + return selectPhoneNumberInteractively(from: trustedPhoneNumbers) + } + .then { trustedPhoneNumber in + Current.network.dataTask(with: try URLRequest.requestSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, trustedPhoneID: trustedPhoneNumber.id)) + .map { _ in + self.promptForSMSSecurityCode(length: authOptions.securityCode.length, for: trustedPhoneNumber) + } + } + .then { code in + Current.network.dataTask(with: try URLRequest.submitSecurityCode(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, code: code)) + .validateSecurityCodeResponse() + } + .then { (data, response) -> Promise in + self.updateSession(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) + } + } +} + +public extension Promise where T == (data: Data, response: URLResponse) { + func validateSecurityCodeResponse() -> Promise { + validate() + .recover { error -> Promise<(data: Data, response: URLResponse)> in + switch error { + case PMKHTTPError.badStatusCode(let code, _, _): + if code == 401 { + throw Client.Error.incorrectSecurityCode + } else { + throw error + } + default: + throw error + } + } + } +} + +struct AuthOptionsResponse: Decodable { + let trustedPhoneNumbers: [TrustedPhoneNumber]? + let trustedDevices: [TrustedDevice]? + let securityCode: SecurityCodeInfo + let noTrustedDevices: Bool? + let serviceErrors: [ServiceError]? + + var kind: Kind { + if trustedDevices != nil { + return .twoStep + } else if trustedPhoneNumbers != nil { + return .twoFactor + } else { + return .unknown + } + } + + // One time with a new testing account I had a response where noTrustedDevices was nil, but the account didn't have any trusted devices. + // This should have been a situation where an SMS security code was sent automatically. + // This resolved itself either after some time passed, or by signing into appleid.apple.com with the account. + // Not sure if it's worth explicitly handling this case or if it'll be really rare. + var canFallBackToSMS: Bool { + noTrustedDevices == true + } + + var smsAutomaticallySent: Bool { + trustedPhoneNumbers?.count == 1 && canFallBackToSMS + } + + struct TrustedPhoneNumber: Decodable { + let id: Int + let numberWithDialCode: String + } + + struct TrustedDevice: Decodable { + let id: String + let name: String + let modelName: String + } + + struct SecurityCodeInfo: Decodable { + let length: Int + let tooManyCodesSent: Bool + let tooManyCodesValidated: Bool + let securityCodeLocked: Bool + let securityCodeCooldown: Bool + } + + enum Kind { + case twoStep, twoFactor, unknown + } +} + +public struct ServiceError: Decodable, Equatable { + let code: String + let message: String +} + +enum SecurityCode { + case device(code: String) + case sms(code: String, phoneNumberId: Int) + + var urlPathComponent: String { + switch self { + case .device: return "trusteddevice" + case .sms: return "phone" } } } diff --git a/Sources/AppleAPI/Environment.swift b/Sources/AppleAPI/Environment.swift new file mode 100644 index 0000000..3977321 --- /dev/null +++ b/Sources/AppleAPI/Environment.swift @@ -0,0 +1,41 @@ +import Foundation +import PromiseKit +import PMKFoundation + +/** + Lightweight dependency injection using global mutable state :P + + - SeeAlso: https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy + - SeeAlso: https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable + - SeeAlso: https://vimeo.com/291588126 + */ +public struct Environment { + public var shell = Shell() + public var network = Network() + public var logging = Logging() +} + +public var Current = Environment() + +public struct Shell { + public var readLine: (String) -> String? = { prompt in + print(prompt, terminator: "") + return Swift.readLine() + } + public func readLine(prompt: String) -> String? { + readLine(prompt) + } +} + +public struct Network { + public var session = URLSession.shared + + public var dataTask: (URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> = { Current.network.session.dataTask(.promise, with: $0) } + public func dataTask(with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> { + dataTask(convertible) + } +} + +public struct Logging { + public var log: (String) -> Void = { print($0) } +} diff --git a/Sources/AppleAPI/URLRequest+Apple.swift b/Sources/AppleAPI/URLRequest+Apple.swift index 459be8a..c6f33aa 100644 --- a/Sources/AppleAPI/URLRequest+Apple.swift +++ b/Sources/AppleAPI/URLRequest+Apple.swift @@ -4,7 +4,8 @@ extension URL { static let itcServiceKey = URL(string: "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com")! static let signIn = URL(string: "https://idmsa.apple.com/appleauth/auth/signin")! static let authOptions = URL(string: "https://idmsa.apple.com/appleauth/auth")! - static let submitSecurityCode = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode")! + static let requestSecurityCode = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/phone")! + static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! } static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")! static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")! } @@ -41,25 +42,52 @@ extension URLRequest { request.allHTTPHeaderFields?["accept"] = "application/json" return request } + + static func requestSecurityCode(serviceKey: String, sessionID: String, scnt: String, trustedPhoneID: Int) throws -> URLRequest { + struct Body: Encodable { + let phoneNumber: PhoneNumber + let mode = "sms" + + struct PhoneNumber: Encodable { + let id: Int + } + } + + var request = URLRequest(url: .requestSecurityCode) + request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + request.allHTTPHeaderFields?["Content-Type"] = "application/json" + request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID + request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey + request.allHTTPHeaderFields?["scnt"] = scnt + request.allHTTPHeaderFields?["accept"] = "application/json" + request.httpMethod = "PUT" + request.httpBody = try JSONEncoder().encode(Body(phoneNumber: .init(id: trustedPhoneID))) + return request + } - static func submitSecurityCode(serviceKey: String, sessionID: String, scnt: String, code: String) throws -> URLRequest { - struct SecurityCode: Encodable { - let code: String - - enum CodingKeys: String, CodingKey { - case securityCode + static func submitSecurityCode(serviceKey: String, sessionID: String, scnt: String, code: SecurityCode) throws -> URLRequest { + struct DeviceSecurityCodeRequest: Encodable { + let securityCode: SecurityCode + + struct SecurityCode: Encodable { + let code: String } - enum SecurityCodeCodingKeys: String, CodingKey { - case code + } + + struct SMSSecurityCodeRequest: Encodable { + let securityCode: SecurityCode + let phoneNumber: PhoneNumber + let mode = "sms" + + struct SecurityCode: Encodable { + let code: String } - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - var securityCode = container.nestedContainer(keyedBy: SecurityCodeCodingKeys.self, forKey: .securityCode) - try securityCode.encode(code, forKey: .code) + struct PhoneNumber: Encodable { + let id: Int } } - var request = URLRequest(url: .submitSecurityCode) + var request = URLRequest(url: .submitSecurityCode(code)) request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey @@ -67,7 +95,12 @@ extension URLRequest { request.allHTTPHeaderFields?["Accept"] = "application/json" request.allHTTPHeaderFields?["Content-Type"] = "application/json" request.httpMethod = "POST" - request.httpBody = try JSONEncoder().encode(SecurityCode(code: code)) + switch code { + case .device(let code): + request.httpBody = try JSONEncoder().encode(DeviceSecurityCodeRequest(securityCode: .init(code: code))) + case .sms(let code, let phoneNumberId): + request.httpBody = try JSONEncoder().encode(SMSSecurityCodeRequest(securityCode: .init(code: code), phoneNumber: .init(id: phoneNumberId))) + } return request } diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index be88edf..7027128 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -160,12 +160,12 @@ private func installedXcodes() -> [InstalledXcode] { public struct Network { private static let client = AppleAPI.Client() - public var dataTask: (URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> = { client.session.dataTask(.promise, with: $0) } + public var dataTask: (URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> = { AppleAPI.Current.network.session.dataTask(.promise, with: $0) } public func dataTask(with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> { dataTask(convertible) } - public var downloadTask: (URLRequestConvertible, URL, Data?) -> (Progress, Promise<(saveLocation: URL, response: URLResponse)>) = { client.session.downloadTask(with: $0, to: $1, resumingWith: $2) } + public var downloadTask: (URLRequestConvertible, URL, Data?) -> (Progress, Promise<(saveLocation: URL, response: URLResponse)>) = { AppleAPI.Current.network.session.downloadTask(with: $0, to: $1, resumingWith: $2) } public func downloadTask(with convertible: URLRequestConvertible, to saveLocation: URL, resumingWith resumeData: Data?) -> (progress: Progress, promise: Promise<(saveLocation: URL, response: URLResponse)>) { return downloadTask(convertible, saveLocation, resumeData) @@ -173,7 +173,7 @@ public struct Network { public var validateSession: () -> Promise = client.validateSession - public var login: (String, String) -> Promise = client.login(accountName:password:) + public var login: (String, String) -> Promise = { client.login(accountName: $0, password: $1) } public func login(accountName: String, password: String) -> Promise { login(accountName, password) } diff --git a/Tests/AppleAPITests/AppleAPITests.swift b/Tests/AppleAPITests/AppleAPITests.swift index 97f3574..1c3a848 100644 --- a/Tests/AppleAPITests/AppleAPITests.swift +++ b/Tests/AppleAPITests/AppleAPITests.swift @@ -3,6 +3,11 @@ import PromiseKit import PMKFoundation @testable import AppleAPI +func fixture(for url: URL, fileURL: URL? = nil, statusCode: Int, headers: [String: String]) -> Promise<(data: Data, response: URLResponse)> { + .value((data: fileURL != nil ? try! Data(contentsOf: fileURL!) : Data(), + response: HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)!)) +} + final class AppleAPITests: XCTestCase { override class func setUp() { super.setUp() @@ -11,5 +16,576 @@ final class AppleAPITests: XCTestCase { } override func setUp() { + Current = .mock + } + + func test_Login_2FA_Succeeds() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + // security code + return "000000" + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_2FA_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_2FA_Succeeds")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_2FA_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .submitSecurityCode(.device(code: "000000")): + return fixture(for: .submitSecurityCode(.device(code: "000000")), + statusCode: 204, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .trust: + return fixture(for: .trust, + statusCode: 204, + headers: [:]) + case .olympusSession: + return fixture(for: .olympusSession, + fileURL: Bundle.module.url(forResource: "OlympusSession", withExtension: "json", subdirectory: "Fixtures/Login_2FA_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail() + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise fulfills") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .fulfilled = result else { + XCTFail("login rejected") + return + } + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + Enter "sms" without quotes to exit this prompt and choose a phone number to send an SMS security code to. + Enter the 6 digit code from one of your trusted devices: + + """) + } + + func test_Login_2FA_IncorrectPassword() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + // security code + return "000000" + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + statusCode: 401, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail() + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise rejects") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .rejected(let error as AppleAPI.Client.Error) = result else { + XCTFail("login fulfilled, but should have rejected with .invalidUsernameOrPassword error") + return + } + XCTAssertEqual(error, AppleAPI.Client.Error.invalidUsernameOrPassword(username: "test@example.com")) + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, "") + } + + func test_Login_SMS_SentAutomatically_Succeeds() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + // security code + return "000000" + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_Succeeds")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .requestSecurityCode: + return fixture(for: .requestSecurityCode, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)): + return fixture(for: .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)), + statusCode: 204, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .trust: + return fixture(for: .trust, + statusCode: 204, + headers: [:]) + case .olympusSession: + return fixture(for: .olympusSession, + fileURL: Bundle.module.url(forResource: "OlympusSession", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail("Unexpected request to \(convertible.pmkRequest.url!)") + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise fulfills") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .fulfilled = result else { + XCTFail("login rejected") + return + } + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + Enter the 6 digit code sent to +1 (•••) •••-••00: + + """) + } + + func test_Login_SMS_SentAutomatically_IncorrectCode() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + // security code + return "000000" + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_IncorrectCode")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_IncorrectCode")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_SMS_SentAutomatically_IncorrectCode")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .requestSecurityCode: + return fixture(for: .requestSecurityCode, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)): + return fixture(for: .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)), + statusCode: 401, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail("Unexpected request to \(convertible.pmkRequest.url!)") + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise rejects") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .rejected(let error as AppleAPI.Client.Error) = result else { + XCTFail("login fulfilled, but should have rejected with .incorrectSecurityCode error") + return + } + XCTAssertEqual(error, AppleAPI.Client.Error.incorrectSecurityCode) + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + Enter the 6 digit code sent to +1 (•••) •••-••00: + + """) + } + + func test_Login_SMS_MultipleNumbers_Succeeds() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + switch readLineCount { + case 0: + // invalid phone number index + return "3" + case 1: + // phone number index + return "1" + case 2: + // security code + return "000000" + default: + XCTFail() + return "" + } + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_Succeeds")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .requestSecurityCode: + return fixture(for: .requestSecurityCode, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)): + return fixture(for: .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)), + statusCode: 204, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .trust: + return fixture(for: .trust, + statusCode: 204, + headers: [:]) + case .olympusSession: + return fixture(for: .olympusSession, + fileURL: Bundle.module.url(forResource: "OlympusSession", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_Succeeds")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail("Unexpected request to \(convertible.pmkRequest.url!)") + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise fulfills") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .fulfilled = result else { + XCTFail("login rejected") + return + } + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + Trusted phone numbers: + 1: +1 (•••) •••-••00 + 2: +1 (•••) •••-••01 + Select a trusted phone number to receive a code via SMS: + Not a valid phone number index. Expecting a whole number between 1-2, but was given 3. + + Trusted phone numbers: + 1: +1 (•••) •••-••00 + 2: +1 (•••) •••-••01 + Select a trusted phone number to receive a code via SMS: + Enter the 6 digit code sent to +1 (•••) •••-••00: + + """) + } + + func test_Login_SMS_MultipleNumbers_IncorrectCode() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + if readLineCount == 0 { + // phone number index + return "1" + } else { + // security code + return "000000" + } + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_IncorrectCode")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_IncorrectCode")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_SMS_MultipleNumbers_IncorrectCode")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .requestSecurityCode: + return fixture(for: .requestSecurityCode, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)): + return fixture(for: .submitSecurityCode(.sms(code: "000000", phoneNumberId: 1)), + statusCode: 401, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail("Unexpected request to \(convertible.pmkRequest.url!)") + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise rejects") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .rejected(let error as AppleAPI.Client.Error) = result else { + XCTFail("login fulfilled, but should have rejected with .incorrectSecurityCode error") + return + } + XCTAssertEqual(error, AppleAPI.Client.Error.incorrectSecurityCode) + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + Trusted phone numbers: + 1: +1 (•••) •••-••00 + 2: +1 (•••) •••-••01 + Select a trusted phone number to receive a code via SMS: + Enter the 6 digit code sent to +1 (•••) •••-••00: + + """) + } + + func test_Login_SMS_NoNumbers() { + var log = "" + Current.logging.log = { log.append($0 + "\n") } + + var readLineCount = 0 + Current.shell.readLine = { prompt in + defer { readLineCount += 1 } + + Current.logging.log(prompt) + + switch readLineCount { + case 0: + // invalid phone number index + return "3" + case 1: + // phone number index + return "1" + case 2: + // security code + return "000000" + default: + XCTFail() + return "" + } + } + + Current.network.dataTask = { convertible in + switch convertible.pmkRequest.url! { + case .itcServiceKey: + return fixture(for: .itcServiceKey, + fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_SMS_NoNumbers")!, + statusCode: 200, + headers: ["Content-Type": "application/json"]) + case .signIn: + return fixture(for: .signIn, + fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_SMS_NoNumbers")!, + statusCode: 409, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + case .authOptions: + return fixture(for: .authOptions, + fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_SMS_NoNumbers")!, + statusCode: 200, + headers: ["Content-Type": "application/json", + "X-Apple-ID-Session-Id": "", + "scnt": ""]) + default: + XCTFail("Unexpected request to \(convertible.pmkRequest.url!)") + return .init(error: PMKError.invalidCallingConvention) + } + } + + let expectation = self.expectation(description: "promise rejects") + + let client = Client() + client.login(accountName: "test@example.com", password: "ABC123") + .tap { result in + guard case .rejected(let error as AppleAPI.Client.Error) = result else { + XCTFail("login fulfilled, but should have rejected with .noTrustedPhoneNumbers error") + return + } + XCTAssertEqual(error, AppleAPI.Client.Error.noTrustedPhoneNumbers) + expectation.fulfill() + } + .cauterize() + + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(log, """ + Two-factor authentication is enabled for this account. + + + """) } } diff --git a/Tests/AppleAPITests/Environment+Mock.swift b/Tests/AppleAPITests/Environment+Mock.swift new file mode 100644 index 0000000..57cfa9a --- /dev/null +++ b/Tests/AppleAPITests/Environment+Mock.swift @@ -0,0 +1,29 @@ +@testable import AppleAPI +import Foundation +import PromiseKit + +extension Environment { + static var mock = Environment( + shell: .mock, + network: .mock, + logging: .mock + ) +} + +extension Shell { + static var mock = Shell( + readLine: { _ in return nil } + ) +} + +extension Network { + static var mock = Network( + dataTask: { url in return Promise.value((data: Data(), response: HTTPURLResponse(url: url.pmkRequest.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)) } + ) +} + +extension Logging { + static var mock = Logging( + log: { print($0) } + ) +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/AuthOptions.json new file mode 100644 index 0000000..f521ff8 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/AuthOptions.json @@ -0,0 +1,34 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + } ], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/SignIn.json new file mode 100644 index 0000000..18a6cc0 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/SignIn.json @@ -0,0 +1,7 @@ +{ + "serviceErrors" : [ { + "code" : "-20101", + "message" : "Your Apple ID or password was incorrect.", + "suppressDismissal" : false + } ] +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/AuthOptions.json new file mode 100644 index 0000000..f521ff8 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/AuthOptions.json @@ -0,0 +1,34 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + } ], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/AuthOptions.json new file mode 100644 index 0000000..2bbc4bc --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/AuthOptions.json @@ -0,0 +1,41 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + { + "obfuscatedNumber" : "(•••) •••-••01", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••01", + "id" : 2 + }], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "noTrustedDevices" : true, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/AuthOptions.json new file mode 100644 index 0000000..2bbc4bc --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/AuthOptions.json @@ -0,0 +1,41 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + { + "obfuscatedNumber" : "(•••) •••-••01", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••01", + "id" : 2 + }], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "noTrustedDevices" : true, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/AuthOptions.json new file mode 100644 index 0000000..2910db7 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/AuthOptions.json @@ -0,0 +1,24 @@ +{ + "trustedPhoneNumbers" : [], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "noTrustedDevices" : true, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/AuthOptions.json new file mode 100644 index 0000000..6bfa630 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/AuthOptions.json @@ -0,0 +1,35 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + } ], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "noTrustedDevices" : true, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/AuthOptions.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/AuthOptions.json new file mode 100644 index 0000000..6bfa630 --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/AuthOptions.json @@ -0,0 +1,35 @@ +{ + "trustedPhoneNumbers" : [ { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + } ], + "securityCode" : { + "length" : 6, + "tooManyCodesSent" : false, + "tooManyCodesValidated" : false, + "securityCodeLocked" : false, + "securityCodeCooldown" : false + }, + "authenticationType" : "hsa2", + "recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142", + "repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone", + "repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair", + "aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921", + "autoVerified" : false, + "showAutoVerificationUI" : false, + "managedAccount" : false, + "noTrustedDevices" : true, + "trustedPhoneNumber" : { + "obfuscatedNumber" : "(•••) •••-••00", + "pushMode" : "sms", + "numberWithDialCode" : "+1 (•••) •••-••00", + "id" : 1 + }, + "hsa2Account" : true, + "restrictedAccount" : false, + "supportsRecovery" : true +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/ITCServiceKey.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/ITCServiceKey.json new file mode 100644 index 0000000..33c00bf --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/ITCServiceKey.json @@ -0,0 +1,4 @@ +{ + "authServiceUrl" : "https://idmsa.apple.com/appleauth", + "authServiceKey" : "NNNNN" +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/OlympusSession.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/OlympusSession.json new file mode 100644 index 0000000..07fb0cb --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/OlympusSession.json @@ -0,0 +1,128 @@ +{ + "user" : { + "fullName" : "Test User", + "firstName" : "Test", + "lastName" : "User", + "emailAddress" : "test@example.com", + "prsId" : "000000000" + }, + "provider" : { + "providerId" : 00000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL", + "pla" : [ { + "id" : "1BC01216-52D4-43DC-8555-195F4454C348", + "version" : "5014", + "types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ], + "contractCountryOfOrigins" : [ "CAN" ] + } ] + }, + "theme" : "APPSTORE_CONNECT", + "availableProviders" : [ { + "providerId" : 000000, + "name" : "Test User", + "contentTypes" : [ "SOFTWARE" ], + "subType" : "INDIVIDUAL" + } ], + "backingType" : "ITC", + "backingTypes" : [ "ITC" ], + "roles" : [ "ADMIN", "LEGAL" ], + "unverifiedRoles" : [ ], + "featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ], + "agreeToTerms" : true, + "termsSignatures" : [ "ASC", "RAD" ], + "modules" : [ { + "key" : "Apps", + "name" : "ITC.HomePage.Apps.IconText", + "localizedName" : "My Apps", + "url" : "https://appstoreconnect.apple.com/apps", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "AppAnalytics", + "name" : "ITC.HomePage.AppAnalytics.IconText", + "localizedName" : "App Analytics", + "url" : "https://analytics.itunes.apple.com/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "SalesTrends", + "name" : "ITC.HomePage.SalesTrends.IconText", + "localizedName" : "Sales and Trends", + "url" : "https://appstoreconnect.apple.com/trends", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "FinancialReports", + "name" : "ITC.HomePage.FinancialReports.IconText", + "localizedName" : "Payments and Financial Reports", + "url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Account", + "name" : "ITC.HomePage.Account.IconText", + "localizedName" : "Users and Access", + "url" : "https://appstoreconnect.apple.com/access/users", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "ContractsTaxBanking", + "name" : "ITC.HomePage.ContractsTaxBanking.IconText", + "localizedName" : "Agreements, Tax, and Banking", + "url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + }, { + "key" : "Resources", + "name" : "ITC.HomePage.Resources.IconText", + "localizedName" : "Resources and Help", + "url" : "https://developer.apple.com/app-store-connect/", + "iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png", + "down" : false, + "visible" : true, + "hasNotifications" : false + } ], + "helpLinks" : [ { + "key" : "AllAsc", + "url" : "https://help.apple.com/app-store-connect/", + "localizedText" : "App Store Connect Resources" + }, { + "key" : "Xcode", + "url" : "https://help.apple.com/xcode/mac/current/", + "localizedText" : "Xcode Help" + }, { + "key" : "SupportContact", + "url" : "https://developer.apple.com/support/", + "localizedText" : "Support and Contact" + } ], + "userProfile" : [ { + "key" : "signIn", + "url" : "https://appstoreconnect.apple.com/login", + "localizedText" : "Sign In" + }, { + "key" : "personalDetails", + "url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings", + "localizedText" : "Edit Profile" + }, { + "key" : "signOut", + "url" : "https://appstoreconnect.apple.com/logout", + "localizedText" : "Sign Out" + } ], + "pccDto" : null, + "publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA", + "ofacState" : null +} diff --git a/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/SignIn.json b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/SignIn.json new file mode 100644 index 0000000..10fb36e --- /dev/null +++ b/Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/SignIn.json @@ -0,0 +1,3 @@ +{ + "authType" : "hsa2" +} diff --git a/Tests/XcodesKitTests/Environment+Mock.swift b/Tests/XcodesKitTests/Environment+Mock.swift index 868c027..bc48da3 100644 --- a/Tests/XcodesKitTests/Environment+Mock.swift +++ b/Tests/XcodesKitTests/Environment+Mock.swift @@ -44,11 +44,11 @@ extension Files { moveItem: { _, _ in return }, contentsAtPath: { path in if path.contains("Info.plist") { - let url = URL(fileURLWithPath: "Stub-0.0.0.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path.contains("version.plist") { - let url = URL(fileURLWithPath: "Stub.version.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else { diff --git a/Tests/XcodesKitTests/LogOutput-FullHappyPath.txt b/Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt similarity index 100% rename from Tests/XcodesKitTests/LogOutput-FullHappyPath.txt rename to Tests/XcodesKitTests/Fixtures/LogOutput-FullHappyPath.txt diff --git a/Tests/XcodesKitTests/Stub-0.0.0.Info.plist b/Tests/XcodesKitTests/Fixtures/Stub-0.0.0.Info.plist similarity index 100% rename from Tests/XcodesKitTests/Stub-0.0.0.Info.plist rename to Tests/XcodesKitTests/Fixtures/Stub-0.0.0.Info.plist diff --git a/Tests/XcodesKitTests/Stub-2.0.0.Info.plist b/Tests/XcodesKitTests/Fixtures/Stub-2.0.0.Info.plist similarity index 100% rename from Tests/XcodesKitTests/Stub-2.0.0.Info.plist rename to Tests/XcodesKitTests/Fixtures/Stub-2.0.0.Info.plist diff --git a/Tests/XcodesKitTests/Stub-2.0.1.Info.plist b/Tests/XcodesKitTests/Fixtures/Stub-2.0.1.Info.plist similarity index 100% rename from Tests/XcodesKitTests/Stub-2.0.1.Info.plist rename to Tests/XcodesKitTests/Fixtures/Stub-2.0.1.Info.plist diff --git a/Tests/XcodesKitTests/Stub.version.plist b/Tests/XcodesKitTests/Fixtures/Stub.version.plist similarity index 100% rename from Tests/XcodesKitTests/Stub.version.plist rename to Tests/XcodesKitTests/Fixtures/Stub.version.plist diff --git a/Tests/XcodesKitTests/developer.apple.com-download-19-6-9.html b/Tests/XcodesKitTests/Fixtures/developer.apple.com-download-19-6-9.html similarity index 100% rename from Tests/XcodesKitTests/developer.apple.com-download-19-6-9.html rename to Tests/XcodesKitTests/Fixtures/developer.apple.com-download-19-6-9.html diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 726d0ad..2afe9b4 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -119,7 +119,7 @@ final class XcodesKitTests: XCTestCase { func test_InstallLogging_FullHappyPath() { var log = "" - Current.logging.log = { log.append($0 + "\n") } + XcodesKit.Current.logging.log = { log.append($0 + "\n") } // Don't have a valid session Current.network.validateSession = { Promise(error: AppleAPI.Client.Error.invalidSession) } @@ -133,7 +133,7 @@ final class XcodesKitTests: XCTestCase { } } // It's an available release version - Current.network.dataTask = { url in + XcodesKit.Current.network.dataTask = { url in if url.pmkRequest.url! == URLRequest.downloads.url! { let downloads = Downloads(downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip")], dateModified: Date())]) let encoder = JSONEncoder() @@ -174,7 +174,7 @@ final class XcodesKitTests: XCTestCase { } // Don't have superuser privileges the first time var validateSudoAuthenticationCallCount = 0 - Current.shell.validateSudoAuthentication = { + XcodesKit.Current.shell.validateSudoAuthentication = { validateSudoAuthenticationCallCount += 1 if validateSudoAuthenticationCallCount == 1 { @@ -185,13 +185,13 @@ final class XcodesKitTests: XCTestCase { } } // User enters password - Current.shell.readSecureLine = { prompt, _ in - Current.logging.log(prompt) + XcodesKit.Current.shell.readSecureLine = { prompt, _ in + XcodesKit.Current.logging.log(prompt) return "password" } // User enters something - Current.shell.readLine = { prompt in - Current.logging.log(prompt) + XcodesKit.Current.shell.readLine = { prompt in + XcodesKit.Current.logging.log(prompt) return "asdf" } @@ -199,8 +199,7 @@ final class XcodesKitTests: XCTestCase { installer.install(.version("0.0.0")) .ensure { - let url = URL(fileURLWithPath: "LogOutput-FullHappyPath.txt", - relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "LogOutput-FullHappyPath", withExtension: "txt", subdirectory: "Fixtures")! XCTAssertEqual(log, try! String(contentsOf: url)) expectation.fulfill() } @@ -221,19 +220,19 @@ final class XcodesKitTests: XCTestCase { Current.files.installedXcodes = { installedXcodes } Current.files.contentsAtPath = { path in if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-0.0.0.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path == "/Applications/Xcode-2.0.0.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-2.0.0.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-2.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path == "/Applications/Xcode-2.0.1.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-2.0.1.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path.contains("version.plist") { - let url = URL(fileURLWithPath: "Stub.version.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else { @@ -344,7 +343,7 @@ final class XcodesKitTests: XCTestCase { } func test_ParsePrereleaseXcodes() { - let url = URL(fileURLWithPath: "developer.apple.com-download-19-6-9.html", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "developer.apple.com-download-19-6-9", withExtension: "html", subdirectory: "Fixtures")! let data = try! Data(contentsOf: url) let xcodes = try! XcodeList().parsePrereleaseXcodes(from: data) @@ -355,7 +354,7 @@ final class XcodesKitTests: XCTestCase { func test_SelectPrint() { var log = "" - Current.logging.log = { log.append($0 + "\n") } + XcodesKit.Current.logging.log = { log.append($0 + "\n") } Current.files.installedXcodes = { [InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!, InstalledXcode(path: Path("/Applications/Xcode-2.0.0.app")!)!] } @@ -373,22 +372,22 @@ final class XcodesKitTests: XCTestCase { func test_SelectPath() { var log = "" - Current.logging.log = { log.append($0 + "\n") } + XcodesKit.Current.logging.log = { log.append($0 + "\n") } // There are installed Xcodes Current.files.installedXcodes = { [InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!, InstalledXcode(path: Path("/Applications/Xcode-2.0.1.app")!)!] } Current.files.contentsAtPath = { path in if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-0.0.0.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path == "/Applications/Xcode-2.0.1.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-2.0.1.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path.contains("version.plist") { - let url = URL(fileURLWithPath: "Stub.version.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else { @@ -420,7 +419,7 @@ final class XcodesKitTests: XCTestCase { } // User enters password Current.shell.readSecureLine = { prompt, _ in - Current.logging.log(prompt) + XcodesKit.Current.logging.log(prompt) return "password" } // It successfully switches @@ -441,22 +440,22 @@ final class XcodesKitTests: XCTestCase { func test_SelectInteractively() { var log = "" - Current.logging.log = { log.append($0 + "\n") } + XcodesKit.Current.logging.log = { log.append($0 + "\n") } // There are installed Xcodes Current.files.installedXcodes = { [InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)!, InstalledXcode(path: Path("/Applications/Xcode-2.0.1.app")!)!] } Current.files.contentsAtPath = { path in if path == "/Applications/Xcode-0.0.0.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-0.0.0.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-0.0.0.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path == "/Applications/Xcode-2.0.1.app/Contents/Info.plist" { - let url = URL(fileURLWithPath: "Stub-2.0.1.Info.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub-2.0.1.Info", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else if path.contains("version.plist") { - let url = URL(fileURLWithPath: "Stub.version.plist", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) + let url = Bundle.module.url(forResource: "Stub.version", withExtension: "plist", subdirectory: "Fixtures")! return try? Data(contentsOf: url) } else { @@ -481,8 +480,8 @@ final class XcodesKitTests: XCTestCase { } } // User enters an index - Current.shell.readLine = { prompt in - Current.logging.log(prompt) + XcodesKit.Current.shell.readLine = { prompt in + XcodesKit.Current.logging.log(prompt) return "1" } // Don't have superuser privileges the first time @@ -499,7 +498,7 @@ final class XcodesKitTests: XCTestCase { } // User enters password Current.shell.readSecureLine = { prompt, _ in - Current.logging.log(prompt) + XcodesKit.Current.logging.log(prompt) return "password" } // It successfully switches