From df498b31f329445739f9a606bc1ce495732fd270 Mon Sep 17 00:00:00 2001 From: socar-humphrey Date: Sun, 17 Sep 2023 22:21:59 +0900 Subject: [PATCH 1/4] feat: add pydantic --- requirements.txt | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 75650d0..83ed273 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,19 @@ -attrs >= 21.3.0 -PyJWT >= 2.6.0, < 3 -requests >= 2.28.0, < 3 -cryptography >= 40.0.0, < 42 -pyOpenSSL >= 23.1.1, < 24 +annotated-types==0.5.0 asn1==2.7.0 -cattrs==23.1.2 \ No newline at end of file +attrs==23.1.0 +cattrs==23.1.2 +certifi==2023.7.22 +cffi==1.15.1 +charset-normalizer==3.2.0 +cryptography==41.0.3 +enum-compat==0.0.3 +exceptiongroup==1.1.3 +idna==3.4 +pycparser==2.21 +pydantic==2.3.0 +pydantic_core==2.6.3 +PyJWT==2.8.0 +pyOpenSSL==23.2.0 +requests==2.31.0 +typing_extensions==4.7.1 +urllib3==2.0.4 From 4d721d6fea32092535faa1fdb8f5d26002cf9014 Mon Sep 17 00:00:00 2001 From: socar-humphrey Date: Sun, 17 Sep 2023 22:35:59 +0900 Subject: [PATCH 2/4] refactor: register pydantic models in v2 module --- .../models/v2/AccountTenure.py | 18 ++ .../models/v2/AppTransaction.py | 94 +++++++++ .../models/v2/AutoRenewStatus.py | 13 ++ appstoreserverlibrary/models/v2/Base.py | 5 + .../v2/CheckTestNotificationResponse.py | 26 +++ .../models/v2/ConsumptionRequest.py | 102 ++++++++++ .../models/v2/ConsumptionStatus.py | 14 ++ appstoreserverlibrary/models/v2/Data.py | 67 +++++++ .../models/v2/DeliveryStatus.py | 17 ++ .../models/v2/Environment.py | 12 ++ .../models/v2/ExpirationIntent.py | 15 ++ .../models/v2/ExtendReasonCode.py | 14 ++ .../models/v2/ExtendRenewalDateRequest.py | 37 ++++ .../models/v2/ExtendRenewalDateResponse.py | 42 ++++ .../models/v2/FirstSendAttemptResult.py | 22 +++ .../models/v2/HistoryResponse.py | 58 ++++++ .../models/v2/InAppOwnershipType.py | 12 ++ .../models/v2/JWSRenewalInfoDecodedPayload.py | 120 ++++++++++++ .../models/v2/JWSTransactionDecodedPayload.py | 181 ++++++++++++++++++ .../models/v2/LastTransactionsItem.py | 43 +++++ .../models/v2/LifetimeDollarsPurchased.py | 19 ++ .../models/v2/LifetimeDollarsRefunded.py | 19 ++ .../models/v2/MassExtendRenewalDateRequest.py | 53 +++++ .../v2/MassExtendRenewalDateResponse.py | 21 ++ .../v2/MassExtendRenewalDateStatusResponse.py | 49 +++++ .../models/v2/NotificationHistoryRequest.py | 61 ++++++ .../models/v2/NotificationHistoryResponse.py | 37 ++++ .../v2/NotificationHistoryResponseItem.py | 31 +++ .../models/v2/NotificationTypeV2.py | 28 +++ appstoreserverlibrary/models/v2/OfferType.py | 14 ++ .../models/v2/OrderLookupResponse.py | 28 +++ .../models/v2/OrderLookupStatus.py | 12 ++ appstoreserverlibrary/models/v2/Platform.py | 14 ++ appstoreserverlibrary/models/v2/PlayTime.py | 19 ++ .../models/v2/PriceIncreaseStatus.py | 13 ++ .../models/v2/RefundHistoryResponse.py | 35 ++++ .../models/v2/ResponseBodyV2.py | 23 +++ .../models/v2/ResponseBodyV2DecodedPayload.py | 69 +++++++ .../models/v2/RevocationReason.py | 13 ++ .../models/v2/SendAttemptItem.py | 29 +++ .../models/v2/SendAttemptResult.py | 22 +++ .../models/v2/SendTestNotificationResponse.py | 21 ++ appstoreserverlibrary/models/v2/Status.py | 16 ++ .../models/v2/StatusResponse.py | 43 +++++ .../v2/SubscriptionGroupIdentifierItem.py | 27 +++ appstoreserverlibrary/models/v2/Subtype.py | 27 +++ appstoreserverlibrary/models/v2/Summary.py | 73 +++++++ .../models/v2/TransactionHistoryRequest.py | 73 +++++++ .../models/v2/TransactionInfoResponse.py | 21 ++ .../models/v2/TransactionReason.py | 13 ++ appstoreserverlibrary/models/v2/Type.py | 15 ++ appstoreserverlibrary/models/v2/UserStatus.py | 16 ++ appstoreserverlibrary/models/v2/__init__.py | 0 53 files changed, 1866 insertions(+) create mode 100644 appstoreserverlibrary/models/v2/AccountTenure.py create mode 100644 appstoreserverlibrary/models/v2/AppTransaction.py create mode 100644 appstoreserverlibrary/models/v2/AutoRenewStatus.py create mode 100644 appstoreserverlibrary/models/v2/Base.py create mode 100644 appstoreserverlibrary/models/v2/CheckTestNotificationResponse.py create mode 100644 appstoreserverlibrary/models/v2/ConsumptionRequest.py create mode 100644 appstoreserverlibrary/models/v2/ConsumptionStatus.py create mode 100644 appstoreserverlibrary/models/v2/Data.py create mode 100644 appstoreserverlibrary/models/v2/DeliveryStatus.py create mode 100644 appstoreserverlibrary/models/v2/Environment.py create mode 100644 appstoreserverlibrary/models/v2/ExpirationIntent.py create mode 100644 appstoreserverlibrary/models/v2/ExtendReasonCode.py create mode 100644 appstoreserverlibrary/models/v2/ExtendRenewalDateRequest.py create mode 100644 appstoreserverlibrary/models/v2/ExtendRenewalDateResponse.py create mode 100644 appstoreserverlibrary/models/v2/FirstSendAttemptResult.py create mode 100644 appstoreserverlibrary/models/v2/HistoryResponse.py create mode 100644 appstoreserverlibrary/models/v2/InAppOwnershipType.py create mode 100644 appstoreserverlibrary/models/v2/JWSRenewalInfoDecodedPayload.py create mode 100644 appstoreserverlibrary/models/v2/JWSTransactionDecodedPayload.py create mode 100644 appstoreserverlibrary/models/v2/LastTransactionsItem.py create mode 100644 appstoreserverlibrary/models/v2/LifetimeDollarsPurchased.py create mode 100644 appstoreserverlibrary/models/v2/LifetimeDollarsRefunded.py create mode 100644 appstoreserverlibrary/models/v2/MassExtendRenewalDateRequest.py create mode 100644 appstoreserverlibrary/models/v2/MassExtendRenewalDateResponse.py create mode 100644 appstoreserverlibrary/models/v2/MassExtendRenewalDateStatusResponse.py create mode 100644 appstoreserverlibrary/models/v2/NotificationHistoryRequest.py create mode 100644 appstoreserverlibrary/models/v2/NotificationHistoryResponse.py create mode 100644 appstoreserverlibrary/models/v2/NotificationHistoryResponseItem.py create mode 100644 appstoreserverlibrary/models/v2/NotificationTypeV2.py create mode 100644 appstoreserverlibrary/models/v2/OfferType.py create mode 100644 appstoreserverlibrary/models/v2/OrderLookupResponse.py create mode 100644 appstoreserverlibrary/models/v2/OrderLookupStatus.py create mode 100644 appstoreserverlibrary/models/v2/Platform.py create mode 100644 appstoreserverlibrary/models/v2/PlayTime.py create mode 100644 appstoreserverlibrary/models/v2/PriceIncreaseStatus.py create mode 100644 appstoreserverlibrary/models/v2/RefundHistoryResponse.py create mode 100644 appstoreserverlibrary/models/v2/ResponseBodyV2.py create mode 100644 appstoreserverlibrary/models/v2/ResponseBodyV2DecodedPayload.py create mode 100644 appstoreserverlibrary/models/v2/RevocationReason.py create mode 100644 appstoreserverlibrary/models/v2/SendAttemptItem.py create mode 100644 appstoreserverlibrary/models/v2/SendAttemptResult.py create mode 100644 appstoreserverlibrary/models/v2/SendTestNotificationResponse.py create mode 100644 appstoreserverlibrary/models/v2/Status.py create mode 100644 appstoreserverlibrary/models/v2/StatusResponse.py create mode 100644 appstoreserverlibrary/models/v2/SubscriptionGroupIdentifierItem.py create mode 100644 appstoreserverlibrary/models/v2/Subtype.py create mode 100644 appstoreserverlibrary/models/v2/Summary.py create mode 100644 appstoreserverlibrary/models/v2/TransactionHistoryRequest.py create mode 100644 appstoreserverlibrary/models/v2/TransactionInfoResponse.py create mode 100644 appstoreserverlibrary/models/v2/TransactionReason.py create mode 100644 appstoreserverlibrary/models/v2/Type.py create mode 100644 appstoreserverlibrary/models/v2/UserStatus.py create mode 100644 appstoreserverlibrary/models/v2/__init__.py diff --git a/appstoreserverlibrary/models/v2/AccountTenure.py b/appstoreserverlibrary/models/v2/AccountTenure.py new file mode 100644 index 0000000..67f48a2 --- /dev/null +++ b/appstoreserverlibrary/models/v2/AccountTenure.py @@ -0,0 +1,18 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class AccountTenure(Enum): + """ + The age of the customer's account. + + https://developer.apple.com/documentation/appstoreserverapi/accounttenure + """ + UNDECLARED = 0 + ZERO_TO_THREE_DAYS = 1 + THREE_DAYS_TO_TEN_DAYS = 2 + TEN_DAYS_TO_THIRTY_DAYS = 3 + THIRTY_DAYS_TO_NINETY_DAYS = 4 + NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5 + ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6 + GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7 diff --git a/appstoreserverlibrary/models/v2/AppTransaction.py b/appstoreserverlibrary/models/v2/AppTransaction.py new file mode 100644 index 0000000..64f0fdf --- /dev/null +++ b/appstoreserverlibrary/models/v2/AppTransaction.py @@ -0,0 +1,94 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + +from .Base import Model +from .Environment import Environment + + +class AppTransaction(Model): + """ + Information that represents the customer’s purchase of the app, cryptographically signed by the App Store. + + https://developer.apple.com/documentation/storekit/apptransaction + """ + + receiptType: Optional[Environment] = Field(default=None) + """ + The server environment that signs the app transaction. + + https://developer.apple.com/documentation/storekit/apptransaction/3963901-environment + """ + + appAppleId: Optional[int] = Field(default=None) + """ + The unique identifier the App Store uses to identify the app. + + https://developer.apple.com/documentation/storekit/apptransaction/3954436-appid + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier that the app transaction applies to. + + https://developer.apple.com/documentation/storekit/apptransaction/3954439-bundleid + """ + + applicationVersion: Optional[str] = Field(default=None) + """ + The app version that the app transaction applies to. + + https://developer.apple.com/documentation/storekit/apptransaction/3954437-appversion + """ + + versionExternalIdentifier: Optional[int] = Field(default=None) + """ + The version external identifier of the app + + https://developer.apple.com/documentation/storekit/apptransaction/3954438-appversionid + """ + + receiptCreationDate: Optional[int] = Field(default=None) + """ + The date that the App Store signed the JWS app transaction. + + https://developer.apple.com/documentation/storekit/apptransaction/3954449-signeddate + """ + + originalPurchaseDate: Optional[int] = Field(default=None) + """ + The date the user originally purchased the app from the App Store. + + https://developer.apple.com/documentation/storekit/apptransaction/3954448-originalpurchasedate + """ + + originalApplicationVersion: Optional[str] = Field(default=None) + """ + The app version that the user originally purchased from the App Store. + + https://developer.apple.com/documentation/storekit/apptransaction/3954447-originalappversion + """ + + deviceVerification: Optional[str] = Field(default=None) + """ + The Base64 device verification value to use to verify whether the app transaction belongs to the device. + + https://developer.apple.com/documentation/storekit/apptransaction/3954441-deviceverification + """ + + deviceVerificationNonce: Optional[str] = Field(default=None) + """ + The UUID used to compute the device verification value. + + https://developer.apple.com/documentation/storekit/apptransaction/3954442-deviceverificationnonce + """ + + preorderDate: Optional[int] = Field(default=None) + """ + The date the customer placed an order for the app before it's available in the App Store. + + https://developer.apple.com/documentation/storekit/apptransaction/4013175-preorderdate + """ diff --git a/appstoreserverlibrary/models/v2/AutoRenewStatus.py b/appstoreserverlibrary/models/v2/AutoRenewStatus.py new file mode 100644 index 0000000..b91e72a --- /dev/null +++ b/appstoreserverlibrary/models/v2/AutoRenewStatus.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class AutoRenewStatus(Enum): + """ + The renewal status for an auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus + """ + OFF = 0 + ON = 1 diff --git a/appstoreserverlibrary/models/v2/Base.py b/appstoreserverlibrary/models/v2/Base.py new file mode 100644 index 0000000..37144cf --- /dev/null +++ b/appstoreserverlibrary/models/v2/Base.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Model(BaseModel): + ... diff --git a/appstoreserverlibrary/models/v2/CheckTestNotificationResponse.py b/appstoreserverlibrary/models/v2/CheckTestNotificationResponse.py new file mode 100644 index 0000000..f67fce9 --- /dev/null +++ b/appstoreserverlibrary/models/v2/CheckTestNotificationResponse.py @@ -0,0 +1,26 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import List, Optional + +from pydantic import Field + + +class CheckTestNotificationResponse: + """ + A response that contains the contents of the test notification sent by the App Store server and the result from your server. + + https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse + """ + + signedPayload: Optional[str] = Field(default=None) + """ + A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. + + https://developer.apple.com/documentation/appstoreservernotifications/signedpayload + """ + + sendAttempts: Optional[List[SendAttemptItem]] = Field(default=None) + """ + An array of information the App Store server records for its attempts to send the TEST notification to your server. The array may contain a maximum of six sendAttemptItem objects. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem + """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/v2/ConsumptionRequest.py b/appstoreserverlibrary/models/v2/ConsumptionRequest.py new file mode 100644 index 0000000..489c3c7 --- /dev/null +++ b/appstoreserverlibrary/models/v2/ConsumptionRequest.py @@ -0,0 +1,102 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + +from .AccountTenure import AccountTenure +from .Base import Model + +from .ConsumptionStatus import ConsumptionStatus +from .DeliveryStatus import DeliveryStatus +from .LifetimeDollarsPurchased import LifetimeDollarsPurchased +from .LifetimeDollarsRefunded import LifetimeDollarsRefunded +from .Platform import Platform +from .PlayTime import PlayTime +from .UserStatus import UserStatus + + +class ConsumptionRequest(Model): + """ + The request body containing consumption information. + + https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest + """ + + customerConsented: Optional[bool] = Field(default=None) + """ + A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. + + https://developer.apple.com/documentation/appstoreserverapi/customerconsented + """ + + consumptionStatus: Optional[ConsumptionStatus] = Field(default=None) + """ + A value that indicates the extent to which the customer consumed the in-app purchase. + + https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus + """ + + platform: Optional[Platform] = Field(default=None) + """ + A value that indicates the platform on which the customer consumed the in-app purchase. + + https://developer.apple.com/documentation/appstoreserverapi/platform + """ + + sampleContentProvided: Optional[bool] = Field(default=None) + """ + A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. + + https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided + """ + + deliveryStatus: Optional[DeliveryStatus] = Field(default=None) + """ + A value that indicates whether the app successfully delivered an in-app purchase that works properly. + + https://developer.apple.com/documentation/appstoreserverapi/deliverystatus + """ + + appAccountToken: Optional[str] = Field(default=None) + """ + The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. + + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken + """ + + accountTenure: Optional[AccountTenure] = Field(default=None) + """ + The age of the customer's account. + + https://developer.apple.com/documentation/appstoreserverapi/accounttenure + """ + + playTime: Optional[PlayTime] = Field(default=None) + """ + A value that indicates the amount of time that the customer used the app. + + https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest + """ + + lifetimeDollarsRefunded: Optional[LifetimeDollarsRefunded] = Field(default=None) + """ + A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. + + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded + """ + + lifetimeDollarsPurchased: Optional[LifetimeDollarsPurchased] = Field(default=None) + """ + A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. + + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased + """ + + userStatus: Optional[UserStatus] = Field(default=None) + """ + The status of the customer's account. + + https://developer.apple.com/documentation/appstoreserverapi/userstatus + """ diff --git a/appstoreserverlibrary/models/v2/ConsumptionStatus.py b/appstoreserverlibrary/models/v2/ConsumptionStatus.py new file mode 100644 index 0000000..a746919 --- /dev/null +++ b/appstoreserverlibrary/models/v2/ConsumptionStatus.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class ConsumptionStatus(Enum): + """ + A value that indicates the extent to which the customer consumed the in-app purchase. + + https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus + """ + UNDECLARED = 0 + NOT_CONSUMED = 1 + PARTIALLY_CONSUMED = 2 + FULLY_CONSUMED = 3 diff --git a/appstoreserverlibrary/models/v2/Data.py b/appstoreserverlibrary/models/v2/Data.py new file mode 100644 index 0000000..ae1a4e5 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Data.py @@ -0,0 +1,67 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + +from .Base import Model +from .Environment import Environment +from .Status import Status + + +class Data(Model): + """ + The app metadata and the signed renewal and transaction information. + + https://developer.apple.com/documentation/appstoreservernotifications/data + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment that the notification applies to, either sandbox or production. + + https://developer.apple.com/documentation/appstoreservernotifications/environment + """ + + appAppleId: Optional[int] = Field(default=None) + """ + The unique identifier of an app in the App Store. + + https://developer.apple.com/documentation/appstoreservernotifications/appappleid + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier of an app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid + """ + + bundleVersion: Optional[str] = Field(default=None) + """ + The version of the build that identifies an iteration of the bundle. + + https://developer.apple.com/documentation/appstoreservernotifications/bundleversion + """ + + signedTransactionInfo: Optional[str] = Field(default=None) + """ + Transaction information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + """ + + signedRenewalInfo: Optional[str] = Field(default=None) + """ + Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo + """ + + status: Optional[Status] = Field(default=None) + """ + The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. + + https://developer.apple.com/documentation/appstoreservernotifications/status + """ diff --git a/appstoreserverlibrary/models/v2/DeliveryStatus.py b/appstoreserverlibrary/models/v2/DeliveryStatus.py new file mode 100644 index 0000000..862d8cc --- /dev/null +++ b/appstoreserverlibrary/models/v2/DeliveryStatus.py @@ -0,0 +1,17 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class DeliveryStatus(Enum): + """ + A value that indicates whether the app successfully delivered an in-app purchase that works properly. + + https://developer.apple.com/documentation/appstoreserverapi/deliverystatus + """ + DELIVERED_AND_WORKING_PROPERLY = 0 + DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1 + DELIVERED_WRONG_ITEM = 2 + DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3 + DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4 + DID_NOT_DELIVER_FOR_OTHER_REASON = 5 diff --git a/appstoreserverlibrary/models/v2/Environment.py b/appstoreserverlibrary/models/v2/Environment.py new file mode 100644 index 0000000..ab1abf7 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Environment.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class Environment(Enum): + """ + The server environment, either sandbox or production. + + https://developer.apple.com/documentation/appstoreserverapi/environment + """ + SANDBOX = "Sandbox" + PRODUCTION = "Production" diff --git a/appstoreserverlibrary/models/v2/ExpirationIntent.py b/appstoreserverlibrary/models/v2/ExpirationIntent.py new file mode 100644 index 0000000..fa2c929 --- /dev/null +++ b/appstoreserverlibrary/models/v2/ExpirationIntent.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class ExpirationIntent(Enum): + """ + The reason an auto-renewable subscription expired. + + https://developer.apple.com/documentation/appstoreserverapi/expirationintent + """ + CUSTOMER_CANCELLED = 1 + BILLING_ERROR = 2 + CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3 + PRODUCT_NOT_AVAILABLE = 4 + OTHER = 5 diff --git a/appstoreserverlibrary/models/v2/ExtendReasonCode.py b/appstoreserverlibrary/models/v2/ExtendReasonCode.py new file mode 100644 index 0000000..614379d --- /dev/null +++ b/appstoreserverlibrary/models/v2/ExtendReasonCode.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class ExtendReasonCode(Enum): + """ + The code that represents the reason for the subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode + """ + UNDECLARED = 0 + CUSTOMER_SATISFACTION = 1 + OTHER = 2 + SERVICE_ISSUE_OR_OUTAGE = 3 diff --git a/appstoreserverlibrary/models/v2/ExtendRenewalDateRequest.py b/appstoreserverlibrary/models/v2/ExtendRenewalDateRequest.py new file mode 100644 index 0000000..fa746ba --- /dev/null +++ b/appstoreserverlibrary/models/v2/ExtendRenewalDateRequest.py @@ -0,0 +1,37 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from .Base import Model +from .ExtendReasonCode import ExtendReasonCode + + +class ExtendRenewalDateRequest(Model): + """ + The request body that contains subscription-renewal-extension data for an individual subscription. + + https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest + """ + + extendByDays: Optional[int] = Field(default=None) + """ + The number of days to extend the subscription renewal date. + + https://developer.apple.com/documentation/appstoreserverapi/extendbydays + maximum: 90 + """ + + extendReasonCode: Optional[ExtendReasonCode] = Field(default=None) + """ + The reason code for the subscription date extension + + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode + """ + + requestIdentifier: Optional[str] = Field(default=None) + """ + A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ diff --git a/appstoreserverlibrary/models/v2/ExtendRenewalDateResponse.py b/appstoreserverlibrary/models/v2/ExtendRenewalDateResponse.py new file mode 100644 index 0000000..4904b87 --- /dev/null +++ b/appstoreserverlibrary/models/v2/ExtendRenewalDateResponse.py @@ -0,0 +1,42 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + + +class ExtendRenewalDateResponse: + """ + A response that indicates whether an individual renewal-date extension succeeded, and related details. + + https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse + """ + + originalTransactionId: Optional[str] = Field(default=None) + """ + The original transaction identifier of a purchase. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + """ + + webOrderLineItemId: Optional[str] = Field(default=None) + """ + The unique identifier of subscription-purchase events across devices, including renewals. + + https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid + """ + + success: Optional[bool] = Field(default=None) + """ + A Boolean value that indicates whether the subscription-renewal-date extension succeeded. + + https://developer.apple.com/documentation/appstoreserverapi/success + """ + + effectiveDate: Optional[int] = Field(default=None) + """ + The new subscription expiration date for a subscription-renewal extension. + + https://developer.apple.com/documentation/appstoreserverapi/effectivedate + """ diff --git a/appstoreserverlibrary/models/v2/FirstSendAttemptResult.py b/appstoreserverlibrary/models/v2/FirstSendAttemptResult.py new file mode 100644 index 0000000..cf67fc2 --- /dev/null +++ b/appstoreserverlibrary/models/v2/FirstSendAttemptResult.py @@ -0,0 +1,22 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum, unique + +@unique +class FirstSendAttemptResult(Enum): + """ + An error or result that the App Store server receives when attempting to send an App Store server notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/firstsendattemptresult + """ + SUCCESS = "SUCCESS" + TIMED_OUT = "TIMED_OUT" + TLS_ISSUE = "TLS_ISSUE" + CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" + NO_RESPONSE = "NO_RESPONSE" + SOCKET_ISSUE = "SOCKET_ISSUE" + UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" + INVALID_RESPONSE = "INVALID_RESPONSE" + PREMATURE_CLOSE = "PREMATURE_CLOSE" + UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" + OTHER = "OTHER" diff --git a/appstoreserverlibrary/models/v2/HistoryResponse.py b/appstoreserverlibrary/models/v2/HistoryResponse.py new file mode 100644 index 0000000..9613884 --- /dev/null +++ b/appstoreserverlibrary/models/v2/HistoryResponse.py @@ -0,0 +1,58 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from attr import define +from typing import List, Optional +import attr +from pydantic import Field + +from .Base import Model +from .Environment import Environment + +class HistoryResponse(Model): + """ + A response that contains the customer's transaction history for an app. + + https://developer.apple.com/documentation/appstoreserverapi/historyresponse + """ + + revision: Optional[str] = Field(default=None) + """ + A token you use in a query to request the next set of transactions for the customer. + + https://developer.apple.com/documentation/appstoreserverapi/revision + """ + + hasMore: Optional[bool] = Field(default=None) + """ + A Boolean value indicating whether the App Store has more transaction data. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier of an app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid + """ + + appAppleId: Optional[int] = Field(default=None) + """ + The unique identifier of an app in the App Store. + + https://developer.apple.com/documentation/appstoreservernotifications/appappleid + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment in which you're making the request, whether sandbox or production. + + https://developer.apple.com/documentation/appstoreserverapi/environment + """ + + signedTransactions: Optional[List[str]] = Field(default=None) + """ + An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/v2/InAppOwnershipType.py b/appstoreserverlibrary/models/v2/InAppOwnershipType.py new file mode 100644 index 0000000..a44fcbc --- /dev/null +++ b/appstoreserverlibrary/models/v2/InAppOwnershipType.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class InAppOwnershipType(Enum): + """ + The relationship of the user with the family-shared purchase to which they have access. + + https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype + """ + FAMILY_SHARED = "FAMILY_SHARED" + PURCHASED = "PURCHASED" diff --git a/appstoreserverlibrary/models/v2/JWSRenewalInfoDecodedPayload.py b/appstoreserverlibrary/models/v2/JWSRenewalInfoDecodedPayload.py new file mode 100644 index 0000000..e136f1c --- /dev/null +++ b/appstoreserverlibrary/models/v2/JWSRenewalInfoDecodedPayload.py @@ -0,0 +1,120 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + +from .AutoRenewStatus import AutoRenewStatus +from .Base import Model +from .Environment import Environment + +from .ExpirationIntent import ExpirationIntent +from .OfferType import OfferType +from .PriceIncreaseStatus import PriceIncreaseStatus + + +class JWSRenewalInfoDecodedPayload(Model): + """ + A decoded payload containing subscription renewal information for an auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload + """ + + expirationIntent: Optional[ExpirationIntent] = Field(default=None) + """ + The reason the subscription expired. + + https://developer.apple.com/documentation/appstoreserverapi/expirationintent + """ + + originalTransactionId: Optional[str] = Field(default=None) + """ + The original transaction identifier of a purchase. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + """ + + autoRenewProductId: Optional[str] = Field(default=None) + """ + The product identifier of the product that will renew at the next billing period. + + https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid + """ + + productId: Optional[str] = Field(default=None) + """ + The unique identifier for the product, that you create in App Store Connect. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ + + autoRenewStatus: Optional[AutoRenewStatus] = Field(default=None) + """ + The renewal status of the auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus + """ + + isInBillingRetryPeriod: Optional[bool] = Field(default=None) + """ + A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription. + + https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod + """ + + priceIncreaseStatus: Optional[PriceIncreaseStatus] = Field(default=None) + """ + The status that indicates whether the auto-renewable subscription is subject to a price increase. + + https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus + """ + + gracePeriodExpiresDate: Optional[int] = Field(default=None) + """ + The time when the billing grace period for subscription renewals expires. + + https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate + """ + + offerType: Optional[OfferType] = Field(default=None) + """ + The type of the subscription offer. + + https://developer.apple.com/documentation/appstoreserverapi/offertype + """ + + offerIdentifier: Optional[str] = Field(default=None) + """ + The identifier that contains the promo code or the promotional offer identifier. + + https://developer.apple.com/documentation/appstoreserverapi/offeridentifier + """ + + signedDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. + + https://developer.apple.com/documentation/appstoreserverapi/signeddate + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment, either sandbox or production. + + https://developer.apple.com/documentation/appstoreserverapi/environment + """ + + recentSubscriptionStartDate: Optional[int] = Field(default=None) + """ + The earliest start date of a subscription in a series of auto-renewable subscription purchases that ignores all lapses of paid service shorter than 60 days. + + https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate + """ + + renewalDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires. + + https://developer.apple.com/documentation/appstoreserverapi/renewaldate + """ diff --git a/appstoreserverlibrary/models/v2/JWSTransactionDecodedPayload.py b/appstoreserverlibrary/models/v2/JWSTransactionDecodedPayload.py new file mode 100644 index 0000000..fd48c14 --- /dev/null +++ b/appstoreserverlibrary/models/v2/JWSTransactionDecodedPayload.py @@ -0,0 +1,181 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from .Base import Model +from .Environment import Environment +from .InAppOwnershipType import InAppOwnershipType +from .OfferType import OfferType +from .RevocationReason import RevocationReason +from .TransactionReason import TransactionReason +from .Type import Type + + +class JWSTransactionDecodedPayload(Model): + """ + A decoded payload containing transaction information. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload + """ + + originalTransactionId: Optional[str] = Field(default=None) + """ + The original transaction identifier of a purchase. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + """ + + transactionId: Optional[str] = Field(default=None) + """ + The unique identifier for a transaction such as an in-app purchase, restored in-app purchase, or subscription renewal. + + https://developer.apple.com/documentation/appstoreserverapi/transactionid + """ + + webOrderLineItemId: Optional[str] = Field(default=None) + """ + The unique identifier of subscription-purchase events across devices, including renewals. + + https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier of an app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid + """ + + productId: Optional[str] = Field(default=None) + """ + The unique identifier for the product, that you create in App Store Connect. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ + + subscriptionGroupIdentifier: Optional[str] = Field(default=None) + """ + The identifier of the subscription group that the subscription belongs to. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier + """ + + purchaseDate: Optional[int] = Field(default=None) + """ + The time that the App Store charged the user's account for an in-app purchase, a restored in-app purchase, a subscription, or a subscription renewal after a lapse. + + https://developer.apple.com/documentation/appstoreserverapi/purchasedate + """ + + originalPurchaseDate: Optional[int] = Field(default=None) + """ + The purchase date of the transaction associated with the original transaction identifier. + + https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate + """ + + expiresDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, an auto-renewable subscription expires or renews. + + https://developer.apple.com/documentation/appstoreserverapi/expiresdate + """ + + quantity: Optional[int] = Field(default=None) + """ + The number of consumable products purchased. + + https://developer.apple.com/documentation/appstoreserverapi/quantity + """ + + type: Optional[Type] = Field(default=None) + """ + The type of the in-app purchase. + + https://developer.apple.com/documentation/appstoreserverapi/type + """ + + appAccountToken: Optional[str] = Field(default=None) + """ + The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. + + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken + """ + + inAppOwnershipType: Optional[InAppOwnershipType] = Field(default=None) + """ + A string that describes whether the transaction was purchased by the user, or is available to them through Family Sharing. + + https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype + """ + + signedDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. + + https://developer.apple.com/documentation/appstoreserverapi/signeddate + """ + + revocationReason: Optional[RevocationReason] = Field(default=None) + """ + The reason that the App Store refunded the transaction or revoked it from family sharing. + + https://developer.apple.com/documentation/appstoreserverapi/revocationreason + """ + + revocationDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that Apple Support refunded a transaction. + + https://developer.apple.com/documentation/appstoreserverapi/revocationdate + """ + + isUpgraded: Optional[bool] = Field(default=None) + """ + The Boolean value that indicates whether the user upgraded to another subscription. + + https://developer.apple.com/documentation/appstoreserverapi/isupgraded + """ + + offerType: Optional[OfferType] = Field(default=None) + """ + A value that represents the promotional offer type. + + https://developer.apple.com/documentation/appstoreserverapi/offertype + """ + + offerIdentifier: Optional[str] = Field(default=None) + """ + The identifier that contains the promo code or the promotional offer identifier. + + https://developer.apple.com/documentation/appstoreserverapi/offeridentifier + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment, either sandbox or production. + + https://developer.apple.com/documentation/appstoreserverapi/environment + """ + + storefront: Optional[str] = Field(default=None) + """ + The three-letter code that represents the country or region associated with the App Store storefront for the purchase. + + https://developer.apple.com/documentation/appstoreserverapi/storefront + """ + + storefrontId: Optional[str] = Field(default=None) + """ + An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase. + + https://developer.apple.com/documentation/appstoreserverapi/storefrontid + """ + + transactionReason: Optional[TransactionReason] = Field(default=None) + """ + The reason for the purchase transaction, which indicates whether it's a customer's purchase or a renewal for an auto-renewable subscription that the system initates. + + https://developer.apple.com/documentation/appstoreserverapi/transactionreason + """ diff --git a/appstoreserverlibrary/models/v2/LastTransactionsItem.py b/appstoreserverlibrary/models/v2/LastTransactionsItem.py new file mode 100644 index 0000000..b654684 --- /dev/null +++ b/appstoreserverlibrary/models/v2/LastTransactionsItem.py @@ -0,0 +1,43 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from appstoreserverlibrary.models.Status import Status +from appstoreserverlibrary.models.v2.Base import Model + + +class LastTransactionsItem(Model): + """ + The most recent App Store-signed transaction information and App Store-signed renewal information for an auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem + """ + + status: Optional[Status] = Field(default=None) + """ + The status of the auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/status + """ + + originalTransactionId: Optional[str] = Field(default=None) + """ + The original transaction identifier of a purchase. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + """ + + signedTransactionInfo: Optional[str] = Field(default=None) + """ + Transaction information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + """ + + signedRenewalInfo: Optional[str] = Field(default=None) + """ + Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo + """ diff --git a/appstoreserverlibrary/models/v2/LifetimeDollarsPurchased.py b/appstoreserverlibrary/models/v2/LifetimeDollarsPurchased.py new file mode 100644 index 0000000..00a8d4d --- /dev/null +++ b/appstoreserverlibrary/models/v2/LifetimeDollarsPurchased.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class LifetimeDollarsPurchased(Enum): + """ + A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. + + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased + """ + UNDECLARED = 0 + ZERO_DOLLARS = 1 + ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 + FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 + ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 + FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 + ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 + TWO_THOUSAND_DOLLARS_OR_GREATER = 7 diff --git a/appstoreserverlibrary/models/v2/LifetimeDollarsRefunded.py b/appstoreserverlibrary/models/v2/LifetimeDollarsRefunded.py new file mode 100644 index 0000000..94528e8 --- /dev/null +++ b/appstoreserverlibrary/models/v2/LifetimeDollarsRefunded.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class LifetimeDollarsRefunded(Enum): + """ + A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms. + + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded + """ + UNDECLARED = 0 + ZERO_DOLLARS = 1 + ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 + FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 + ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 + FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 + ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 + TWO_THOUSAND_DOLLARS_OR_GREATER = 7 diff --git a/appstoreserverlibrary/models/v2/MassExtendRenewalDateRequest.py b/appstoreserverlibrary/models/v2/MassExtendRenewalDateRequest.py new file mode 100644 index 0000000..c8dc17e --- /dev/null +++ b/appstoreserverlibrary/models/v2/MassExtendRenewalDateRequest.py @@ -0,0 +1,53 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from attr import define +from typing import List, Optional +import attr +from pydantic import Field + +from .Base import Model +from .ExtendReasonCode import ExtendReasonCode + + +class MassExtendRenewalDateRequest(Model): + """ + The request body that contains subscription-renewal-extension data to apply for all eligible active subscribers. + + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldaterequest + """ + + extendByDays: Optional[int] = Field(default=None) + """ + The number of days to extend the subscription renewal date. + + https://developer.apple.com/documentation/appstoreserverapi/extendbydays + maximum: 90 + """ + + extendReasonCode: Optional[ExtendReasonCode] = Field(default=None) + """ + The reason code for the subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode + """ + + requestIdentifier: Optional[str] = Field(default=None) + """ + A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ + + storefrontCountryCodes: Optional[List[str]] = Field(default=None) + """ + A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes + """ + + productId: Optional[str] = Field(default=None) + """ + The unique identifier for the product, that you create in App Store Connect. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ diff --git a/appstoreserverlibrary/models/v2/MassExtendRenewalDateResponse.py b/appstoreserverlibrary/models/v2/MassExtendRenewalDateResponse.py new file mode 100644 index 0000000..e915601 --- /dev/null +++ b/appstoreserverlibrary/models/v2/MassExtendRenewalDateResponse.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class MassExtendRenewalDateResponse(Model): + """ + A response that indicates the server successfully received the subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldateresponse + """ + + requestIdentifier: Optional[str] = Field(default=None) + """ + A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ diff --git a/appstoreserverlibrary/models/v2/MassExtendRenewalDateStatusResponse.py b/appstoreserverlibrary/models/v2/MassExtendRenewalDateStatusResponse.py new file mode 100644 index 0000000..77672c5 --- /dev/null +++ b/appstoreserverlibrary/models/v2/MassExtendRenewalDateStatusResponse.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class MassExtendRenewalDateStatusResponse(Model): + """ + A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. + + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldatestatusresponse + """ + + requestIdentifier: Optional[str] = Field(default=None) + """ + A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ + + complete: Optional[bool] = Field(default=None) + """ + A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers. + + https://developer.apple.com/documentation/appstoreserverapi/complete + """ + + completeDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers. + + https://developer.apple.com/documentation/appstoreserverapi/completedate + """ + + succeededCount: Optional[int] = Field(default=None) + """ + The count of subscriptions that successfully receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/succeededcount + """ + + failedCount: Optional[int] = Field(default=None) + """ + The count of subscriptions that fail to receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/failedcount + """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/v2/NotificationHistoryRequest.py b/appstoreserverlibrary/models/v2/NotificationHistoryRequest.py new file mode 100644 index 0000000..cec5649 --- /dev/null +++ b/appstoreserverlibrary/models/v2/NotificationHistoryRequest.py @@ -0,0 +1,61 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from pydantic import Field + +from .Base import Model +from .NotificationTypeV2 import NotificationTypeV2 +from .Subtype import Subtype + + +class NotificationHistoryRequest(Model): + """ + The request body for notification history. + + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest + """ + + startDate: Optional[int] = Field(default=None) + """ + The start date of the timespan for the requested App Store Server Notification history records. The startDate needs to precede the endDate. Choose a startDate that's within the past 180 days from the current date. + + https://developer.apple.com/documentation/appstoreserverapi/startdate + """ + + endDate: Optional[int] = Field(default=None) + """ + The end date of the timespan for the requested App Store Server Notification history records. Choose an endDate that's later than the startDate. If you choose an endDate in the future, the endpoint automatically uses the current date as the endDate. + + https://developer.apple.com/documentation/appstoreserverapi/enddate + """ + + notificationType: Optional[NotificationTypeV2] = Field(default=None) + """ + A notification type. Provide this field to limit the notification history records to those with this one notification type. For a list of notifications types, see notificationType. + Include either the transactionId or the notificationType in your query, but not both. + + https://developer.apple.com/documentation/appstoreserverapi/notificationtype + """ + + notificationSubtype: Optional[Subtype] = Field(default=None) + """ + A notification subtype. Provide this field to limit the notification history records to those with this one notification subtype. For a list of subtypes, see subtype. If you specify a notificationSubtype, you need to also specify its related notificationType. + + https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype + """ + + transactionId: Optional[str] = Field(default=None) + """ + The transaction identifier, which may be an original transaction identifier, of any transaction belonging to the customer. Provide this field to limit the notification history request to this one customer. + Include either the transactionId or the notificationType in your query, but not both. + + https://developer.apple.com/documentation/appstoreserverapi/transactionid + """ + + onlyFailures: Optional[bool] = Field(default=None) + """ + A Boolean value you set to true to request only the notifications that haven’t reached your server successfully. The response also includes notifications that the App Store server is currently retrying to send to your server. + + https://developer.apple.com/documentation/appstoreserverapi/onlyfailures + """ diff --git a/appstoreserverlibrary/models/v2/NotificationHistoryResponse.py b/appstoreserverlibrary/models/v2/NotificationHistoryResponse.py new file mode 100644 index 0000000..02e2310 --- /dev/null +++ b/appstoreserverlibrary/models/v2/NotificationHistoryResponse.py @@ -0,0 +1,37 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + +from attr import define +import attr +from pydantic import Field + +from .Base import Model +from .NotificationHistoryResponseItem import NotificationHistoryResponseItem + + +class NotificationHistoryResponse(Model): + """ + A response that contains the App Store Server Notifications history for your app. + + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse + """ + + paginationToken: Optional[str] = Field(default=None) + """ + A pagination token that you return to the endpoint on a subsequent call to receive the next set of results. + + https://developer.apple.com/documentation/appstoreserverapi/paginationtoken + """ + + hasMore: Optional[bool] = Field(default=None) + """ + A Boolean value indicating whether the App Store has more transaction data. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore + """ + + notificationHistory: Optional[List[NotificationHistoryResponseItem]] = Field(default=None) + """ + An array of App Store server notification history records. + + """ diff --git a/appstoreserverlibrary/models/v2/NotificationHistoryResponseItem.py b/appstoreserverlibrary/models/v2/NotificationHistoryResponseItem.py new file mode 100644 index 0000000..47cbb88 --- /dev/null +++ b/appstoreserverlibrary/models/v2/NotificationHistoryResponseItem.py @@ -0,0 +1,31 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + +from attr import define +import attr +from pydantic import Field + +from .Base import Model +from .SendAttemptItem import SendAttemptItem + + +class NotificationHistoryResponseItem(Model): + """ + The App Store server notification history record, including the signed notification payload and the result of the server's first send attempt. + + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem + """ + + signedPayload: Optional[str] = Field(default=None) + """ + A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. + + https://developer.apple.com/documentation/appstoreservernotifications/signedpayload + """ + + sendAttempts: Optional[List[SendAttemptItem]] = Field(default=None) + """ + An array of information the App Store server records for its attempts to send a notification to your server. The maximum number of entries in the array is six. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem + """ diff --git a/appstoreserverlibrary/models/v2/NotificationTypeV2.py b/appstoreserverlibrary/models/v2/NotificationTypeV2.py new file mode 100644 index 0000000..c250f81 --- /dev/null +++ b/appstoreserverlibrary/models/v2/NotificationTypeV2.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class NotificationTypeV2(Enum): + """ + A notification type value that App Store Server Notifications V2 uses. + + https://developer.apple.com/documentation/appstoreserverapi/notificationtype + """ + SUBSCRIBED = "SUBSCRIBED" + DID_CHANGE_RENEWAL_PREF = "DID_CHANGE_RENEWAL_PREF" + DID_CHANGE_RENEWAL_STATUS = "DID_CHANGE_RENEWAL_STATUS" + OFFER_REDEEMED = "OFFER_REDEEMED" + DID_RENEW = "DID_RENEW" + EXPIRED = "EXPIRED" + DID_FAIL_TO_RENEW = "DID_FAIL_TO_RENEW" + GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED" + PRICE_INCREASE = "PRICE_INCREASE" + REFUND = "REFUND" + REFUND_DECLINED = "REFUND_DECLINED" + CONSUMPTION_REQUEST = "CONSUMPTION_REQUEST" + RENEWAL_EXTENDED = "RENEWAL_EXTENDED" + REVOKE = "REVOKE" + TEST = "TEST" + RENEWAL_EXTENSION = "RENEWAL_EXTENSION" + REFUND_REVERSED = "REFUND_REVERSED" diff --git a/appstoreserverlibrary/models/v2/OfferType.py b/appstoreserverlibrary/models/v2/OfferType.py new file mode 100644 index 0000000..0a38c23 --- /dev/null +++ b/appstoreserverlibrary/models/v2/OfferType.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class OfferType(Enum): + """ + The type of subscription offer. + + https://developer.apple.com/documentation/appstoreserverapi/offertype + """ + INTRODUCTORY_OFFER = 1 + PROMOTIONAL_OFFER = 2 + SUBSCRIPTION_OFFER_CODE = 3 diff --git a/appstoreserverlibrary/models/v2/OrderLookupResponse.py b/appstoreserverlibrary/models/v2/OrderLookupResponse.py new file mode 100644 index 0000000..e6456d9 --- /dev/null +++ b/appstoreserverlibrary/models/v2/OrderLookupResponse.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from typing import List, Optional + +from pydantic import Field + +from .Base import Model +from .OrderLookupStatus import OrderLookupStatus + + +class OrderLookupResponse(Model): + """ + A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. + + https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse + """ + + status: Optional[OrderLookupStatus] = Field(default=None) + """ + The status that indicates whether the order ID is valid. + + https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus + """ + + signedTransactions: Optional[List[str]] = Field(default=None) + """ + An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. + """ diff --git a/appstoreserverlibrary/models/v2/OrderLookupStatus.py b/appstoreserverlibrary/models/v2/OrderLookupStatus.py new file mode 100644 index 0000000..7c5b195 --- /dev/null +++ b/appstoreserverlibrary/models/v2/OrderLookupStatus.py @@ -0,0 +1,12 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + +class OrderLookupStatus(Enum): + """ + A value that indicates whether the order ID in the request is valid for your app. + + https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus + """ + VALID = 0 + INVALID = 1 diff --git a/appstoreserverlibrary/models/v2/Platform.py b/appstoreserverlibrary/models/v2/Platform.py new file mode 100644 index 0000000..7bdbf57 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Platform.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class Platform(Enum): + """ + The platform on which the customer consumed the in-app purchase. + + https://developer.apple.com/documentation/appstoreserverapi/platform + """ + UNDECLARED = 0 + APPLE = 1 + NON_APPLE = 2 diff --git a/appstoreserverlibrary/models/v2/PlayTime.py b/appstoreserverlibrary/models/v2/PlayTime.py new file mode 100644 index 0000000..6106ea4 --- /dev/null +++ b/appstoreserverlibrary/models/v2/PlayTime.py @@ -0,0 +1,19 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class PlayTime(Enum): + """ + A value that indicates the amount of time that the customer used the app. + + https://developer.apple.com/documentation/appstoreserverapi/playtime + """ + UNDECLARED = 0 + ZERO_TO_FIVE_MINUTES = 1 + FIVE_TO_SIXTY_MINUTES = 2 + ONE_TO_SIX_HOURS = 3 + SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4 + ONE_DAY_TO_FOUR_DAYS = 5 + FOUR_DAYS_TO_SIXTEEN_DAYS = 6 + OVER_SIXTEEN_DAYS = 7 diff --git a/appstoreserverlibrary/models/v2/PriceIncreaseStatus.py b/appstoreserverlibrary/models/v2/PriceIncreaseStatus.py new file mode 100644 index 0000000..151da39 --- /dev/null +++ b/appstoreserverlibrary/models/v2/PriceIncreaseStatus.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class PriceIncreaseStatus(Enum): + """ + The status that indicates whether an auto-renewable subscription is subject to a price increase. + + https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus + """ + CUSTOMER_HAS_NOT_RESPONDED = 0 + CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1 diff --git a/appstoreserverlibrary/models/v2/RefundHistoryResponse.py b/appstoreserverlibrary/models/v2/RefundHistoryResponse.py new file mode 100644 index 0000000..b9dfa98 --- /dev/null +++ b/appstoreserverlibrary/models/v2/RefundHistoryResponse.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from attr import define +from typing import List, Optional +import attr +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class RefundHistoryResponse(Model): + """ + A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. + + https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse + """ + + signedTransactions: Optional[List[str]] = Field(default=None) + """ + A list of up to 20 JWS transactions, or an empty array if the customer hasn't received any refunds in your app. The transactions are sorted in ascending order by revocationDate. + """ + + revision: Optional[str] = Field(default=None) + """ + A token you use in a query to request the next set of transactions for the customer. + + https://developer.apple.com/documentation/appstoreserverapi/revision + """ + + hasMore: Optional[bool] = Field(default=None) + """ + A Boolean value indicating whether the App Store has more transaction data. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore + """ diff --git a/appstoreserverlibrary/models/v2/ResponseBodyV2.py b/appstoreserverlibrary/models/v2/ResponseBodyV2.py new file mode 100644 index 0000000..2db888c --- /dev/null +++ b/appstoreserverlibrary/models/v2/ResponseBodyV2.py @@ -0,0 +1,23 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class ResponseBodyV2(Model): + """ + The response body the App Store sends in a version 2 server notification. + + https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 + """ + + signedPayload: Optional[str] = Field(default=None) + """ + A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. + + https://developer.apple.com/documentation/appstoreservernotifications/signedpayload + """ diff --git a/appstoreserverlibrary/models/v2/ResponseBodyV2DecodedPayload.py b/appstoreserverlibrary/models/v2/ResponseBodyV2DecodedPayload.py new file mode 100644 index 0000000..2f88ee7 --- /dev/null +++ b/appstoreserverlibrary/models/v2/ResponseBodyV2DecodedPayload.py @@ -0,0 +1,69 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from .Base import Model +from .Data import Data +from .NotificationTypeV2 import NotificationTypeV2 +from .Subtype import Subtype +from .Summary import Summary + + +class ResponseBodyV2DecodedPayload(Model): + """ + A decoded payload containing the version 2 notification data. + + https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload + """ + + notificationType: Optional[NotificationTypeV2] = Field(default=None) + """ + The in-app purchase event for which the App Store sends this version 2 notification. + + https://developer.apple.com/documentation/appstoreservernotifications/notificationtype + """ + + subtype: Optional[Subtype] = Field(default=None) + """ + Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. + + https://developer.apple.com/documentation/appstoreservernotifications/subtype + """ + + notificationUUID: Optional[str] = Field(default=None) + """ + A unique identifier for the notification. + + https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid + """ + + data: Optional[Data] = Field(default=None) + """ + The object that contains the app metadata and signed renewal and transaction information. + The data and summary fields are mutually exclusive. The payload contains one of the fields, but not both. + + https://developer.apple.com/documentation/appstoreservernotifications/data + """ + + version: Optional[str] = Field(default=None) + """ + A string that indicates the notification's App Store Server Notifications version number. + + https://developer.apple.com/documentation/appstoreservernotifications/version + """ + + signedDate: Optional[int] = Field(default=None) + """ + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. + + https://developer.apple.com/documentation/appstoreserverapi/signeddate + """ + + summary: Optional[Summary] = Field(default=None) + """ + The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers. + The data and summary fields are mutually exclusive. The payload contains one of the fields, but not both. + + https://developer.apple.com/documentation/appstoreservernotifications/summary + """ diff --git a/appstoreserverlibrary/models/v2/RevocationReason.py b/appstoreserverlibrary/models/v2/RevocationReason.py new file mode 100644 index 0000000..3a47887 --- /dev/null +++ b/appstoreserverlibrary/models/v2/RevocationReason.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class RevocationReason(Enum): + """ + The reason for a refunded transaction. + + https://developer.apple.com/documentation/appstoreserverapi/revocationreason + """ + REFUNDED_DUE_TO_ISSUE = 1 + REFUNDED_FOR_OTHER_REASON = 0 diff --git a/appstoreserverlibrary/models/v2/SendAttemptItem.py b/appstoreserverlibrary/models/v2/SendAttemptItem.py new file mode 100644 index 0000000..edd24ee --- /dev/null +++ b/appstoreserverlibrary/models/v2/SendAttemptItem.py @@ -0,0 +1,29 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from .Base import Model +from .SendAttemptResult import SendAttemptResult + + +class SendAttemptItem(Model): + """ + The success or error information and the date the App Store server records when it attempts to send a server notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem + """ + + attemptDate: Optional[int] = Field(default=None) + """ + The date the App Store server attempts to send a notification. + + https://developer.apple.com/documentation/appstoreserverapi/attemptdate + """ + + sendAttemptResult: Optional[SendAttemptResult] = Field(default=None) + """ + The success or error information the App Store server records when it attempts to send an App Store server notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult + """ diff --git a/appstoreserverlibrary/models/v2/SendAttemptResult.py b/appstoreserverlibrary/models/v2/SendAttemptResult.py new file mode 100644 index 0000000..508a7ae --- /dev/null +++ b/appstoreserverlibrary/models/v2/SendAttemptResult.py @@ -0,0 +1,22 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class SendAttemptResult(Enum): + """ + The success or error information the App Store server records when it attempts to send an App Store server notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult + """ + SUCCESS = "SUCCESS" + TIMED_OUT = "TIMED_OUT" + TLS_ISSUE = "TLS_ISSUE" + CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" + NO_RESPONSE = "NO_RESPONSE" + SOCKET_ISSUE = "SOCKET_ISSUE" + UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" + INVALID_RESPONSE = "INVALID_RESPONSE" + PREMATURE_CLOSE = "PREMATURE_CLOSE" + UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" + OTHER = "OTHER" diff --git a/appstoreserverlibrary/models/v2/SendTestNotificationResponse.py b/appstoreserverlibrary/models/v2/SendTestNotificationResponse.py new file mode 100644 index 0000000..a12e878 --- /dev/null +++ b/appstoreserverlibrary/models/v2/SendTestNotificationResponse.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class SendTestNotificationResponse(Model): + """ + A response that contains the test notification token. + + https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse + """ + + testNotificationToken: Optional[str] = Field(default=None) + """ + A unique identifier for a notification test that the App Store server sends to your server. + + https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken + """ diff --git a/appstoreserverlibrary/models/v2/Status.py b/appstoreserverlibrary/models/v2/Status.py new file mode 100644 index 0000000..5ce12b6 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Status.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class Status(Enum): + """ + The status of an auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/status + """ + ACTIVE = 1 + EXPIRED = 2 + BILLING_RETRY = 3 + BILLING_GRACE_PERIOD = 4 + REVOKED = 5 diff --git a/appstoreserverlibrary/models/v2/StatusResponse.py b/appstoreserverlibrary/models/v2/StatusResponse.py new file mode 100644 index 0000000..ebbbe18 --- /dev/null +++ b/appstoreserverlibrary/models/v2/StatusResponse.py @@ -0,0 +1,43 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + +from pydantic import Field + +from .Base import Model +from .Environment import Environment +from .SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem + + +class StatusResponse(Model): + """ + A response that contains status information for all of a customer's auto-renewable subscriptions in your app. + + https://developer.apple.com/documentation/appstoreserverapi/statusresponse + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment, sandbox or production, in which the App Store generated the response. + + https://developer.apple.com/documentation/appstoreserverapi/environment + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier of an app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid + """ + + appAppleId: Optional[int] = Field(default=None) + """ + The unique identifier of an app in the App Store. + + https://developer.apple.com/documentation/appstoreservernotifications/appappleid + """ + + data: Optional[List[SubscriptionGroupIdentifierItem]] = Field(default=None) + """ + An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information. + + """ diff --git a/appstoreserverlibrary/models/v2/SubscriptionGroupIdentifierItem.py b/appstoreserverlibrary/models/v2/SubscriptionGroupIdentifierItem.py new file mode 100644 index 0000000..c3da848 --- /dev/null +++ b/appstoreserverlibrary/models/v2/SubscriptionGroupIdentifierItem.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + +from pydantic import Field + +from .Base import Model +from .LastTransactionsItem import LastTransactionsItem + + +class SubscriptionGroupIdentifierItem(Model): + """ + Information for auto-renewable subscriptions, including signed transaction information and signed renewal information, for one subscription group. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem + """ + + subscriptionGroupIdentifier: Optional[str] = Field(default=None) + """ + The identifier of the subscription group that the subscription belongs to. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier + """ + + lastTransactions: Optional[List[LastTransactionsItem]] = Field(default=None) + """ + An array of the most recent App Store-signed transaction information and App Store-signed renewal information for all auto-renewable subscriptions in the subscription group. + """ diff --git a/appstoreserverlibrary/models/v2/Subtype.py b/appstoreserverlibrary/models/v2/Subtype.py new file mode 100644 index 0000000..5d28233 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Subtype.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class Subtype(Enum): + """ + A notification subtype value that App Store Server Notifications 2 uses. + + https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype + """ + INITIAL_BUY = "INITIAL_BUY" + RESUBSCRIBE = "RESUBSCRIBE" + DOWNGRADE = "DOWNGRADE" + UPGRADE = "UPGRADE" + AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED" + AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED" + VOLUNTARY = "VOLUNTARY" + BILLING_RETRY = "BILLING_RETRY" + PRICE_INCREASE = "PRICE_INCREASE" + GRACE_PERIOD = "GRACE_PERIOD" + PENDING = "PENDING" + ACCEPTED = "ACCEPTED" + BILLING_RECOVERY = "BILLING_RECOVERY" + PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE" + SUMMARY = "SUMMARY" + FAILURE = "FAILURE" diff --git a/appstoreserverlibrary/models/v2/Summary.py b/appstoreserverlibrary/models/v2/Summary.py new file mode 100644 index 0000000..2f1c280 --- /dev/null +++ b/appstoreserverlibrary/models/v2/Summary.py @@ -0,0 +1,73 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from attr import define +from typing import List, Optional +import attr +from pydantic import Field + +from .Base import Model +from .Environment import Environment + + +class Summary(Model): + """ + The payload data for a subscription-renewal-date extension notification. + + https://developer.apple.com/documentation/appstoreservernotifications/summary + """ + + environment: Optional[Environment] = Field(default=None) + """ + The server environment that the notification applies to, either sandbox or production. + + https://developer.apple.com/documentation/appstoreservernotifications/environment + """ + + appAppleId: Optional[int] = Field(default=None) + """ + The unique identifier of an app in the App Store. + + https://developer.apple.com/documentation/appstoreservernotifications/appappleid + """ + + bundleId: Optional[str] = Field(default=None) + """ + The bundle identifier of an app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid + """ + + productId: Optional[str] = Field(default=None) + """ + The unique identifier for the product, that you create in App Store Connect. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ + + requestIdentifier: Optional[str] = Field(default=None) + """ + A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ + + storefrontCountryCodes: Optional[List[str]] = Field(default=None) + """ + A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes + """ + + succeededCount: Optional[int] = Field(default=None) + """ + The count of subscriptions that successfully receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/succeededcount + """ + + failedCount: Optional[int] = Field(default=None) + """ + The count of subscriptions that fail to receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/failedcount + """ diff --git a/appstoreserverlibrary/models/v2/TransactionHistoryRequest.py b/appstoreserverlibrary/models/v2/TransactionHistoryRequest.py new file mode 100644 index 0000000..cc3e5b4 --- /dev/null +++ b/appstoreserverlibrary/models/v2/TransactionHistoryRequest.py @@ -0,0 +1,73 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum +from typing import List, Optional + +from pydantic import Field + +from .Base import Model +from .InAppOwnershipType import InAppOwnershipType + + +class ProductType(Enum): + AUTO_RENEWABLE = "AUTO_RENEWABLE" + NON_RENEWABLE = "NON_RENEWABLE" + CONSUMABLE = "CONSUMABLE" + NON_CONSUMABLE = "NON_CONSUMABLE" + + +class Order(Enum): + ASCENDING = "ASCENDING" + DESCENDING = "DESCENDING" + + +class TransactionHistoryRequest(Model): + startDate: Optional[int] = Field(default=None) + """ + An optional start date of the timespan for the transaction history records you're requesting. The startDate must precede the endDate if you specify both dates. To be included in results, the transaction's purchaseDate must be equal to or greater than the startDate. + + https://developer.apple.com/documentation/appstoreserverapi/startdate + """ + + endDate: Optional[int] = Field(default=None) + """ + An optional end date of the timespan for the transaction history records you're requesting. Choose an endDate that's later than the startDate if you specify both dates. Using an endDate in the future is valid. To be included in results, the transaction's purchaseDate must be less than the endDate. + + https://developer.apple.com/documentation/appstoreserverapi/enddate + """ + + productIds: Optional[List[str]] = Field(default=None) + """ + An optional filter that indicates the product identifier to include in the transaction history. Your query may specify more than one productID. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ + + productTypes: Optional[List[ProductType]] = Field(default=None) + """ + An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType. + """ + + sort: Optional[Order] = Field(default=None) + """ + An optional sort order for the transaction history records. The response sorts the transaction records by their recently modified date. The default value is ASCENDING, so you receive the oldest records first. + """ + + subscriptionGroupIdentifiers: Optional[List[str]] = Field(default=None) + """ + An optional filter that indicates the subscription group identifier to include in the transaction history. Your query may specify more than one subscriptionGroupIdentifier. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier + """ + + inAppOwnershipType: Optional[InAppOwnershipType] = Field(default=None) + """ + An optional filter that limits the transaction history by the in-app ownership type. + + https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype + """ + + revoked: Optional[bool] = Field(default=None) + """ + An optional Boolean value that indicates whether the response includes only revoked transactions when the value is true, or contains only nonrevoked transactions when the value is false. By default, the request doesn't include this parameter. + """ diff --git a/appstoreserverlibrary/models/v2/TransactionInfoResponse.py b/appstoreserverlibrary/models/v2/TransactionInfoResponse.py new file mode 100644 index 0000000..1594b39 --- /dev/null +++ b/appstoreserverlibrary/models/v2/TransactionInfoResponse.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional + +from pydantic import Field + +from appstoreserverlibrary.models.v2.Base import Model + + +class TransactionInfoResponse(Model): + """ + A response that contains signed transaction information for a single transaction. + + https://developer.apple.com/documentation/appstoreserverapi/transactioninforesponse + """ + + signedTransactionInfo: Optional[str] = Field(default=None) + """ + A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + """ diff --git a/appstoreserverlibrary/models/v2/TransactionReason.py b/appstoreserverlibrary/models/v2/TransactionReason.py new file mode 100644 index 0000000..0330745 --- /dev/null +++ b/appstoreserverlibrary/models/v2/TransactionReason.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class TransactionReason(Enum): + """ + The cause of a purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates. + + https://developer.apple.com/documentation/appstoreserverapi/transactionreason + """ + PURCHASE = "PURCHASE" + RENEWAL = "RENEWAL" diff --git a/appstoreserverlibrary/models/v2/Type.py b/appstoreserverlibrary/models/v2/Type.py new file mode 100644 index 0000000..155d06d --- /dev/null +++ b/appstoreserverlibrary/models/v2/Type.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class Type(Enum): + """ + The type of in-app purchase products you can offer in your app. + + https://developer.apple.com/documentation/appstoreserverapi/type + """ + AUTO_RENEWABLE_SUBSCRIPTION = "Auto-Renewable Subscription" + NON_CONSUMABLE = "Non-Consumable" + CONSUMABLE = "Consumable" + NON_RENEWING_SUBSCRIPTION = "Non-Renewing Subscription" diff --git a/appstoreserverlibrary/models/v2/UserStatus.py b/appstoreserverlibrary/models/v2/UserStatus.py new file mode 100644 index 0000000..84fccf3 --- /dev/null +++ b/appstoreserverlibrary/models/v2/UserStatus.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from enum import Enum + + +class UserStatus(Enum): + """ + The status of a customer's account within your app. + + https://developer.apple.com/documentation/appstoreserverapi/userstatus + """ + UNDECLARED = 0 + ACTIVE = 1 + SUSPENDED = 2 + TERMINATED = 3 + LIMITED_ACCESS = 4 diff --git a/appstoreserverlibrary/models/v2/__init__.py b/appstoreserverlibrary/models/v2/__init__.py new file mode 100644 index 0000000..e69de29 From 0e58e6665f7644a0bfc872fb285aefd7b7d4b303 Mon Sep 17 00:00:00 2001 From: socar-humphrey Date: Sun, 17 Sep 2023 22:56:35 +0900 Subject: [PATCH 3/4] test: add variant to test pydantic compatibility --- .../signed_data_verifier_v2.py | 298 ++++++++++++++++++ tests/test_payload_verification.py | 81 +++-- 2 files changed, 348 insertions(+), 31 deletions(-) create mode 100644 appstoreserverlibrary/signed_data_verifier_v2.py diff --git a/appstoreserverlibrary/signed_data_verifier_v2.py b/appstoreserverlibrary/signed_data_verifier_v2.py new file mode 100644 index 0000000..2d0e9f4 --- /dev/null +++ b/appstoreserverlibrary/signed_data_verifier_v2.py @@ -0,0 +1,298 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +from typing import List +from base64 import b64decode +from enum import Enum +import time +import datetime + +import cattrs + +import asn1 +import jwt +import requests +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA +from cryptography.hazmat.primitives.hashes import SHA1, SHA256 +from cryptography.x509 import ocsp, oid +from OpenSSL import crypto + +from appstoreserverlibrary.models.v2.ResponseBodyV2DecodedPayload import ResponseBodyV2DecodedPayload +from appstoreserverlibrary.models.v2.AppTransaction import AppTransaction +from appstoreserverlibrary.models.v2.Environment import Environment +from appstoreserverlibrary.models.v2.JWSRenewalInfoDecodedPayload import JWSRenewalInfoDecodedPayload +from appstoreserverlibrary.models.v2.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload + + +class SignedDataVerifierV2: + """ + A class providing utility methods for verifying and decoding App Store signed data. + """ + + def __init__( + self, + root_certificates: List[bytes], + enable_online_checks: bool, + environment: Environment, + bundle_id: str, + app_apple_id: str = None, + ): + self._chain_verifier = _ChainVerifier(root_certificates) + self._environment = environment + self._bundle_id = bundle_id + self._app_apple_id = app_apple_id + self._enable_online_checks = enable_online_checks + + def verify_and_decode_renewal_info(self, signed_renewal_info: str) -> JWSRenewalInfoDecodedPayload: + """ + Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device + + :param signed_renewal_info: The signedRenewalInfo field + :return: The decoded renewal info after verification + :throws VerificationException: Thrown if the data could not be verified + """ + return cattrs.structure(self._decode_signed_object(signed_renewal_info), JWSRenewalInfoDecodedPayload) + + def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTransactionDecodedPayload: + """ + Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device + + :param signed_transaction: The signedRenewalInfo field + :return: The decoded transaction info after verification + :throws VerificationException: Thrown if the data could not be verified + """ + decoded_transaction_info = cattrs.structure(self._decode_signed_object(signed_transaction), + JWSTransactionDecodedPayload) + if decoded_transaction_info.bundleId != self._bundle_id: + raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) + if decoded_transaction_info.environment != self._environment: + raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) + return decoded_transaction_info + + def verify_and_decode_notification(self, signed_payload: str) -> ResponseBodyV2DecodedPayload: + """ + Verifies and decodes an App Store Server Notification signedPayload + + :param signedPayload: The payload received by your server + :return: The decoded payload after verification + :throws VerificationException: Thrown if the data could not be verified + """ + decoded_dict = self._decode_signed_object(signed_payload) + decoded_signed_notification = ResponseBodyV2DecodedPayload.model_validate(decoded_dict) + bundle_id = None + app_apple_id = None + environment = None + if decoded_signed_notification.data: + bundle_id = decoded_signed_notification.data.bundleId + app_apple_id = decoded_signed_notification.data.appAppleId + environment = decoded_signed_notification.data.environment + elif decoded_signed_notification.summary: + bundle_id = decoded_signed_notification.summary.bundleId + app_apple_id = decoded_signed_notification.summary.appAppleId + environment = decoded_signed_notification.summary.environment + if bundle_id != self._bundle_id or ( + self._environment == Environment.PRODUCTION and app_apple_id != self._app_apple_id): + raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) + if environment != self._environment: + raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) + return decoded_signed_notification + + def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppTransaction: + """ + Verifies and decodes a signed AppTransaction + + :param signed_app_transaction: The signed AppTransaction + :return: The decoded AppTransaction after validation + :throws VerificationException: Thrown if the data could not be verified + """ + decoded_dict = self._decode_signed_object(signed_app_transaction) + decoded_app_transaction = cattrs.structure(decoded_dict, AppTransaction) + environment = decoded_app_transaction.receiptType + if decoded_app_transaction.bundleId != self._bundle_id or ( + self._environment == Environment.PRODUCTION and decoded_app_transaction.appAppleId != self._app_apple_id): + raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) + if environment != self._environment: + raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) + return decoded_app_transaction + + def _decode_signed_object(self, signed_obj: str) -> dict: + try: + unverified_headers: dict = jwt.get_unverified_header(signed_obj) + x5c_header: List[str] = unverified_headers.get("x5c") + if x5c_header is None or len(x5c_header) == 0: + raise Exception("x5c claim was empty") + algorithm_header: str = unverified_headers.get("alg") + if algorithm_header is None or "ES256" != algorithm_header: + raise Exception("Algorithm was not ES256") + decoded_jwt = jwt.decode(signed_obj, options={"verify_signature": False}) + signed_date = decoded_jwt.get('signedDate') if decoded_jwt.get( + 'signedDate') is not None else decoded_jwt.get('receiptCreationDate') + effective_date = time.time() if self._enable_online_checks or signed_date is None else int( + signed_date) // 1000 + signing_key = self._chain_verifier.verify_chain(x5c_header, self._enable_online_checks, effective_date) + return jwt.decode(signed_obj, signing_key, algorithms=["ES256"]) + except VerificationException as e: + raise e + except Exception as e: + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e + + +class _ChainVerifier: + def __init__(self, root_certificates: List[bytes], enable_strict_checks=True): + self.enable_strict_checks = enable_strict_checks + self.root_certificates = root_certificates + + def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str: + if len(self.root_certificates) == 0: + raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) + if len(certificates) != 3: + raise VerificationException(VerificationStatus.INVALID_CHAIN_LENGTH) + trusted_store = crypto.X509Store() + try: + for trusted_cert_bytes in self.root_certificates: + trusted_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, trusted_cert_bytes) + trusted_store.add_cert(trusted_cert) + if self.enable_strict_checks: + trusted_store.set_flags(crypto.X509StoreFlags.X509_STRICT) + leaf_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[0], validate=True)) + intermediate_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[1], validate=True)) + verification_context = crypto.X509StoreContext(trusted_store, leaf_cert, [intermediate_cert]) + except Exception as e: + raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) from e + + trusted_store.set_time(datetime.datetime.fromtimestamp(effective_date, tz=datetime.timezone.utc)) + try: + verification_context.verify_certificate() + trusted_chain = verification_context.get_verified_chain() + except Exception as e: + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e + self.check_oid(trusted_chain[0].to_cryptography(), "1.2.840.113635.100.6.11.1") + self.check_oid(trusted_chain[1].to_cryptography(), "1.2.840.113635.100.6.2.1") + if perform_online_checks: + self.check_ocsp_status(trusted_chain[1], trusted_chain[2], trusted_chain[2]) + self.check_ocsp_status(trusted_chain[0], trusted_chain[1], trusted_chain[2]) + return ( + leaf_cert.to_cryptography() + .public_key() + .public_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo) + .decode() + ) + + def check_oid(self, cert: x509.Certificate, oid: str): + try: + cert.extensions.get_extension_for_oid(x509.ObjectIdentifier(oid)) + except Exception as e: + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e + + def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto.X509): + builder = ocsp.OCSPRequestBuilder() + builder = builder.add_certificate(cert.to_cryptography(), issuer.to_cryptography(), SHA256()) + req = builder.build() + authority_values = ( + cert.to_cryptography() + .extensions.get_extension_for_oid(x509.oid.ExtensionOID.AUTHORITY_INFORMATION_ACCESS) + .value + ) + ocsps = [val for val in authority_values if val.access_method == x509.oid.AuthorityInformationAccessOID.OCSP] + for o in ocsps: + r = requests.post( + o.access_location.value, + headers={"Content-Type": "application/ocsp-request"}, + data=req.public_bytes(serialization.Encoding.DER), + ) + if r.status_code == 200: + ocsp_resp = ocsp.load_der_ocsp_response(r.content) + if ocsp_resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL: + certs = [issuer] + for ocsp_cert in ocsp_resp.certificates: + certs.append(crypto.X509.from_cryptography(ocsp_cert)) + # Find signing cert + signing_cert = None + for potential_signing_cert in certs: + if ocsp_resp.responder_key_hash: + subject_public_key_info = ( + potential_signing_cert.get_pubkey() + .to_cryptography_key() + .public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ) + decoder = asn1.Decoder() + decoder.start(subject_public_key_info) + decoder.enter() + decoder.read() + _, value = decoder.read() + digest = hashes.Hash(SHA1()) + digest.update(value) + if digest.finalize() == ocsp_resp.responder_key_hash: + signing_cert = potential_signing_cert + break + + elif ocsp_resp.responder_name: + if ocsp_resp.responder_name == potential_signing_cert.subject.rfc4514_string(): + signing_cert = potential_signing_cert + break + if signing_cert is None: + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) + + if signing_cert.to_cryptography().public_bytes( + encoding=serialization.Encoding.DER + ) == issuer.to_cryptography().public_bytes(encoding=serialization.Encoding.DER): + # We trust this because it is the issuer + pass + else: + trusted_store = crypto.X509Store() + trusted_store.add_cert(issuer) + trusted_store.add_cert(root) # Apparently a full chain is always needed + verification_context = crypto.X509StoreContext(trusted_store, signing_cert, []) + verification_context.verify_certificate() + if ( + oid.ExtendedKeyUsageOID.OCSP_SIGNING + not in signing_cert.to_cryptography() + .extensions.get_extension_for_class(x509.ExtendedKeyUsage) + .value._usages + ): + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) + + # Confirm response is signed by signing_certificate + signing_cert.to_cryptography().public_key().verify( + ocsp_resp.signature, ocsp_resp.tbs_response_bytes, ECDSA(ocsp_resp.signature_hash_algorithm) + ) + + # Get the CertId + for single_response in ocsp_resp.responses: + # Get the cert ID with the provided hashing algorithm (using the request builder wrapper) + builder = ocsp.OCSPRequestBuilder() + builder = builder.add_certificate( + cert.to_cryptography(), issuer.to_cryptography(), single_response.hash_algorithm + ) + req = builder.build() + if ( + single_response.certificate_status == ocsp.OCSPCertStatus.GOOD + and single_response.serial_number == req.serial_number + and single_response.issuer_key_hash == req.issuer_key_hash + and single_response.issuer_name_hash == req.issuer_name_hash + ): + # Success + return + + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) + + +class VerificationStatus(Enum): + OK = 0 + VERIFICATION_FAILURE = 1 + INVALID_APP_IDENTIFIER = 2 + INVALID_CERTIFICATE = 3 + INVALID_CHAIN_LENGTH = 4 + INVALID_CHAIN = 5 + INVALID_ENVIRONMENT = 6 + + +class VerificationException(Exception): + def __init__(self, status: VerificationStatus): + super().__init__("Verification failed with status " + status.name) + self.status = status diff --git a/tests/test_payload_verification.py b/tests/test_payload_verification.py index d09acd7..0d64be5 100644 --- a/tests/test_payload_verification.py +++ b/tests/test_payload_verification.py @@ -6,6 +6,7 @@ from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationTypeV2 from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus, SignedDataVerifier +from appstoreserverlibrary.signed_data_verifier_v2 import SignedDataVerifierV2 ROOT_CA_BASE64_ENCODED = "MIIBgjCCASmgAwIBAgIJALUc5ALiH5pbMAoGCCqGSM49BAMDMDYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8wHhcNMjMwMTA1MjEzMDIyWhcNMzMwMTAyMjEzMDIyWjA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc+/Bl+gospo6tf9Z7io5tdKdrlN1YdVnqEhEDXDShzdAJPQijamXIMHf8xWWTa1zgoYTxOKpbuJtDplz1XriTaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDRwAwRAIgemWQXnMAdTad2JDJWng9U4uBBL5mA7WI05H7oH7c6iQCIHiRqMjNfzUAyiu9h6rOU/K+iTR0I/3Y/NSWsXHX+acc" @@ -15,65 +16,83 @@ RENEWAL_INFO = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw" TRANSACTION_INFO = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ" + +def get_payload_verifier(verifier_cls): + verifier = verifier_cls([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.SANDBOX, "com.example") + verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs + return verifier + + +verifier_variants = [ + ("attr", get_payload_verifier(SignedDataVerifier)), + ("pydantic", get_payload_verifier(SignedDataVerifierV2)) +] + + class PayloadVerification(unittest.TestCase): def test_app_store_server_notification_decoding(self): - verifier = self.get_payload_verifier() - notification = verifier.verify_and_decode_notification(TEST_NOTIFICATION) - self.assertEqual(notification.notificationType, NotificationTypeV2.TEST) + for variant, verifier in verifier_variants: + with self.subTest(variant): + notification = verifier.verify_and_decode_notification(TEST_NOTIFICATION) + self.assertEqual(notification.notificationType, NotificationTypeV2.TEST) def test_app_store_server_notification_decoding_production(self): - verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", 1234) + verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", + 1234) verifier._chain_verifier.enable_strict_checks = False with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(TEST_NOTIFICATION) self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) def test_missing_x5c_header(self): - verifier = self.get_payload_verifier() - with self.assertRaises(VerificationException) as context: - verifier.verify_and_decode_notification(MISSING_X5C_HEADER_CLAIM) - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + for variant, verifier in verifier_variants: + with self.subTest(variant): + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_notification(MISSING_X5C_HEADER_CLAIM) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) def test_wrong_bundle_id_for_server_notification(self): - verifier = self.get_payload_verifier() - with self.assertRaises(VerificationException) as context: - verifier.verify_and_decode_notification(WRONG_BUNDLE_ID) - self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + for variant, verifier in verifier_variants: + with self.subTest(variant): + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_notification(WRONG_BUNDLE_ID) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) def test_wrong_app_apple_id_for_server_notification(self): - verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", 1235) + verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", + 1235) verifier._chain_verifier.enable_strict_checks = False with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(TEST_NOTIFICATION) self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) def test_renewal_info_decoding(self): - verifier = self.get_payload_verifier() - notification = verifier.verify_and_decode_renewal_info(RENEWAL_INFO) - self.assertEqual(notification.environment, Environment.SANDBOX) + for variant, verifier in verifier_variants: + with self.subTest(variant): + notification = verifier.verify_and_decode_renewal_info(RENEWAL_INFO) + self.assertEqual(notification.environment, Environment.SANDBOX) def test_transaction_info_decoding(self): - verifier = self.get_payload_verifier() - notification = verifier.verify_and_decode_signed_transaction(TRANSACTION_INFO) - self.assertEqual(notification.environment, Environment.SANDBOX) + for variant, verifier in verifier_variants: + with self.subTest(variant): + notification = verifier.verify_and_decode_signed_transaction(TRANSACTION_INFO) + self.assertEqual(notification.environment, Environment.SANDBOX) def test_malformed_jwt_with_too_many_parts(self): - verifier = self.get_payload_verifier() - with self.assertRaises(VerificationException) as context: - verifier.verify_and_decode_notification("a.b.c.d") - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + for variant, verifier in verifier_variants: + with self.subTest(variant): + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_notification("a.b.c.d") + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) def test_malformed_jwt_with_malformed_data(self): - verifier = self.get_payload_verifier() - with self.assertRaises(VerificationException) as context: - verifier.verify_and_decode_notification("a.b.c") - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + for variant, verifier in verifier_variants: + with self.subTest(variant): + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_notification("a.b.c") + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) - def get_payload_verifier(self) -> SignedDataVerifier: - verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.SANDBOX, "com.example") - verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs - return verifier if __name__ == '__main__': unittest.main() From 42431d3887adbf240a5ebf9714a9f74873a2e9bb Mon Sep 17 00:00:00 2001 From: socar-humphrey Date: Sun, 17 Sep 2023 23:50:12 +0900 Subject: [PATCH 4/4] refactor: make tests pass --- .../models/v2/Environment.py | 3 +- .../signed_data_verifier_v2.py | 10 ++-- tests/test_payload_verification.py | 58 +++++++++++-------- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/appstoreserverlibrary/models/v2/Environment.py b/appstoreserverlibrary/models/v2/Environment.py index ab1abf7..2d26532 100644 --- a/appstoreserverlibrary/models/v2/Environment.py +++ b/appstoreserverlibrary/models/v2/Environment.py @@ -2,7 +2,8 @@ from enum import Enum -class Environment(Enum): + +class Environment(str, Enum): """ The server environment, either sandbox or production. diff --git a/appstoreserverlibrary/signed_data_verifier_v2.py b/appstoreserverlibrary/signed_data_verifier_v2.py index 2d0e9f4..2a34099 100644 --- a/appstoreserverlibrary/signed_data_verifier_v2.py +++ b/appstoreserverlibrary/signed_data_verifier_v2.py @@ -6,8 +6,6 @@ import time import datetime -import cattrs - import asn1 import jwt import requests @@ -52,7 +50,7 @@ def verify_and_decode_renewal_info(self, signed_renewal_info: str) -> JWSRenewal :return: The decoded renewal info after verification :throws VerificationException: Thrown if the data could not be verified """ - return cattrs.structure(self._decode_signed_object(signed_renewal_info), JWSRenewalInfoDecodedPayload) + return JWSRenewalInfoDecodedPayload.model_validate(self._decode_signed_object(signed_renewal_info)) def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTransactionDecodedPayload: """ @@ -62,8 +60,8 @@ def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTr :return: The decoded transaction info after verification :throws VerificationException: Thrown if the data could not be verified """ - decoded_transaction_info = cattrs.structure(self._decode_signed_object(signed_transaction), - JWSTransactionDecodedPayload) + signed_object = self._decode_signed_object(signed_transaction) + decoded_transaction_info = JWSTransactionDecodedPayload.model_validate(signed_object) if decoded_transaction_info.bundleId != self._bundle_id: raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) if decoded_transaction_info.environment != self._environment: @@ -107,7 +105,7 @@ def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppT :throws VerificationException: Thrown if the data could not be verified """ decoded_dict = self._decode_signed_object(signed_app_transaction) - decoded_app_transaction = cattrs.structure(decoded_dict, AppTransaction) + decoded_app_transaction = AppTransaction.model_validate(decoded_dict) environment = decoded_app_transaction.receiptType if decoded_app_transaction.bundleId != self._bundle_id or ( self._environment == Environment.PRODUCTION and decoded_app_transaction.appAppleId != self._app_apple_id): diff --git a/tests/test_payload_verification.py b/tests/test_payload_verification.py index 0d64be5..a7ee730 100644 --- a/tests/test_payload_verification.py +++ b/tests/test_payload_verification.py @@ -1,10 +1,12 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. - +import importlib import unittest from base64 import b64decode -from appstoreserverlibrary.models.Environment import Environment -from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationTypeV2 +import appstoreserverlibrary.models as v1 +import appstoreserverlibrary.models.v2 as v2 +from appstoreserverlibrary.models.Environment import Environment +from appstoreserverlibrary.models.v2.Environment import Environment as EnvironmentV2 from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus, SignedDataVerifier from appstoreserverlibrary.signed_data_verifier_v2 import SignedDataVerifierV2 @@ -18,24 +20,26 @@ def get_payload_verifier(verifier_cls): - verifier = verifier_cls([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.SANDBOX, "com.example") + environment = v1.Environment.Environment.SANDBOX if verifier_cls == SignedDataVerifier else v2.Environment.Environment.SANDBOX + verifier = verifier_cls([b64decode(ROOT_CA_BASE64_ENCODED)], False, environment, "com.example") verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs return verifier verifier_variants = [ - ("attr", get_payload_verifier(SignedDataVerifier)), - ("pydantic", get_payload_verifier(SignedDataVerifierV2)) + ("v1", v1.__name__, get_payload_verifier(SignedDataVerifier)), + ("v2", v2.__name__, get_payload_verifier(SignedDataVerifierV2)) ] class PayloadVerification(unittest.TestCase): def test_app_store_server_notification_decoding(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): notification = verifier.verify_and_decode_notification(TEST_NOTIFICATION) - self.assertEqual(notification.notificationType, NotificationTypeV2.TEST) + self.assertEqual(notification.notificationType, + importlib.import_module(module_name).NotificationTypeV2.NotificationTypeV2.TEST) def test_app_store_server_notification_decoding_production(self): verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", @@ -46,18 +50,20 @@ def test_app_store_server_notification_decoding_production(self): self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) def test_missing_x5c_header(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): - with self.assertRaises(VerificationException) as context: + with self.assertRaises(importlib.import_module(verifier.__module__).VerificationException) as context: verifier.verify_and_decode_notification(MISSING_X5C_HEADER_CLAIM) - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + self.assertEqual(context.exception.status, + importlib.import_module(verifier.__module__).VerificationStatus.VERIFICATION_FAILURE) def test_wrong_bundle_id_for_server_notification(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): - with self.assertRaises(VerificationException) as context: + with self.assertRaises(importlib.import_module(verifier.__module__).VerificationException) as context: verifier.verify_and_decode_notification(WRONG_BUNDLE_ID) - self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + self.assertEqual(context.exception.status, + importlib.import_module(verifier.__module__).VerificationStatus.INVALID_APP_IDENTIFIER) def test_wrong_app_apple_id_for_server_notification(self): verifier = SignedDataVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False, Environment.PRODUCTION, "com.example", @@ -68,30 +74,34 @@ def test_wrong_app_apple_id_for_server_notification(self): self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) def test_renewal_info_decoding(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): notification = verifier.verify_and_decode_renewal_info(RENEWAL_INFO) - self.assertEqual(notification.environment, Environment.SANDBOX) + self.assertEqual(notification.environment, + importlib.import_module(module_name).Environment.Environment.SANDBOX) def test_transaction_info_decoding(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): notification = verifier.verify_and_decode_signed_transaction(TRANSACTION_INFO) - self.assertEqual(notification.environment, Environment.SANDBOX) + self.assertEqual(notification.environment, + importlib.import_module(module_name).Environment.Environment.SANDBOX) def test_malformed_jwt_with_too_many_parts(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): - with self.assertRaises(VerificationException) as context: + with self.assertRaises(importlib.import_module(verifier.__module__).VerificationException) as context: verifier.verify_and_decode_notification("a.b.c.d") - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + self.assertEqual(context.exception.status, + importlib.import_module(verifier.__module__).VerificationStatus.VERIFICATION_FAILURE) def test_malformed_jwt_with_malformed_data(self): - for variant, verifier in verifier_variants: + for variant, module_name, verifier in verifier_variants: with self.subTest(variant): - with self.assertRaises(VerificationException) as context: + with self.assertRaises(importlib.import_module(verifier.__module__).VerificationException) as context: verifier.verify_and_decode_notification("a.b.c") - self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + self.assertEqual(context.exception.status, + importlib.import_module(verifier.__module__).VerificationStatus.VERIFICATION_FAILURE) if __name__ == '__main__':