Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Imple a sugar-coating method (try-await-catch) for the verify function that can throw VerificationError #39

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Sources/AppStoreServerLibrary/ChainVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public enum VerificationResult<T> {
case invalid(VerificationError)
}

public enum VerificationError: Hashable, Sendable {
public enum VerificationError: Error, Hashable, Sendable {
case INVALID_JWT_FORMAT
case INVALID_CERTIFICATE
case VERIFICATION_FAILURE
Expand Down
70 changes: 66 additions & 4 deletions Sources/AppStoreServerLibrary/SignedDataVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ public struct SignedDataVerifier {
}
return renewalInfoResult
}

/// Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device
///
/// - Parameter signedRenewalInfo The signedRenewalInfo field
/// - Throws: the reason for verification failure error of type `VerificationError`
/// - Returns: the decoded renewal info after verification
public func verifyAndDecodeRenewalInfoThrowing(signedRenewalInfo: String) async throws -> JWSRenewalInfoDecodedPayload {
let result: VerificationResult<JWSRenewalInfoDecodedPayload> = await verifyAndDecodeRenewalInfo(signedRenewalInfo: signedRenewalInfo)
switch result {
case .valid(let result):
return result
case .invalid(let verificationError):
throw verificationError
}
}

/// Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device
/// See [JWSTransaction](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction)
///
Expand All @@ -70,6 +86,22 @@ public struct SignedDataVerifier {
}
return transactionResult
}

/// Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device
///
/// - Parameter signedTransaction The signedTransaction field
/// - Throws: the reason for verification failure error of type `VerificationError`
/// - Returns: the decoded transaction info after verification
public func verifyAndDecodeTransactionThrowing(signedTransaction: String) async throws -> JWSTransactionDecodedPayload {
let result: VerificationResult<JWSTransactionDecodedPayload> = await verifyAndDecodeTransaction(signedTransaction: signedTransaction)
switch result {
case .valid(let result):
return result
case .invalid(let verificationError):
throw verificationError
}
}

/// Verifies and decodes an App Store Server Notification signedPayload
/// See [signedPayload](https://developer.apple.com/documentation/appstoreservernotifications/signedpayload)
///
Expand Down Expand Up @@ -125,12 +157,27 @@ public struct SignedDataVerifier {
}
return nil
}

/// Verifies and decodes an App Store Server Notification signedPayload
///
/// - Parameter signedPayload The payload received by your server
/// - Throws: the reason for verification failure error of type `VerificationError`
/// - Returns: the decoded payload after verification
public func verifyAndDecodeNotificationThrowing(signedPayload: String) async throws -> ResponseBodyV2DecodedPayload {
let result: VerificationResult<ResponseBodyV2DecodedPayload> = await verifyAndDecodeNotification(signedPayload: signedPayload)
switch result {
case .valid(let result):
return result
case .invalid(let verificationError):
throw verificationError
}
}

