From f226ee14f77313670b78ec7dd982abb5f920ea31 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Fri, 18 Sep 2020 22:17:44 -0600 Subject: [PATCH 1/5] Use Xcode 12, Swift 5.3 --- .github/workflows/ci.yml | 2 +- .swift-version | 2 +- Package.swift | 2 +- README.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb91ff..4b365cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,5 +7,5 @@ jobs: - uses: actions/checkout@v2.3.3 - name: Run tests env: - DEVELOPER_DIR: /Applications/Xcode_11.6.app + DEVELOPER_DIR: /Applications/Xcode_12.app run: swift test 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/Package.swift b/Package.swift index 36bad7f..cf45f0f 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( 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 From 48516cce2b5bfdeb6165998ab4915e791bc0e0cd Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Fri, 18 Sep 2020 22:18:53 -0600 Subject: [PATCH 2/5] Add simple success and failure tests for 2FA login --- Package.resolved | 9 ++ Package.swift | 8 +- Sources/AppleAPI/Client.swift | 2 +- Tests/AppleAPITests/AppleAPITests.swift | 89 ++++++++++++ .../AuthOptions.json | 34 +++++ .../ITCServiceKey.json | 4 + .../OlympusSession.json | 128 ++++++++++++++++++ .../Login_2FA_IncorrectPassword/SignIn.json | 7 + .../Login_2FA_Succeeds/AuthOptions.json | 34 +++++ .../Login_2FA_Succeeds/ITCServiceKey.json | 4 + .../Login_2FA_Succeeds/OlympusSession.json | 128 ++++++++++++++++++ .../Fixtures/Login_2FA_Succeeds/SignIn.json | 3 + 12 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_IncorrectPassword/SignIn.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_2FA_Succeeds/SignIn.json diff --git a/Package.resolved b/Package.resolved index 9e562b2..c6ccdd9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -37,6 +37,15 @@ "version": "1.0.1" } }, + { + "package": "OHHTTPStubs", + "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs", + "state": { + "branch": null, + "revision": "e92b5a5746ef16add2a1424f1fc19529d9a75cde", + "version": "9.0.0" + } + }, { "package": "Path.swift", "repositoryURL": "https://github.com/mxcl/Path.swift.git", diff --git a/Package.swift b/Package.swift index cf45f0f..a8b4f00 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,8 @@ 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")), + .package(url: "https://github.com/AliSoftware/OHHTTPStubs", .upToNextMinor(from: "9.0.0")), ], targets: [ .target( @@ -47,6 +48,9 @@ let package = Package( ]), .testTarget( name: "AppleAPITests", - dependencies: ["AppleAPI"]), + dependencies: ["AppleAPI", .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs")], + resources: [ + .copy("Fixtures"), + ]), ] ) diff --git a/Sources/AppleAPI/Client.swift b/Sources/AppleAPI/Client.swift index b3cc7c1..9cb0db3 100644 --- a/Sources/AppleAPI/Client.swift +++ b/Sources/AppleAPI/Client.swift @@ -8,7 +8,7 @@ public class Client { public init() {} - public enum Error: Swift.Error, LocalizedError { + public enum Error: Swift.Error, LocalizedError, Equatable { case invalidSession case invalidUsernameOrPassword(username: String) case unexpectedSignInResponse(statusCode: Int, message: String?) diff --git a/Tests/AppleAPITests/AppleAPITests.swift b/Tests/AppleAPITests/AppleAPITests.swift index 97f3574..f39a72d 100644 --- a/Tests/AppleAPITests/AppleAPITests.swift +++ b/Tests/AppleAPITests/AppleAPITests.swift @@ -1,4 +1,6 @@ import XCTest +import OHHTTPStubs +import OHHTTPStubsSwift import PromiseKit import PMKFoundation @testable import AppleAPI @@ -12,4 +14,91 @@ final class AppleAPITests: XCTestCase { override func setUp() { } + + override func tearDown() { + HTTPStubs.removeAllStubs() + super.tearDown() + } + + func test_Login_2FA_Succeeds() { + stub(condition: isAbsoluteURLString(URL.itcServiceKey.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "ITCServiceKey", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.signIn.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "SignIn", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.authOptions.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "AuthOptions", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.submitSecurityCode.absoluteString)) { _ in + HTTPStubsResponse(data: Data(), statusCode: 204, headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.trust.absoluteString)) { _ in + HTTPStubsResponse(data: Data(), statusCode: 204, headers: nil) + } + stub(condition: isAbsoluteURLString(URL.olympusSession.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "OlympusSession", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, + headers: ["Content-Type": "application/json"]) + } + + 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) + } + + func test_Login_2FA_IncorrectPassword() { + stub(condition: isAbsoluteURLString(URL.itcServiceKey.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "ITCServiceKey", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.signIn.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "SignIn", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + status: 401, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.authOptions.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "AuthOptions", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.submitSecurityCode.absoluteString)) { _ in + HTTPStubsResponse(data: Data(), statusCode: 204, headers: ["Content-Type": "application/json"]) + } + stub(condition: isAbsoluteURLString(URL.trust.absoluteString)) { _ in + HTTPStubsResponse(data: Data(), statusCode: 204, headers: nil) + } + stub(condition: isAbsoluteURLString(URL.olympusSession.absoluteString)) { _ in + fixture(filePath: Bundle.module.path(forResource: "OlympusSession", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, + headers: ["Content-Type": "application/json"]) + } + + 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) + } } 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" +} From 6287f35e93117d029e495111acfa507fcebb5f41 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Fri, 18 Sep 2020 22:33:33 -0600 Subject: [PATCH 3/5] Use SPM resource support for XcodeKitTests --- Package.swift | 3 +++ Tests/XcodesKitTests/Environment+Mock.swift | 4 +-- .../LogOutput-FullHappyPath.txt | 0 .../{ => Fixtures}/Stub-0.0.0.Info.plist | 0 .../{ => Fixtures}/Stub-2.0.0.Info.plist | 0 .../{ => Fixtures}/Stub-2.0.1.Info.plist | 0 .../{ => Fixtures}/Stub.version.plist | 0 .../developer.apple.com-download-19-6-9.html | 0 Tests/XcodesKitTests/XcodesKitTests.swift | 25 +++++++++---------- 9 files changed, 17 insertions(+), 15 deletions(-) rename Tests/XcodesKitTests/{ => Fixtures}/LogOutput-FullHappyPath.txt (100%) rename Tests/XcodesKitTests/{ => Fixtures}/Stub-0.0.0.Info.plist (100%) rename Tests/XcodesKitTests/{ => Fixtures}/Stub-2.0.0.Info.plist (100%) rename Tests/XcodesKitTests/{ => Fixtures}/Stub-2.0.1.Info.plist (100%) rename Tests/XcodesKitTests/{ => Fixtures}/Stub.version.plist (100%) rename Tests/XcodesKitTests/{ => Fixtures}/developer.apple.com-download-19-6-9.html (100%) diff --git a/Package.swift b/Package.swift index a8b4f00..808afed 100644 --- a/Package.swift +++ b/Package.swift @@ -40,6 +40,9 @@ let package = Package( name: "XcodesKitTests", dependencies: [ "XcodesKit", "Version" + ], + resources: [ + .copy("Fixtures"), ]), .target( name: "AppleAPI", 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..90ad7df 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -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) @@ -380,15 +379,15 @@ final class XcodesKitTests: XCTestCase { 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 { @@ -448,15 +447,15 @@ final class XcodesKitTests: XCTestCase { 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 { From 24b4aa0c3fb4dac2ec14ca11a2d6a38dbc6d8e0b Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Tue, 22 Sep 2020 21:47:17 -0600 Subject: [PATCH 4/5] Add support for two-factor via SMS --- Apple.paw | Bin 22209 -> 30979 bytes Package.resolved | 9 - Package.swift | 3 +- Sources/AppleAPI/Client.swift | 231 ++++++- Sources/AppleAPI/Environment.swift | 41 ++ Sources/AppleAPI/URLRequest+Apple.swift | 63 +- Sources/XcodesKit/Environment.swift | 6 +- Tests/AppleAPITests/AppleAPITests.swift | 573 ++++++++++++++++-- Tests/AppleAPITests/Environment+Mock.swift | 29 + .../AuthOptions.json | 41 ++ .../ITCServiceKey.json | 4 + .../OlympusSession.json | 128 ++++ .../SignIn.json | 3 + .../AuthOptions.json | 41 ++ .../ITCServiceKey.json | 4 + .../OlympusSession.json | 128 ++++ .../SignIn.json | 3 + .../Login_SMS_NoNumbers/AuthOptions.json | 24 + .../Login_SMS_NoNumbers/ITCServiceKey.json | 4 + .../Login_SMS_NoNumbers/OlympusSession.json | 128 ++++ .../Fixtures/Login_SMS_NoNumbers/SignIn.json | 3 + .../AuthOptions.json | 35 ++ .../ITCServiceKey.json | 4 + .../OlympusSession.json | 128 ++++ .../SignIn.json | 3 + .../AuthOptions.json | 35 ++ .../ITCServiceKey.json | 4 + .../OlympusSession.json | 128 ++++ .../SignIn.json | 3 + Tests/XcodesKitTests/XcodesKitTests.swift | 28 +- 30 files changed, 1731 insertions(+), 103 deletions(-) create mode 100644 Sources/AppleAPI/Environment.swift create mode 100644 Tests/AppleAPITests/Environment+Mock.swift create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_IncorrectCode/SignIn.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_MultipleNumbers_Succeeds/SignIn.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_NoNumbers/SignIn.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_IncorrectCode/SignIn.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/AuthOptions.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/ITCServiceKey.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/OlympusSession.json create mode 100644 Tests/AppleAPITests/Fixtures/Login_SMS_SentAutomatically_Succeeds/SignIn.json diff --git a/Apple.paw b/Apple.paw index 50dc39c07d566daab453ccb116c75295c7b0b166..f1efcf42a04654184a31191fb7983e7dcd912ad3 100644 GIT binary patch delta 14992 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

S+;N~PdNW3~RwGUmm180o0(OU3 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 E0mum4E&u=k diff --git a/Package.resolved b/Package.resolved index c6ccdd9..9e562b2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -37,15 +37,6 @@ "version": "1.0.1" } }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs", - "state": { - "branch": null, - "revision": "e92b5a5746ef16add2a1424f1fc19529d9a75cde", - "version": "9.0.0" - } - }, { "package": "Path.swift", "repositoryURL": "https://github.com/mxcl/Path.swift.git", diff --git a/Package.swift b/Package.swift index 808afed..c4f7bc1 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,6 @@ let package = Package( .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/AliSoftware/OHHTTPStubs", .upToNextMinor(from: "9.0.0")), ], targets: [ .target( @@ -51,7 +50,7 @@ let package = Package( ]), .testTarget( name: "AppleAPITests", - dependencies: ["AppleAPI", .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs")], + dependencies: ["AppleAPI"], resources: [ .copy("Fixtures"), ]), diff --git a/Sources/AppleAPI/Client.swift b/Sources/AppleAPI/Client.swift index 9cb0db3..ba47128 100644 --- a/Sources/AppleAPI/Client.swift +++ b/Sources/AppleAPI/Client.swift @@ -3,7 +3,6 @@ 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() {} @@ -11,8 +10,11 @@ public class Client { 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 f39a72d..1c3a848 100644 --- a/Tests/AppleAPITests/AppleAPITests.swift +++ b/Tests/AppleAPITests/AppleAPITests.swift @@ -1,10 +1,13 @@ import XCTest -import OHHTTPStubs -import OHHTTPStubsSwift 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() @@ -13,35 +16,206 @@ final class AppleAPITests: XCTestCase { } override func setUp() { - } - - override func tearDown() { - HTTPStubs.removeAllStubs() - super.tearDown() + Current = .mock } func test_Login_2FA_Succeeds() { - stub(condition: isAbsoluteURLString(URL.itcServiceKey.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "ITCServiceKey", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, - headers: ["Content-Type": "application/json"]) + 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" } - stub(condition: isAbsoluteURLString(URL.signIn.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "SignIn", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, - headers: ["Content-Type": "application/json"]) + + 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) + } } - stub(condition: isAbsoluteURLString(URL.authOptions.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "AuthOptions", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, - headers: ["Content-Type": "application/json"]) + + 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" } - stub(condition: isAbsoluteURLString(URL.submitSecurityCode.absoluteString)) { _ in - HTTPStubsResponse(data: Data(), statusCode: 204, headers: ["Content-Type": "application/json"]) + + 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) + } } - stub(condition: isAbsoluteURLString(URL.trust.absoluteString)) { _ in - HTTPStubsResponse(data: Data(), statusCode: 204, headers: nil) + + 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" } - stub(condition: isAbsoluteURLString(URL.olympusSession.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "OlympusSession", ofType: "json", inDirectory: "Fixtures/Login_2FA_Succeeds")!, - headers: ["Content-Type": "application/json"]) + + 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") @@ -58,31 +232,338 @@ final class AppleAPITests: XCTestCase { .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_2FA_IncorrectPassword() { - stub(condition: isAbsoluteURLString(URL.itcServiceKey.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "ITCServiceKey", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, - headers: ["Content-Type": "application/json"]) + 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" } - stub(condition: isAbsoluteURLString(URL.signIn.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "SignIn", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, - status: 401, - headers: ["Content-Type": "application/json"]) + + 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) + } } - stub(condition: isAbsoluteURLString(URL.authOptions.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "AuthOptions", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, - headers: ["Content-Type": "application/json"]) + + 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" + } } - stub(condition: isAbsoluteURLString(URL.submitSecurityCode.absoluteString)) { _ in - HTTPStubsResponse(data: Data(), statusCode: 204, headers: ["Content-Type": "application/json"]) + + 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) + } } - stub(condition: isAbsoluteURLString(URL.trust.absoluteString)) { _ in - HTTPStubsResponse(data: Data(), statusCode: 204, headers: nil) + + 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 "" + } } - stub(condition: isAbsoluteURLString(URL.olympusSession.absoluteString)) { _ in - fixture(filePath: Bundle.module.path(forResource: "OlympusSession", ofType: "json", inDirectory: "Fixtures/Login_2FA_IncorrectPassword")!, - headers: ["Content-Type": "application/json"]) + + 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") @@ -91,14 +572,20 @@ final class AppleAPITests: XCTestCase { 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") + XCTFail("login fulfilled, but should have rejected with .noTrustedPhoneNumbers error") return } - XCTAssertEqual(error, AppleAPI.Client.Error.invalidUsernameOrPassword(username: "test@example.com")) + 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_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/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 90ad7df..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" } @@ -354,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")!)!] } @@ -372,7 +372,7 @@ 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")!)!, @@ -419,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 @@ -440,7 +440,7 @@ 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")!)!, @@ -480,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 @@ -498,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 From 6576aa576995db77d58c0c02f12ced0a5a4a1a0f Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Mon, 5 Oct 2020 20:12:44 -0600 Subject: [PATCH 5/5] Use xcodebuild instead of swift test as a workaround for SPM package resource bug https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051/10 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b365cc..92f2678 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,4 +8,6 @@ jobs: - name: Run tests env: DEVELOPER_DIR: /Applications/Xcode_12.app - run: swift test + # 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