Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions Sources/AppleAPI/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class Client {
case incorrectSecurityCode
case unexpectedSignInResponse(statusCode: Int, message: String?)
case appleIDAndPrivacyAcknowledgementRequired
case serviceTemporarilyUnavailable
case noTrustedPhoneNumbers
case notAuthenticated
case invalidHashcash
Expand All @@ -27,6 +28,8 @@ 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 .serviceTemporarilyUnavailable:
return "The service is temporarily unavailable. Please try again later."
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:
Expand Down Expand Up @@ -92,20 +95,25 @@ public class Client {
}

let httpResponse = response as! HTTPURLResponse
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)

switch httpResponse.statusCode {
case 200:
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
case 401:
throw Error.invalidUsernameOrPassword(username: accountName)
case 409:
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
throw Error.appleIDAndPrivacyAcknowledgementRequired
default:
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
do {
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
switch httpResponse.statusCode {
case 200:
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
case 401:
throw Error.invalidUsernameOrPassword(username: accountName)
case 409:
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
throw Error.appleIDAndPrivacyAcknowledgementRequired
default:
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
}
} catch DecodingError.dataCorrupted where httpResponse.statusCode == 503 {
throw Error.serviceTemporarilyUnavailable
} catch {
throw error
}
}
}
Expand Down
90 changes: 90 additions & 0 deletions Tests/AppleAPITests/AppleAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,96 @@ final class AppleAPITests: XCTestCase {
""")
}

func test_Login_Service_Temporarily_Unavailable() {
var log = ""
Current.logging.log = { log.append($0 + "\n") }

var readLineCount = 0
Current.shell.readLine = { prompt in
defer { readLineCount += 1 }

Current.logging.log(prompt)

// security code
return "000000"
}

Current.network.dataTask = { convertible in

switch convertible.pmkRequest.url! {
case .itcServiceKey:
return fixture(for: .itcServiceKey,
fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
statusCode: 200,
headers: ["Content-Type": "application/json"])
case .signIn:
if convertible.pmkRequest.httpMethod == "GET" {
return fixture(for: .signIn,
fileURL: Bundle.module.url(forResource: "Federate", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
statusCode: 200,
headers: ["Content-Type": "application/json",
"X-Apple-HC-Bits": "10",
"X-Apple-HC-Challenge": "somestring",
"scnt": ""])
} else {
return fixture(for: .signIn,
fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
statusCode: 503,
headers: ["Content-Type": "text/html",
"X-Apple-ID-Session-Id": "",
"scnt": ""])
}
case .authOptions:
return fixture(for: .authOptions,
fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
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_Service_Temporarily_Unavailable")!,
statusCode: 200,
headers: ["Content-Type": "application/json",
"X-Apple-ID-Session-Id": "",
"scnt": ""])
default:
print(convertible.pmkRequest.url!)
XCTFail()
return .init(error: PMKError.invalidCallingConvention)
}
}

let expectation = self.expectation(description: "promise fulfills")

let client = Client()
client.login(accountName: "test@example.com", password: "ABC123")
.tap { result in
guard case .rejected(let error as AppleAPI.Client.Error) = result else {
XCTFail("login fulfilled, but should have rejected with .noTrustedPhoneNumbers error")
return
}
XCTAssertEqual(error, AppleAPI.Client.Error.serviceTemporarilyUnavailable)
expectation.fulfill()
}
.cauterize()

wait(for: [expectation], timeout: 1.0)

XCTAssertEqual(log, "")
}


func testValidHashCashMint() {
let bits: UInt = 11
let resource = "4d74fb15eb23f465f1f6fcbf534e5877"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"authType" : "hsa2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"authServiceUrl" : "https://idmsa.apple.com/appleauth",
"authServiceKey" : "NNNNN"
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<html>

<head>
<title>503 Service Temporarily Unavailable</title>
</head>

<body>
<center>
<h1>503 Service Temporarily Unavailable</h1>
</center>
<hr>
<center>Apple</center>
</body>

</html>