///Verifies and decodes a signed AppTransaction
///See [AppTransaction](https://developer.apple.com/documentation/storekit/apptransaction)
/// Verifies and decodes a signed AppTransaction
/// See [AppTransaction](https://developer.apple.com/documentation/storekit/apptransaction)
///
///- Parameter signedAppTransaction The signed AppTransaction
///- Returns: If success, the decoded AppTransaction after validation, else the reason for verification failure
/// - Parameter signedAppTransaction The signed AppTransaction
/// - Returns: If success, the decoded AppTransaction after validation, else the reason for verification failure
public func verifyAndDecodeAppTransaction(signedAppTransaction: String) async -> VerificationResult<AppTransaction> {
let appTransactionResult = await decodeSignedData(signedData: signedAppTransaction, type: AppTransaction.self)
switch appTransactionResult {
Expand All @@ -148,6 +195,21 @@ public struct SignedDataVerifier {
return appTransactionResult
}

/// Verifies and decodes a signed AppTransaction
///
/// - Parameter signedAppTransaction The signed AppTransaction
/// - Throws: the reason for verification failure error of type `VerificationError`
/// - Returns: the decoded AppTransaction after validation
public func verifyAndDecodeAppTransactionThrowing(signedAppTransaction: String) async throws -> AppTransaction {
let result: VerificationResult<AppTransaction> = await verifyAndDecodeAppTransaction(signedAppTransaction: signedAppTransaction)
switch result {
case .valid(let result):
return result
case .invalid(let verificationError):
throw verificationError
}
}

private func decodeSignedData<T: DecodedSignedData>(signedData: String, type: T.Type) async -> VerificationResult<T> where T : Decodable {
return await chainVerifier.verify(signedData: signedData, type: type, onlineVerification: self.enableOnlineChecks, environment: self.environment)
}
Expand Down
145 changes: 139 additions & 6 deletions Tests/AppStoreServerLibraryTests/SignedModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class SignedModelTests: XCTestCase {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedNotification.json")

let verifiedNotification = await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotification(signedPayload: signedNotification)

guard case .valid(let notification) = verifiedNotification else {
XCTAssertTrue(false)
return
Expand All @@ -36,11 +36,37 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(1, notification.data!.rawStatus)
}

public func testNotificationDecodingThrowing() async throws {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedNotification.json")

let notification = try await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotificationThrowing(signedPayload: signedNotification)

XCTAssertEqual(NotificationTypeV2.subscribed, notification.notificationType)
XCTAssertEqual("SUBSCRIBED", notification.rawNotificationType)
XCTAssertEqual(Subtype.initialBuy, notification.subtype)
XCTAssertEqual("INITIAL_BUY", notification.rawSubtype)
XCTAssertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
XCTAssertEqual("2.0", notification.version)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), notification.signedDate)
XCTAssertNotNil(notification.data)
XCTAssertNil(notification.summary)
XCTAssertNil(notification.externalPurchaseToken)
XCTAssertEqual(Environment.localTesting, notification.data!.environment)
XCTAssertEqual("LocalTesting", notification.data!.rawEnvironment)
XCTAssertEqual(41234, notification.data!.appAppleId)
XCTAssertEqual("com.example", notification.data!.bundleId)
XCTAssertEqual("1.2.3", notification.data!.bundleVersion)
XCTAssertEqual("signed_transaction_info_value", notification.data!.signedTransactionInfo)
XCTAssertEqual("signed_renewal_info_value", notification.data!.signedRenewalInfo)
XCTAssertEqual(Status.active, notification.data!.status)
XCTAssertEqual(1, notification.data!.rawStatus)
}

public func testSummaryNotificationDecoding() async throws {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedSummaryNotification.json")

let verifiedNotification = await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotification(signedPayload: signedNotification)

guard case .valid(let notification) = verifiedNotification else {
XCTAssertTrue(false)
return
Expand All @@ -67,6 +93,32 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(2, notification.summary!.failedCount)
}

public func testSummaryNotificationDecodingThrowing() async throws {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedSummaryNotification.json")

let notification = try await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotificationThrowing(signedPayload: signedNotification)

XCTAssertEqual(NotificationTypeV2.renewalExtension, notification.notificationType)
XCTAssertEqual("RENEWAL_EXTENSION", notification.rawNotificationType)
XCTAssertEqual(Subtype.summary, notification.subtype)
XCTAssertEqual("SUMMARY", notification.rawSubtype)
XCTAssertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
XCTAssertEqual("2.0", notification.version)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), notification.signedDate)
XCTAssertNil(notification.data)
XCTAssertNotNil(notification.summary)
XCTAssertNil(notification.externalPurchaseToken)
XCTAssertEqual(Environment.localTesting, notification.summary!.environment)
XCTAssertEqual("LocalTesting", notification.summary!.rawEnvironment)
XCTAssertEqual(41234, notification.summary!.appAppleId)
XCTAssertEqual("com.example", notification.summary!.bundleId)
XCTAssertEqual("com.example.product", notification.summary!.productId)
XCTAssertEqual("efb27071-45a4-4aca-9854-2a1e9146f265", notification.summary!.requestIdentifier)
XCTAssertEqual(["CAN", "USA", "MEX"], notification.summary!.storefrontCountryCodes)
XCTAssertEqual(5, notification.summary!.succeededCount)
XCTAssertEqual(2, notification.summary!.failedCount)
}

