diff --git a/index.ts b/index.ts index 6d666f2..c819af8 100644 --- a/index.ts +++ b/index.ts @@ -76,13 +76,14 @@ export { AppTransaction } from './models/AppTransaction' import jsonwebtoken = require('jsonwebtoken'); import { NotificationHistoryRequest } from './models/NotificationHistoryRequest'; import { NotificationHistoryResponse, NotificationHistoryResponseValidator } from './models/NotificationHistoryResponse'; +import { URLSearchParams } from 'url'; export class AppStoreServerAPIClient { private static PRODUCTION_URL = "https://api.storekit.itunes.apple.com"; private static SANDBOX_URL = "https://api.storekit-sandbox.itunes.apple.com"; private static USER_AGENT = "app-store-server-library/node/0.1"; - private issueId: string + private issuerId: string private keyId: string private signingKey: string private bundleId: string @@ -97,31 +98,32 @@ export class AppStoreServerAPIClient { * @param environment The environment to target */ public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string, environment: Environment) { - this.issueId = issuerId + this.issuerId = issuerId this.keyId = keyId this.bundleId = bundleId this.signingKey = signingKey this.urlBase = environment === "Sandbox" ? AppStoreServerAPIClient.SANDBOX_URL : AppStoreServerAPIClient.PRODUCTION_URL } - protected async makeRequest(path: string, method: string, queryParameters: { [key: string]: [string]}, body: object | null, validator: Validator | null): Promise { + protected async makeRequest(path: string, method: string, queryParameters: { [key: string]: string[]}, body: object | null, validator: Validator | null): Promise { const headers: { [key: string]: string } = { 'User-Agent': AppStoreServerAPIClient.USER_AGENT, 'Authorization': 'Bearer ' + this.createBearerToken(), 'Accept': 'application/json', } - const parsedQueryParameters = new URLSearchParams(queryParameters) + const parsedQueryParameters = new URLSearchParams() + for (const queryParam in queryParameters) { + for (const queryVal of queryParameters[queryParam]) { + parsedQueryParameters.append(queryParam, queryVal) + } + } let stringBody = undefined if (body != null) { stringBody = JSON.stringify(body) headers['Content-Type'] = 'application/json' } - const response = await fetch(this.urlBase + path + '?' + parsedQueryParameters, { - method: method, - body: stringBody, - headers: headers - }) + const response = await this.makeFetchRequest(path, parsedQueryParameters, method, stringBody, headers) if(response.ok) { // Success @@ -142,8 +144,8 @@ export class AppStoreServerAPIClient { const responseBody = await response.json() const errorCode = responseBody['errorCode'] - if (Object.values(APIError).includes(errorCode)) { - throw new APIException(response.status, errorCode as APIError) + if (errorCode) { + throw new APIException(response.status, errorCode) } throw new APIException(response.status) @@ -156,6 +158,14 @@ export class AppStoreServerAPIClient { } } + protected async makeFetchRequest(path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) { + return await fetch(this.urlBase + path + '?' + parsedQueryParameters, { + method: method, + body: stringBody, + headers: headers + }); + } + /** * Uses a subscription’s product identifier to extend the renewal date for all of its eligible active subscribers. * @@ -165,7 +175,7 @@ export class AppStoreServerAPIClient { * {@link https://developer.apple.com/documentation/appstoreserverapi/extend_subscription_renewal_dates_for_all_active_subscribers Extend Subscription Renewal Dates for All Active Subscribers} */ public async extendRenewalDateForAllActiveSubscribers(massExtendRenewalDateRequest: MassExtendRenewalDateRequest): Promise { - return await this.makeRequest("/inApps/v1/subscriptions/extend/mass/", "POST", {}, massExtendRenewalDateRequest, new MassExtendRenewalDateResponseValidator()); + return await this.makeRequest("/inApps/v1/subscriptions/extend/mass", "POST", {}, massExtendRenewalDateRequest, new MassExtendRenewalDateResponseValidator()); } /** @@ -190,7 +200,7 @@ export class AppStoreServerAPIClient { * @throws APIException If a response was returned indicating the request could not be processed * {@link https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses Get All Subscription Statuses} */ - public async getAllSubscriptionStatuses(transactionId: string, status: [Status] | undefined = undefined): Promise { + public async getAllSubscriptionStatuses(transactionId: string, status: Status[] | undefined = undefined): Promise { const queryParameters: { [key: string]: [string]} = {} if (status != null) { queryParameters["status"] = status.map(s => s.toString()) as [string]; @@ -269,7 +279,7 @@ export class AppStoreServerAPIClient { * {@link https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history Get Transaction History} */ public async getTransactionHistory(transactionId: string, revision: string | null, transactionHistoryRequest: TransactionHistoryRequest): Promise { - const queryParameters: { [key: string]: [string]} = {} + const queryParameters: { [key: string]: string[]} = {} if (revision != null) { queryParameters["revision"] = [revision]; } @@ -294,7 +304,7 @@ export class AppStoreServerAPIClient { if (transactionHistoryRequest.inAppOwnershipType) { queryParameters["inAppOwnershipType"] = [transactionHistoryRequest.inAppOwnershipType]; } - if (transactionHistoryRequest.revoked) { + if (transactionHistoryRequest.revoked !== undefined) { queryParameters["revoked"] = [transactionHistoryRequest.revoked.toString()]; } return await this.makeRequest("/inApps/v1/history/" + transactionId, "GET", queryParameters, null, new HistoryResponseValidator()); @@ -351,16 +361,16 @@ export class AppStoreServerAPIClient { const payload = { bid: this.bundleId } - return jsonwebtoken.sign(payload, this.signingKey, { algorithm: 'ES256', keyid: this.keyId, issuer: this.issueId, audience: 'appstoreconnect-v1', expiresIn: '5m'}); + return jsonwebtoken.sign(payload, this.signingKey, { algorithm: 'ES256', keyid: this.keyId, issuer: this.issuerId, audience: 'appstoreconnect-v1', expiresIn: '5m'}); } } export class APIException extends Error { public httpStatusCode: number - public apiError: APIError | null + public apiError: number | APIError | null - constructor(httpStatusCode: number, apiError: APIError | null = null) { + constructor(httpStatusCode: number, apiError: number | null = null) { super() this.httpStatusCode = httpStatusCode this.apiError = apiError diff --git a/models/AccountTenure.ts b/models/AccountTenure.ts index 2201140..7f3dc08 100644 --- a/models/AccountTenure.ts +++ b/models/AccountTenure.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The age of the customer’s account. @@ -18,8 +18,4 @@ export enum AccountTenure { GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7, } -export class AccountTenureValidator implements Validator { - validate(obj: any): obj is AccountTenure { - return Object.values(AccountTenure).includes(obj) - } -} +export class AccountTenureValidator extends NumberValidator {} diff --git a/models/AppTransaction.ts b/models/AppTransaction.ts index 8b31555..ade674e 100644 --- a/models/AppTransaction.ts +++ b/models/AppTransaction.ts @@ -15,7 +15,7 @@ export interface AppTransaction { * * {@link https://developer.apple.com/documentation/storekit/apptransaction/3963901-environment environment} */ - receiptType?: Environment + receiptType?: Environment | string /** * The unique identifier the App Store uses to identify the app. diff --git a/models/AutoRenewStatus.ts b/models/AutoRenewStatus.ts index fd4e93e..eced91b 100644 --- a/models/AutoRenewStatus.ts +++ b/models/AutoRenewStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The renewal status for an auto-renewable subscription. @@ -12,8 +12,4 @@ export enum AutoRenewStatus { ON = 1, } -export class AutoRenewStatusValidator implements Validator { - validate(obj: any): obj is AutoRenewStatus { - return Object.values(AutoRenewStatus).includes(obj) - } -} +export class AutoRenewStatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/ConsumptionRequest.ts b/models/ConsumptionRequest.ts index 3d6da58..5eab90d 100644 --- a/models/ConsumptionRequest.ts +++ b/models/ConsumptionRequest.ts @@ -28,14 +28,14 @@ export interface ConsumptionRequest { * * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus consumptionStatus} **/ - consumptionStatus?: ConsumptionStatus + consumptionStatus?: ConsumptionStatus | number /** * A value that indicates the platform on which the customer consumed the in-app purchase. * * {@link https://developer.apple.com/documentation/appstoreserverapi/platform platform} **/ - platform?: Platform + platform?: Platform | number /** * 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. @@ -49,7 +49,7 @@ export interface ConsumptionRequest { * * {@link https://developer.apple.com/documentation/appstoreserverapi/deliverystatus deliveryStatus} **/ - deliveryStatus?: DeliveryStatus + deliveryStatus?: DeliveryStatus | number /** * The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction. @@ -63,33 +63,33 @@ export interface ConsumptionRequest { * * {@link https://developer.apple.com/documentation/appstoreserverapi/accounttenure accountTenure} **/ - accountTenure?: AccountTenure + accountTenure?: AccountTenure | number /** * A value that indicates the amount of time that the customer used the app. * * {@link https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest ConsumptionRequest} **/ - playTime?: PlayTime + playTime?: PlayTime | number /** * A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. * * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded lifetimeDollarsRefunded} **/ - lifetimeDollarsRefunded?: LifetimeDollarsRefunded + lifetimeDollarsRefunded?: LifetimeDollarsRefunded | number /** * A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. * * {@link https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased lifetimeDollarsPurchased} **/ - lifetimeDollarsPurchased?: LifetimeDollarsPurchased + lifetimeDollarsPurchased?: LifetimeDollarsPurchased | number /** * The status of the customer’s account. * * {@link https://developer.apple.com/documentation/appstoreserverapi/userstatus userStatus} **/ - userStatus?: UserStatus + userStatus?: UserStatus | number } \ No newline at end of file diff --git a/models/ConsumptionStatus.ts b/models/ConsumptionStatus.ts index 33921a6..71898e2 100644 --- a/models/ConsumptionStatus.ts +++ b/models/ConsumptionStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates the extent to which the customer consumed the in-app purchase. @@ -14,8 +14,4 @@ export enum ConsumptionStatus { FULLY_CONSUMED = 3, } -export class ConsumptionStatusValidator implements Validator { - validate(obj: any): obj is ConsumptionStatus { - return Object.values(ConsumptionStatus).includes(obj) - } -} +export class ConsumptionStatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/Data.ts b/models/Data.ts index 4d8e305..440a5ee 100644 --- a/models/Data.ts +++ b/models/Data.ts @@ -16,7 +16,7 @@ export interface Data { * * {@link https://developer.apple.com/documentation/appstoreservernotifications/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * The unique identifier of an app in the App Store. @@ -58,7 +58,7 @@ export interface Data { * * {@link https://developer.apple.com/documentation/appstoreservernotifications/status status} **/ - status?: Status + status?: Status | number } diff --git a/models/DeliveryStatus.ts b/models/DeliveryStatus.ts index d41f2ea..032e7f5 100644 --- a/models/DeliveryStatus.ts +++ b/models/DeliveryStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates whether the app successfully delivered an in-app purchase that works properly. @@ -16,8 +16,4 @@ export enum DeliveryStatus { DID_NOT_DELIVER_FOR_OTHER_REASON = 5, } -export class DeliveryStatusValidator implements Validator { - validate(obj: any): obj is DeliveryStatus { - return Object.values(DeliveryStatus).includes(obj) - } -} +export class DeliveryStatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/Environment.ts b/models/Environment.ts index 9be7b41..d40faf6 100644 --- a/models/Environment.ts +++ b/models/Environment.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * The server environment, either sandbox or production. @@ -14,8 +14,4 @@ export enum Environment { LOCAL_TESTING = "LocalTesting", } -export class EnvironmentValidator implements Validator { - validate(obj: any): obj is Environment { - return Object.values(Environment).includes(obj) - } -} +export class EnvironmentValidator extends StringValidator {} \ No newline at end of file diff --git a/models/ExpirationIntent.ts b/models/ExpirationIntent.ts index 7ee41bd..46f5501 100644 --- a/models/ExpirationIntent.ts +++ b/models/ExpirationIntent.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The reason an auto-renewable subscription expired. @@ -15,8 +15,4 @@ export enum ExpirationIntent { OTHER = 5, } -export class ExpirationIntentValidator implements Validator { - validate(obj: any): obj is ExpirationIntent { - return Object.values(ExpirationIntent).includes(obj) - } -} +export class ExpirationIntentValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/ExtendReasonCode.ts b/models/ExtendReasonCode.ts index 7cf42f9..9e0db5e 100644 --- a/models/ExtendReasonCode.ts +++ b/models/ExtendReasonCode.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The code that represents the reason for the subscription-renewal-date extension. @@ -14,8 +14,4 @@ export enum ExtendReasonCode { SERVICE_ISSUE_OR_OUTAGE = 3, } -export class ExtendReasonCodeValidator implements Validator { - validate(obj: any): obj is ExtendReasonCode { - return Object.values(ExtendReasonCode).includes(obj) - } -} +export class ExtendReasonCodeValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/FirstSendAttemptResult.ts b/models/FirstSendAttemptResult.ts index dc77d2c..a3df980 100644 --- a/models/FirstSendAttemptResult.ts +++ b/models/FirstSendAttemptResult.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator, Validator } from "./Validator"; /** * An error or result that the App Store server receives when attempting to send an App Store server notification to your server. @@ -21,8 +21,4 @@ export enum FirstSendAttemptResult { OTHER = "OTHER", } -export class FirstSendAttemptResultValidator implements Validator { - validate(obj: any): obj is FirstSendAttemptResult { - return Object.values(FirstSendAttemptResult).includes(obj) - } -} +export class FirstSendAttemptResultValidator extends StringValidator {} \ No newline at end of file diff --git a/models/HistoryResponse.ts b/models/HistoryResponse.ts index 9488993..d81ed55 100644 --- a/models/HistoryResponse.ts +++ b/models/HistoryResponse.ts @@ -42,7 +42,7 @@ export interface HistoryResponse { * * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. diff --git a/models/InAppOwnershipType.ts b/models/InAppOwnershipType.ts index 28fbff6..fb8a71e 100644 --- a/models/InAppOwnershipType.ts +++ b/models/InAppOwnershipType.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * The relationship of the user with the family-shared purchase to which they have access. @@ -12,8 +12,4 @@ export enum InAppOwnershipType { PURCHASED = "PURCHASED", } -export class InAppOwnershipTypeValidator implements Validator { - validate(obj: any): obj is InAppOwnershipType { - return Object.values(InAppOwnershipType).includes(obj) - } -} +export class InAppOwnershipTypeValidator extends StringValidator {} \ No newline at end of file diff --git a/models/JWSRenewalInfoDecodedPayload.ts b/models/JWSRenewalInfoDecodedPayload.ts index 77d13a9..af9e552 100644 --- a/models/JWSRenewalInfoDecodedPayload.ts +++ b/models/JWSRenewalInfoDecodedPayload.ts @@ -20,7 +20,7 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/expirationintent expirationIntent} **/ - expirationIntent?: ExpirationIntent + expirationIntent?: ExpirationIntent | number /** * The original transaction identifier of a purchase. @@ -48,7 +48,7 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus autoRenewStatus} **/ - autoRenewStatus?: AutoRenewStatus + autoRenewStatus?: AutoRenewStatus | number /** * A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription. @@ -62,7 +62,7 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus priceIncreaseStatus} **/ - priceIncreaseStatus?: PriceIncreaseStatus + priceIncreaseStatus?: PriceIncreaseStatus | number /** * The time when the billing grace period for subscription renewals expires. @@ -76,7 +76,7 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/offertype offerType} **/ - offerType?: OfferType + offerType?: OfferType | number /** * The identifier that contains the promo code or the promotional offer identifier. @@ -97,7 +97,7 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * 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. diff --git a/models/JWSTransactionDecodedPayload.ts b/models/JWSTransactionDecodedPayload.ts index cdca270..98840bd 100644 --- a/models/JWSTransactionDecodedPayload.ts +++ b/models/JWSTransactionDecodedPayload.ts @@ -92,7 +92,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/type type} **/ - type?: Type + type?: Type | string /** * The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction. @@ -106,7 +106,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype inAppOwnershipType} **/ - inAppOwnershipType?: InAppOwnershipType + inAppOwnershipType?: InAppOwnershipType | string /** * The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. @@ -120,7 +120,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/revocationreason revocationReason} **/ - revocationReason?: RevocationReason + revocationReason?: RevocationReason | number /** * The UNIX time, in milliseconds, that Apple Support refunded a transaction. @@ -141,7 +141,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/offertype offerType} **/ - offerType?: OfferType + offerType?: OfferType | number /** * The identifier that contains the promo code or the promotional offer identifier. @@ -155,7 +155,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * The three-letter code that represents the country or region associated with the App Store storefront for the purchase. @@ -176,7 +176,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/transactionreason transactionReason} **/ - transactionReason?: TransactionReason + transactionReason?: TransactionReason | string /** * The three-letter ISO 4217 currency code for the price of the product. @@ -197,7 +197,7 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType} **/ - offerDiscountType?: OfferDiscountType + offerDiscountType?: OfferDiscountType | string } diff --git a/models/LastTransactionsItem.ts b/models/LastTransactionsItem.ts index b3160fc..d75d1c7 100644 --- a/models/LastTransactionsItem.ts +++ b/models/LastTransactionsItem.ts @@ -15,7 +15,7 @@ export interface LastTransactionsItem { * * {@link https://developer.apple.com/documentation/appstoreserverapi/status status} **/ - status?: Status + status?: Status | number /** * The original transaction identifier of a purchase. diff --git a/models/LifetimeDollarsPurchased.ts b/models/LifetimeDollarsPurchased.ts index 498b1d6..cb2e779 100644 --- a/models/LifetimeDollarsPurchased.ts +++ b/models/LifetimeDollarsPurchased.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. @@ -18,8 +18,4 @@ export enum LifetimeDollarsPurchased { TWO_THOUSAND_DOLLARS_OR_GREATER = 7, } -export class LifetimeDollarsPurchasedValidator implements Validator { - validate(obj: any): obj is LifetimeDollarsPurchased { - return Object.values(LifetimeDollarsPurchased).includes(obj) - } -} +export class LifetimeDollarsPurchasedValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/LifetimeDollarsRefunded.ts b/models/LifetimeDollarsRefunded.ts index f01fe9c..be543c7 100644 --- a/models/LifetimeDollarsRefunded.ts +++ b/models/LifetimeDollarsRefunded.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms. @@ -18,8 +18,4 @@ export enum LifetimeDollarsRefunded { TWO_THOUSAND_DOLLARS_OR_GREATER = 7, } -export class LifetimeDollarsRefundedValidator implements Validator { - validate(obj: any): obj is LifetimeDollarsRefunded { - return Object.values(LifetimeDollarsRefunded).includes(obj) - } -} +export class LifetimeDollarsRefundedValidator extends NumberValidator {} diff --git a/models/NotificationTypeV2.ts b/models/NotificationTypeV2.ts index 8103e8c..1f0ea5e 100644 --- a/models/NotificationTypeV2.ts +++ b/models/NotificationTypeV2.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator, Validator } from "./Validator"; /** * A notification type value that App Store Server Notifications V2 uses. @@ -27,9 +27,4 @@ export enum NotificationTypeV2 { REFUND_REVERSED = "REFUND_REVERSED", } -export class NotificationTypeV2Validator implements Validator { - validate(obj: any): obj is NotificationTypeV2 { - return Object.values(NotificationTypeV2).includes(obj) - } - } - \ No newline at end of file +export class NotificationTypeV2Validator extends StringValidator {} \ No newline at end of file diff --git a/models/OfferDiscountType.ts b/models/OfferDiscountType.ts index 30dfa8e..a60ad8b 100644 --- a/models/OfferDiscountType.ts +++ b/models/OfferDiscountType.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * The payment mode you configure for an introductory offer, promotional offer, or offer code on an auto-renewable subscription. @@ -13,8 +13,4 @@ export enum OfferDiscountType { PAY_UP_FRONT = "PAY_UP_FRONT" } -export class OfferDiscountTypeValidator implements Validator { - validate(obj: any): obj is OfferDiscountType { - return Object.values(OfferDiscountType).includes(obj) - } -} +export class OfferDiscountTypeValidator extends StringValidator {} \ No newline at end of file diff --git a/models/OfferType.ts b/models/OfferType.ts index 2ce1985..f068d89 100644 --- a/models/OfferType.ts +++ b/models/OfferType.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The type of subscription offer. @@ -13,8 +13,4 @@ export enum OfferType { SUBSCRIPTION_OFFER_CODE = 3, } -export class OfferTypeValidator implements Validator { - validate(obj: any): obj is OfferType { - return Object.values(OfferType).includes(obj) - } -} +export class OfferTypeValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/OrderLookupResponse.ts b/models/OrderLookupResponse.ts index 90b8ca5..9138a63 100644 --- a/models/OrderLookupResponse.ts +++ b/models/OrderLookupResponse.ts @@ -14,7 +14,7 @@ export interface OrderLookupResponse { * * {@link https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus OrderLookupStatus} **/ - status?: OrderLookupStatus + status?: OrderLookupStatus | number /** * An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. diff --git a/models/OrderLookupStatus.ts b/models/OrderLookupStatus.ts index 4c744a0..10ffd50 100644 --- a/models/OrderLookupStatus.ts +++ b/models/OrderLookupStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates whether the order ID in the request is valid for your app. @@ -12,8 +12,4 @@ export enum OrderLookupStatus { INVALID = 1, } -export class OrderLookupStatusValidator implements Validator { - validate(obj: any): obj is OrderLookupStatus { - return Object.values(OrderLookupStatus).includes(obj) - } -} +export class OrderLookupStatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/Platform.ts b/models/Platform.ts index ede2701..b7abeac 100644 --- a/models/Platform.ts +++ b/models/Platform.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The platform on which the customer consumed the in-app purchase. @@ -13,8 +13,4 @@ export enum Platform { NON_APPLE = 2, } -export class PlatformValidator implements Validator { - validate(obj: any): obj is Platform { - return Object.values(Platform).includes(obj) - } -} +export class PlatformValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/PlayTime.ts b/models/PlayTime.ts index 68540cd..6ea85da 100644 --- a/models/PlayTime.ts +++ b/models/PlayTime.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * A value that indicates the amount of time that the customer used the app. @@ -18,8 +18,4 @@ export enum PlayTime { OVER_SIXTEEN_DAYS = 7, } -export class PlayTimeValidator implements Validator { - validate(obj: any): obj is PlayTime { - return Object.values(PlayTime).includes(obj) - } -} +export class PlayTimeValidator extends NumberValidator {} diff --git a/models/PriceIncreaseStatus.ts b/models/PriceIncreaseStatus.ts index 71f1fba..00559fe 100644 --- a/models/PriceIncreaseStatus.ts +++ b/models/PriceIncreaseStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The status that indicates whether an auto-renewable subscription is subject to a price increase. @@ -12,8 +12,4 @@ export enum PriceIncreaseStatus { CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1, } -export class PriceIncreaseStatusValidator implements Validator { - validate(obj: any): obj is PriceIncreaseStatus { - return Object.values(PriceIncreaseStatus).includes(obj) - } -} +export class PriceIncreaseStatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/ResponseBodyV2DecodedPayload.ts b/models/ResponseBodyV2DecodedPayload.ts index ca100e2..4a65bdd 100644 --- a/models/ResponseBodyV2DecodedPayload.ts +++ b/models/ResponseBodyV2DecodedPayload.ts @@ -19,14 +19,14 @@ export interface ResponseBodyV2DecodedPayload extends DecodedSignedData { * * {@link https://developer.apple.com/documentation/appstoreservernotifications/notificationtype notificationType} **/ - notificationType?: NotificationTypeV2; + notificationType?: NotificationTypeV2 | string; /** * Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. * * {@link https://developer.apple.com/documentation/appstoreservernotifications/subtype subtype} **/ - subtype?: Subtype + subtype?: Subtype | string /** * A unique identifier for the notification. diff --git a/models/RevocationReason.ts b/models/RevocationReason.ts index b99bed5..430a6f1 100644 --- a/models/RevocationReason.ts +++ b/models/RevocationReason.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The reason for a refunded transaction. @@ -12,8 +12,4 @@ export enum RevocationReason { REFUNDED_FOR_OTHER_REASON = 0, } -export class RevocationReasonValidator implements Validator { - validate(obj: any): obj is RevocationReason { - return Object.values(RevocationReason).includes(obj) - } -} +export class RevocationReasonValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/SendAttemptItem.ts b/models/SendAttemptItem.ts index e1bab4f..b5d3ab0 100644 --- a/models/SendAttemptItem.ts +++ b/models/SendAttemptItem.ts @@ -22,7 +22,7 @@ export interface SendAttemptItem { * * {@link https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult sendAttemptResult} **/ - sendAttemptResult?: SendAttemptResult + sendAttemptResult?: SendAttemptResult | string } export class SendAttemptItemValidator implements Validator { diff --git a/models/SendAttemptResult.ts b/models/SendAttemptResult.ts index 20f6bbc..fdfb434 100644 --- a/models/SendAttemptResult.ts +++ b/models/SendAttemptResult.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * The success or error information the App Store server records when it attempts to send an App Store server notification to your server. @@ -21,8 +21,4 @@ export enum SendAttemptResult { OTHER = "OTHER", } -export class SendAttemptResultValidator implements Validator { - validate(obj: any): obj is SendAttemptResult { - return Object.values(SendAttemptResult).includes(obj) - } -} +export class SendAttemptResultValidator extends StringValidator {} diff --git a/models/Status.ts b/models/Status.ts index d7e1002..a8452f4 100644 --- a/models/Status.ts +++ b/models/Status.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The status of an auto-renewable subscription. @@ -15,8 +15,4 @@ export enum Status { REVOKED = 5, } -export class StatusValidator implements Validator { - validate(obj: any): obj is Status { - return Object.values(Status).includes(obj) - } -} +export class StatusValidator extends NumberValidator {} \ No newline at end of file diff --git a/models/StatusResponse.ts b/models/StatusResponse.ts index 6f8d2cc..99aba6e 100644 --- a/models/StatusResponse.ts +++ b/models/StatusResponse.ts @@ -15,7 +15,7 @@ export interface StatusResponse { * * {@link https://developer.apple.com/documentation/appstoreserverapi/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * The bundle identifier of an app. diff --git a/models/Subtype.ts b/models/Subtype.ts index a855156..4d33871 100644 --- a/models/Subtype.ts +++ b/models/Subtype.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * A notification subtype value that App Store Server Notifications 2 uses. @@ -26,8 +26,4 @@ export enum Subtype { FAILURE = "FAILURE", } -export class SubtypeValidator implements Validator { - validate(obj: any): obj is Subtype { - return Object.values(Subtype).includes(obj) - } -} +export class SubtypeValidator extends StringValidator {} diff --git a/models/Summary.ts b/models/Summary.ts index a6b676d..08ff278 100644 --- a/models/Summary.ts +++ b/models/Summary.ts @@ -14,7 +14,7 @@ export interface Summary { * * {@link https://developer.apple.com/documentation/appstoreservernotifications/environment environment} **/ - environment?: Environment + environment?: Environment | string /** * The unique identifier of an app in the App Store. diff --git a/models/TransactionHistoryRequest.ts b/models/TransactionHistoryRequest.ts index 3322775..61de821 100644 --- a/models/TransactionHistoryRequest.ts +++ b/models/TransactionHistoryRequest.ts @@ -23,12 +23,12 @@ export interface TransactionHistoryRequest { * * {@link https://developer.apple.com/documentation/appstoreserverapi/productid productId} */ - productIds?: [string] + productIds?: string[] /** * An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType. */ - productTypes?: [ProductType] + productTypes?: ProductType[] /** * 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. @@ -40,7 +40,7 @@ export interface TransactionHistoryRequest { * * {@link https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier subscriptionGroupIdentifier} */ - subscriptionGroupIdentifiers?: [string] + subscriptionGroupIdentifiers?: string[] /** * An optional filter that limits the transaction history by the in-app ownership type. diff --git a/models/TransactionReason.ts b/models/TransactionReason.ts index ab2b1a9..9781487 100644 --- a/models/TransactionReason.ts +++ b/models/TransactionReason.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * 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. @@ -12,8 +12,4 @@ export enum TransactionReason { RENEWAL = "RENEWAL", } -export class TransactionReasonValidator implements Validator { - validate(obj: any): obj is TransactionReason { - return Object.values(TransactionReason).includes(obj) - } -} +export class TransactionReasonValidator extends StringValidator {} diff --git a/models/Type.ts b/models/Type.ts index 07f521e..25e8f43 100644 --- a/models/Type.ts +++ b/models/Type.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { StringValidator } from "./Validator"; /** * The type of in-app purchase products you can offer in your app. @@ -14,8 +14,4 @@ export enum Type { NON_RENEWING_SUBSCRIPTION ="Non-Renewing Subscription", } -export class TypeValidator implements Validator { - validate(obj: any): obj is Type { - return Object.values(Type).includes(obj) - } -} +export class TypeValidator extends StringValidator {} \ No newline at end of file diff --git a/models/UserStatus.ts b/models/UserStatus.ts index 3a65d9e..5b93614 100644 --- a/models/UserStatus.ts +++ b/models/UserStatus.ts @@ -1,6 +1,6 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. -import { Validator } from "./Validator"; +import { NumberValidator } from "./Validator"; /** * The status of a customer’s account within your app. @@ -15,8 +15,4 @@ export enum UserStatus { LIMITED_ACCESS = 4, } -export class UserStatusValidator implements Validator { - validate(obj: any): obj is UserStatus { - return Object.values(UserStatus).includes(obj) - } -} +export class UserStatusValidator extends NumberValidator {} diff --git a/models/Validator.ts b/models/Validator.ts index 2bc14f6..4a39bc7 100644 --- a/models/Validator.ts +++ b/models/Validator.ts @@ -2,4 +2,16 @@ export interface Validator { validate(obj: any): obj is T -} \ No newline at end of file +} + +export class NumberValidator implements Validator { + validate(obj: any): obj is number { + return typeof obj === 'number' + } + } + + export class StringValidator implements Validator { + validate(obj: any): obj is string { + return typeof obj === "string" || obj instanceof String + } + } \ No newline at end of file diff --git a/tests/unit-tests/api_client.test.ts b/tests/unit-tests/api_client.test.ts new file mode 100644 index 0000000..ebd7c0d --- /dev/null +++ b/tests/unit-tests/api_client.test.ts @@ -0,0 +1,458 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +import { AccountTenure } from "../../models/AccountTenure"; +import { ConsumptionRequest } from "../../models/ConsumptionRequest"; +import { ConsumptionStatus } from "../../models/ConsumptionStatus"; +import { DeliveryStatus } from "../../models/DeliveryStatus"; +import { Environment } from "../../models/Environment"; +import { LifetimeDollarsPurchased } from "../../models/LifetimeDollarsPurchased"; +import { LifetimeDollarsRefunded } from "../../models/LifetimeDollarsRefunded"; +import { NotificationTypeV2 } from "../../models/NotificationTypeV2"; +import { Platform } from "../../models/Platform"; +import { PlayTime } from "../../models/PlayTime"; +import { Status } from "../../models/Status"; +import { Subtype } from "../../models/Subtype"; +import { UserStatus } from "../../models/UserStatus"; +import { readFile } from "../util" +import { InAppOwnershipType } from "../../models/InAppOwnershipType"; +import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index"; +import { Response } from "node-fetch"; + +import jsonwebtoken = require('jsonwebtoken'); + +type callbackType = (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => void + +class AppStoreServerAPIClientForTest extends AppStoreServerAPIClient { + + private callback: callbackType + private body: string + private statusCode: number + + public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string, environment: Environment, callback: callbackType, body: string, statusCode: number) { + super(signingKey, keyId, issuerId, bundleId, environment) + this.callback = callback + this.body = body + this.statusCode = statusCode + } + protected async makeFetchRequest(path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }): Promise { + expect(headers['Content-Type']).toBe(typeof stringBody !== 'undefined' ? 'application/json' : undefined) + expect('application/json').toBe(headers['Accept']) + expect(headers['Authorization']).toMatch(/^Bearer .+/) + const token = headers['Authorization'].substring(7) + const decodedToken = jsonwebtoken.decode(token) as jsonwebtoken.JwtPayload + expect(decodedToken['bid']).toBe('bundleId') + expect(decodedToken['aud']).toBe('appstoreconnect-v1') + expect(decodedToken['iss']).toBe('issuerId') + expect(headers['User-Agent']).toMatch(/^app-store-server-library\/node\/.+/) + this.callback(path, parsedQueryParameters, method, stringBody, headers) + return Promise.resolve(new Response(this.body, { + status: this.statusCode + })) + } +} + +function getClientWithBody(path: string, callback: callbackType, statusCode: number = 200): AppStoreServerAPIClient { + const body = readFile(path) + return getAppStoreServerAPIClient(body, statusCode, callback) +} + +function getAppStoreServerAPIClient(body: string, statusCode: number, callback: callbackType): AppStoreServerAPIClient { + const key = readFile('tests/resources/certs/testSigningKey.p8') + return new AppStoreServerAPIClientForTest(key, "keyId", "issuerId", "bundleId", Environment.LOCAL_TESTING, callback, body, statusCode) +} + +describe('The api client ', () => { + + it('calls extendRenewalDateForAllActiveSubscribers', async () => { + const client = getClientWithBody("tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/inApps/v1/subscriptions/extend/mass").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(stringBody).toBeTruthy() + const body = JSON.parse(stringBody!) + expect(45).toBe(body.extendByDays) + expect(1).toBe(body.extendReasonCode) + expect("fdf964a4-233b-486c-aac1-97d8d52688ac").toBe(body.requestIdentifier) + expect(["USA", "MEX"]).toStrictEqual(body.storefrontCountryCodes) + expect("com.example.productId").toBe(body.productId) + }); + + const extendRenewalDateRequest: MassExtendRenewalDateRequest = { + extendByDays: 45, + extendReasonCode: ExtendReasonCode.CUSTOMER_SATISFACTION, + requestIdentifier: "fdf964a4-233b-486c-aac1-97d8d52688ac", + storefrontCountryCodes: ["USA", "MEX"], + productId: "com.example.productId" + } + + const massExtendRenewalDateResponse = await client.extendRenewalDateForAllActiveSubscribers(extendRenewalDateRequest); + + expect(massExtendRenewalDateResponse).toBeTruthy() + expect("758883e8-151b-47b7-abd0-60c4d804c2f5").toBe(massExtendRenewalDateResponse.requestIdentifier) + }) + + it('calls extendSubscriptionRenewalDate', async () => { + const client = getClientWithBody("tests/resources/models/extendSubscriptionRenewalDateResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("PUT").toBe(method) + expect("/inApps/v1/subscriptions/extend/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(stringBody).toBeTruthy() + const body = JSON.parse(stringBody!) + expect(45).toBe(body.extendByDays) + expect(1).toBe(body.extendReasonCode) + expect("fdf964a4-233b-486c-aac1-97d8d52688ac").toBe(body.requestIdentifier) + }); + + const extendRenewalDateRequest: ExtendRenewalDateRequest = { + extendByDays: 45, + extendReasonCode: ExtendReasonCode.CUSTOMER_SATISFACTION, + requestIdentifier: "fdf964a4-233b-486c-aac1-97d8d52688ac" + } + + const extendRenewalDateResponse = await client.extendSubscriptionRenewalDate("4124214", extendRenewalDateRequest); + + expect(extendRenewalDateResponse).toBeTruthy() + expect("2312412").toBe(extendRenewalDateResponse.originalTransactionId) + expect("9993").toBe(extendRenewalDateResponse.webOrderLineItemId) + expect(extendRenewalDateResponse.success).toBe(true) + expect(1698148900000).toBe(extendRenewalDateResponse.effectiveDate) + }) + + it('calls getAllSubscriptionStatuses', async () => { + const client = getClientWithBody("tests/resources/models/getAllSubscriptionStatusesResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/subscriptions/4321").toBe(path) + expect(["2", "1"]).toStrictEqual(parsedQueryParameters.getAll("status")) + expect(stringBody).toBeUndefined() + }); + + const statusResponse = await client.getAllSubscriptionStatuses("4321", [Status.EXPIRED, Status.ACTIVE]); + + expect(statusResponse).toBeTruthy() + expect(Environment.LOCAL_TESTING).toBe(statusResponse.environment) + expect("com.example").toBe(statusResponse.bundleId) + expect(5454545).toBe(statusResponse.appAppleId) + + const item = [ + { + subscriptionGroupIdentifier: 'sub_group_one', + lastTransactions: [ + { + status: Status.ACTIVE, + originalTransactionId: "3749183", + signedTransactionInfo: "signed_transaction_one", + signedRenewalInfo: "signed_renewal_one" + }, + { + status: Status.REVOKED, + originalTransactionId: "5314314134", + signedTransactionInfo: "signed_transaction_two", + signedRenewalInfo: "signed_renewal_two" + } + ] + }, + { + subscriptionGroupIdentifier: "sub_group_two", + lastTransactions: [ + { + status: Status.EXPIRED, + originalTransactionId: "3413453", + signedTransactionInfo: "signed_transaction_three", + signedRenewalInfo: "signed_renewal_three" + } + ] + } + ] + expect(statusResponse.data).toStrictEqual(item) + }) + + it('calls getRefundHistory', async () => { + const client = getClientWithBody("tests/resources/models/getRefundHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v2/refund/lookup/555555").toBe(path) + expect("revision_input").toBe(parsedQueryParameters.get("revision")) + expect(stringBody).toBeUndefined() + }); + + const refundHistoryResponse = await client.getRefundHistory("555555", "revision_input"); + + expect(refundHistoryResponse).toBeTruthy() + expect(["signed_transaction_one", "signed_transaction_two"]).toStrictEqual(refundHistoryResponse.signedTransactions) + expect("revision_output").toBe(refundHistoryResponse.revision) + expect(refundHistoryResponse.hasMore).toBe(true) + }) + + it('calls getStatusOfSubscriptionRenewalDateExtensions', async () => { + const client = getClientWithBody("tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/subscriptions/extend/mass/20fba8a0-2b80-4a7d-a17f-85c1854727f8/com.example.product").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }); + + const massExtendRenewalDateStatusResponse = await client.getStatusOfSubscriptionRenewalDateExtensions("com.example.product", "20fba8a0-2b80-4a7d-a17f-85c1854727f8"); + + expect(massExtendRenewalDateStatusResponse).toBeTruthy() + expect("20fba8a0-2b80-4a7d-a17f-85c1854727f8").toBe(massExtendRenewalDateStatusResponse.requestIdentifier) + expect(massExtendRenewalDateStatusResponse.complete).toBe(true) + expect(1698148900000).toBe(massExtendRenewalDateStatusResponse.completeDate) + expect(30).toBe(massExtendRenewalDateStatusResponse.succeededCount) + expect(2).toBe(massExtendRenewalDateStatusResponse.failedCount) + }) + + it('calls getTestNotificationStatus', async () => { + const client = getClientWithBody("tests/resources/models/getTestNotificationStatusResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/notifications/test/8cd2974c-f905-492a-bf9a-b2f47c791d19").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }); + + const checkTestNotificationResponse = await client.getTestNotificationStatus("8cd2974c-f905-492a-bf9a-b2f47c791d19"); + + expect(checkTestNotificationResponse).toBeTruthy(); + expect("signed_payload").toBe(checkTestNotificationResponse.signedPayload) + const sendAttemptItems = [ + { + attemptDate: 1698148900000, + sendAttemptResult: SendAttemptResult.NO_RESPONSE + }, + { + attemptDate: 1698148950000, + sendAttemptResult: SendAttemptResult.SUCCESS + } + ] + expect(sendAttemptItems).toStrictEqual(checkTestNotificationResponse.sendAttempts) + }) + + it('calls getNotificationHistoryResponse', async () => { + const client = getClientWithBody("tests/resources/models/getNotificationHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/inApps/v1/notifications/history").toBe(path) + expect("a036bc0e-52b8-4bee-82fc-8c24cb6715d6").toBe(parsedQueryParameters.get("paginationToken")) + expect(stringBody).toBeTruthy() + const body = JSON.parse(stringBody!) + expect(1698148900000).toBe(body.startDate) + expect(1698148950000).toBe(body.endDate) + expect("SUBSCRIBED").toBe(body.notificationType) + expect("INITIAL_BUY").toBe(body.notificationSubtype) + expect("999733843").toBe(body.transactionId) + expect(body.onlyFailures).toBe(true); + }); + + const notificationHistoryRequest: NotificationHistoryRequest = { + startDate: 1698148900000, + endDate: 1698148950000, + notificationType: NotificationTypeV2.SUBSCRIBED, + notificationSubtype: Subtype.INITIAL_BUY, + transactionId: "999733843", + onlyFailures: true + } + + const notificationHistoryResponse = await client.getNotificationHistory("a036bc0e-52b8-4bee-82fc-8c24cb6715d6", notificationHistoryRequest); + + expect(notificationHistoryResponse).toBeTruthy() + expect("57715481-805a-4283-8499-1c19b5d6b20a").toBe(notificationHistoryResponse.paginationToken) + expect(notificationHistoryResponse.hasMore).toBe(true) + const expectedNotificationHistory: NotificationHistoryResponseItem[] = [ + { + sendAttempts: [ + { + attemptDate: 1698148900000, + sendAttemptResult: SendAttemptResult.NO_RESPONSE + }, + { + attemptDate: 1698148950000, + sendAttemptResult: SendAttemptResult.SUCCESS + } + ], + signedPayload: "signed_payload_one" + }, + { + sendAttempts: [ + { + attemptDate: 1698148800000, + sendAttemptResult: SendAttemptResult.CIRCULAR_REDIRECT + } + ], + signedPayload: "signed_payload_two" + } + ] + expect(expectedNotificationHistory).toStrictEqual(notificationHistoryResponse.notificationHistory) + }) + + it('calls getTransactionHistory', async () => { + const client = getClientWithBody("tests/resources/models/transactionHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/history/1234").toBe(path) + expect("revision_input").toBe(parsedQueryParameters.get("revision")) + expect("123455").toBe(parsedQueryParameters.get("startDate")) + expect("123456").toBe(parsedQueryParameters.get("endDate")) + expect(["com.example.1", "com.example.2"]).toStrictEqual(parsedQueryParameters.getAll("productId")) + expect(["CONSUMABLE", "AUTO_RENEWABLE"]).toStrictEqual(parsedQueryParameters.getAll("productType")) + expect("ASCENDING").toBe(parsedQueryParameters.get("sort")) + expect(["sub_group_id", "sub_group_id_2"]).toStrictEqual(parsedQueryParameters.getAll("subscriptionGroupIdentifier")) + expect("FAMILY_SHARED").toBe(parsedQueryParameters.get("inAppOwnershipType")) + expect("false").toBe(parsedQueryParameters.get("revoked")) + expect(stringBody).toBeUndefined() + }); + + const request: TransactionHistoryRequest = { + sort: Order.ASCENDING, + productTypes: [ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], + endDate: 123456, + startDate: 123455, + revoked: false, + inAppOwnershipType: InAppOwnershipType.FAMILY_SHARED, + productIds: ["com.example.1", "com.example.2"], + subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"] + } + + const historyResponse = await client.getTransactionHistory("1234", "revision_input", request); + + expect(historyResponse).toBeTruthy() + expect("revision_output").toBe(historyResponse.revision) + expect(historyResponse.hasMore).toBe(true) + expect("com.example").toBe(historyResponse.bundleId) + expect(323232).toBe(historyResponse.appAppleId) + expect(Environment.LOCAL_TESTING).toBe(historyResponse.environment) + expect(["signed_transaction_value", "signed_transaction_value2"]).toStrictEqual(historyResponse.signedTransactions) + }) + + it('calls getTransactionInfo', async () => { + const client = getClientWithBody("tests/resources/models/transactionInfoResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/transactions/1234").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }); + + const transactionInfoResponse = await client.getTransactionInfo("1234"); + + expect(transactionInfoResponse).toBeTruthy() + expect("signed_transaction_info_value").toBe(transactionInfoResponse.signedTransactionInfo) + }) + + it('calls lookUpOrderId', async () => { + const client = getClientWithBody("tests/resources/models/lookupOrderIdResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/lookup/W002182").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }); + + const orderLookupResponse = await client.lookUpOrderId("W002182"); + + expect(orderLookupResponse).toBeTruthy() + expect(OrderLookupStatus.INVALID).toBe(orderLookupResponse.status) + expect(["signed_transaction_one", "signed_transaction_two"]).toStrictEqual(orderLookupResponse.signedTransactions) + }) + + it('calls requestTestNotification', async () => { + const client = getClientWithBody("tests/resources/models/requestTestNotificationResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/inApps/v1/notifications/test").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }); + + const sendTestNotificationResponse = await client.requestTestNotification(); + + expect(sendTestNotificationResponse).toBeTruthy() + expect("ce3af791-365e-4c60-841b-1674b43c1609").toBe(sendTestNotificationResponse.testNotificationToken) + }) + + it('calls sendConsumptionData', async () => { + const client = getAppStoreServerAPIClient("", 200, (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("PUT").toBe(method) + expect("/inApps/v1/transactions/consumption/49571273").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(stringBody).toBeTruthy() + const body = JSON.parse(stringBody!) + expect(body.customerConsented).toBe(true) + expect(1).toBe(body.consumptionStatus) + expect(2).toBe(body.platform) + expect(body.sampleContentProvided).toBe(false) + expect(3).toBe(body.deliveryStatus) + expect("7389a31a-fb6d-4569-a2a6-db7d85d84813").toBe(body.appAccountToken) + expect(4).toBe(body.accountTenure) + expect(5).toBe(body.playTime) + expect(6).toBe(body.lifetimeDollarsRefunded) + expect(7).toBe(body.lifetimeDollarsPurchased) + expect(4).toBe(body.userStatus) + }); + + const consumptionRequest: ConsumptionRequest = { + customerConsented: true, + consumptionStatus: ConsumptionStatus.NOT_CONSUMED, + platform: Platform.NON_APPLE, + sampleContentProvided: false, + deliveryStatus: DeliveryStatus.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, + appAccountToken: "7389a31a-fb6d-4569-a2a6-db7d85d84813", + accountTenure: AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS, + playTime: PlayTime.ONE_DAY_TO_FOUR_DAYS, + lifetimeDollarsRefunded: LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, + lifetimeDollarsPurchased: LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER, + userStatus: UserStatus.LIMITED_ACCESS + } + + client.sendConsumptionData("49571273", consumptionRequest); + }) + + it('calls getTransactionInfo but receives a general internal error', async () => { + const client = getClientWithBody("tests/resources/models/apiException.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/transactions/1234").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }, 500); + + try { + const transactionInfoResponse = await client.getTransactionInfo("1234"); + fail('this test call is expected to throw') + } catch (e) { + let error = e as APIException + expect(error.httpStatusCode).toBe(500) + expect(error.apiError).toBe(APIError.GENERAL_INTERNAL) + } + }) + + it('calls getTransactionInfo but receives a rate limit exceeded error', async () => { + const client = getClientWithBody("tests/resources/models/apiTooManyRequestsException.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/transactions/1234").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }, 429); + + try { + const transactionInfoResponse = await client.getTransactionInfo("1234"); + fail('this test call is expected to throw') + } catch (e) { + let error = e as APIException + expect(error.httpStatusCode).toBe(429) + expect(error.apiError).toBe(APIError.RATE_LIMIT_EXCEEDED) + } + }) + + it('calls getTransactionInfo but receives an unknown/new error code', async () => { + const client = getClientWithBody("tests/resources/models/apiUnknownError.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => { + expect("GET").toBe(method) + expect("/inApps/v1/transactions/1234").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + expect(stringBody).toBeUndefined() + }, 400); + + try { + const transactionInfoResponse = await client.getTransactionInfo("1234"); + fail('this test call is expected to throw') + } catch (e) { + let error = e as APIException + expect(error.httpStatusCode).toBe(400) + expect(error.apiError).toBe(9990000) + } + }) +}) \ No newline at end of file diff --git a/tests/unit-tests/promotional_offer_signature_creator.test.ts b/tests/unit-tests/promotional_offer_signature_creator.test.ts new file mode 100644 index 0000000..f577ab5 --- /dev/null +++ b/tests/unit-tests/promotional_offer_signature_creator.test.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +import { PromotionalOfferSignatureCreator } from "../../promotional_offer"; +import { readFile } from "../util" + + +describe('Promotional Offer Signature Creation Test', () => { + it('should create a non-null signature', async () => { + const signatureCreator = new PromotionalOfferSignatureCreator(readFile('tests/resources/certs/testSigningKey.p8'), "keyId", "bundleId"); + const signature = signatureCreator.createSignature('productId', 'offerId', 'applicationUsername', "20fba8a0-2b80-4a7d-a17f-85c1854727f8", 1698148900000) + expect(signature).toBeTruthy() + }) +}) \ No newline at end of file