public func testExternalPurchaseTokenNotificationDecoding() async throws {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedExternalPurchaseTokenNotification.json")

Expand All @@ -76,7 +128,7 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(.production, environment)
return nil
}

guard case .valid(let notification) = verifiedNotification else {
XCTAssertTrue(false)
return
Expand Down Expand Up @@ -107,7 +159,7 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(.sandbox, environment)
return nil
}

guard case .valid(let notification) = verifiedNotification else {
XCTAssertTrue(false)
return
Expand All @@ -133,7 +185,7 @@ final class SignedModelTests: XCTestCase {
let signedTransaction = TestingUtility.createSignedDataFromJson("resources/models/signedTransaction.json")

let verifiedTransaction = await TestingUtility.getSignedDataVerifier().verifyAndDecodeTransaction(signedTransaction: signedTransaction)

guard case .valid(let transaction) = verifiedTransaction else {
XCTAssertTrue(false)
return
Expand Down Expand Up @@ -170,11 +222,47 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual("LocalTesting", transaction.rawEnvironment)
}

public func testTransactionDecodingThrowing() async throws {
let signedTransaction = TestingUtility.createSignedDataFromJson("resources/models/signedTransaction.json")

let transaction = try await TestingUtility.getSignedDataVerifier().verifyAndDecodeTransactionThrowing(signedTransaction: signedTransaction)

XCTAssertEqual("12345", transaction.originalTransactionId)
XCTAssertEqual("23456", transaction.transactionId)
XCTAssertEqual("34343", transaction.webOrderLineItemId)
XCTAssertEqual("com.example", transaction.bundleId)
XCTAssertEqual("com.example.product", transaction.productId)
XCTAssertEqual("55555", transaction.subscriptionGroupIdentifier)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), transaction.originalPurchaseDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), transaction.purchaseDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148950), transaction.revocationDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698149000), transaction.expiresDate)
XCTAssertEqual(1, transaction.quantity)
XCTAssertEqual(ProductType.autoRenewableSubscription, transaction.type)
XCTAssertEqual("Auto-Renewable Subscription", transaction.rawType)
XCTAssertEqual(UUID(uuidString: "7e3fb20b-4cdb-47cc-936d-99d65f608138"), transaction.appAccountToken)
XCTAssertEqual(InAppOwnershipType.purchased, transaction.inAppOwnershipType)
XCTAssertEqual("PURCHASED", transaction.rawInAppOwnershipType)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), transaction.signedDate)
XCTAssertEqual(RevocationReason.refundedDueToIssue, transaction.revocationReason)
XCTAssertEqual(1, transaction.rawRevocationReason)
XCTAssertEqual("abc.123", transaction.offerIdentifier)
XCTAssertEqual(true, transaction.isUpgraded)
XCTAssertEqual(OfferType.introductoryOffer, transaction.offerType)
XCTAssertEqual(1, transaction.rawOfferType)
XCTAssertEqual("USA", transaction.storefront)
XCTAssertEqual("143441", transaction.storefrontId)
XCTAssertEqual(TransactionReason.purchase, transaction.transactionReason)
XCTAssertEqual("PURCHASE", transaction.rawTransactionReason)
XCTAssertEqual(Environment.localTesting, transaction.environment)
XCTAssertEqual("LocalTesting", transaction.rawEnvironment)
}

public func testRenewalInfoDecoding() async throws {
let signedRenewalInfo = TestingUtility.createSignedDataFromJson("resources/models/signedRenewalInfo.json")

let verifiedRenewalInfo = await TestingUtility.getSignedDataVerifier().verifyAndDecodeRenewalInfo(signedRenewalInfo: signedRenewalInfo)

guard case .valid(let renewalInfo) = verifiedRenewalInfo else {
XCTAssertTrue(false)
return
Expand All @@ -201,6 +289,32 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(Date(timeIntervalSince1970: 1698148850), renewalInfo.renewalDate)
}

public func testRenewalInfoDecodingThrowing() async throws {
let signedRenewalInfo = TestingUtility.createSignedDataFromJson("resources/models/signedRenewalInfo.json")

let renewalInfo = try await TestingUtility.getSignedDataVerifier().verifyAndDecodeRenewalInfoThrowing(signedRenewalInfo: signedRenewalInfo)

XCTAssertEqual(ExpirationIntent.customerCancelled, renewalInfo.expirationIntent)
XCTAssertEqual(1, renewalInfo.rawExpirationIntent)
XCTAssertEqual("12345", renewalInfo.originalTransactionId)
XCTAssertEqual("com.example.product.2", renewalInfo.autoRenewProductId)
XCTAssertEqual("com.example.product", renewalInfo.productId)
XCTAssertEqual(AutoRenewStatus.on, renewalInfo.autoRenewStatus)
XCTAssertEqual(1, renewalInfo.rawAutoRenewStatus)
XCTAssertEqual(true, renewalInfo.isInBillingRetryPeriod)
XCTAssertEqual(PriceIncreaseStatus.customerHasNotResponded, renewalInfo.priceIncreaseStatus)
XCTAssertEqual(0, renewalInfo.rawPriceIncreaseStatus)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), renewalInfo.gracePeriodExpiresDate)
XCTAssertEqual(OfferType.promotionalOffer, renewalInfo.offerType)
XCTAssertEqual(2, renewalInfo.rawOfferType)
XCTAssertEqual("abc.123", renewalInfo.offerIdentifier)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), renewalInfo.signedDate)
XCTAssertEqual(Environment.localTesting, renewalInfo.environment)
XCTAssertEqual("LocalTesting", renewalInfo.rawEnvironment)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), renewalInfo.recentSubscriptionStartDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148850), renewalInfo.renewalDate)
}

public func testAppTransactionDecoding() async throws {
let signedAppTransaction = TestingUtility.createSignedDataFromJson("resources/models/appTransaction.json")

Expand All @@ -225,6 +339,25 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual(Date(timeIntervalSince1970: 1698148700), appTransaction.preorderDate)
}

public func testAppTransactionDecodingThrowing() async throws {
let signedAppTransaction = TestingUtility.createSignedDataFromJson("resources/models/appTransaction.json")

let appTransaction = try await TestingUtility.getSignedDataVerifier().verifyAndDecodeAppTransactionThrowing(signedAppTransaction: signedAppTransaction)

XCTAssertEqual(Environment.localTesting, appTransaction.receiptType)
XCTAssertEqual("LocalTesting", appTransaction.rawReceiptType)
XCTAssertEqual(531412, appTransaction.appAppleId)
XCTAssertEqual("com.example", appTransaction.bundleId)
XCTAssertEqual("1.2.3", appTransaction.applicationVersion)
XCTAssertEqual(512, appTransaction.versionExternalIdentifier)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), appTransaction.receiptCreationDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), appTransaction.originalPurchaseDate)
XCTAssertEqual("1.1.2", appTransaction.originalApplicationVersion)
XCTAssertEqual("device_verification_value", appTransaction.deviceVerification)
XCTAssertEqual(UUID(uuidString: "48ccfa42-7431-4f22-9908-7e88983e105a"), appTransaction.deviceVerificationNonce)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148700), appTransaction.preorderDate)
}

// Xcode-generated dates are not well formed, therefore we only compare to ms precision
private func compareXcodeDates(_ first: Date, _ second: Date?) {
XCTAssertEqual(floor((first.timeIntervalSince1970 * 1000)), floor(((second?.timeIntervalSince1970 ?? 0.0) * 1000)))
Expand Down
Loading