diff --git a/src/http.ts b/src/http.ts index 17f80e65..88b886a3 100644 --- a/src/http.ts +++ b/src/http.ts @@ -58,16 +58,25 @@ export interface MWSOptions { } type HttpMethod = 'GET' | 'POST' -type ParameterTypes = string | number | (number | string)[] | object[] | boolean | undefined +type ParameterTypes = + | string + | number + | (number | string)[] + | object[] + | boolean + | { [key: string]: ParameterTypes } + | undefined + type Parameters = Record type CleanParameters = Record export enum Resource { - Sellers = 'Sellers', + FulfilmentInventory = 'FulfillmentInventory', Orders = 'Orders', Products = 'Products', - FulfilmentInventory = 'FulfillmentInventory', Reports = 'Reports', + Sellers = 'Sellers', + Subscriptions = 'Subscriptions', } interface ResourceActions { @@ -117,6 +126,17 @@ interface ResourceActions { | 'GetReportScheduleListByNextToken' | 'GetReportScheduleCount' | 'UpdateReportAcknowledgements' + [Resource.Subscriptions]: + | 'RegisterDestination' + | 'DeregisterDestination' + | 'ListRegisteredDestinations' + | 'SendTestNotificationToDestination' + | 'CreateSubscription' + | 'GetSubscription' + | 'DeleteSubscription' + | 'ListSubscriptions' + | 'UpdateSubscription' + | 'GetServiceStatus' } interface Request { @@ -173,7 +193,15 @@ export const cleanParameters = (parameters: Parameters): CleanParameters => Object.entries(parameters) .filter(([, parameter]) => parameter !== undefined) .reduce((result, [key, parameter]) => { - if (Array.isArray(parameter)) { + if (typeof parameter === 'string' || !Number.isNaN(Number(parameter))) { + /** + * If parameter is type string or number, assign it to result + */ + Object.assign(result, { [key]: String(parameter) }) + } else if (Array.isArray(parameter)) { + /** + * If parameter is type array reduce it to dotnotation + */ parameter.forEach((parameterChild: string | number | object, index: number) => { if (typeof parameterChild === 'string' || !Number.isNaN(Number(parameterChild))) { Object.assign(result, { [`${key}.${index + 1}`]: String(parameterChild) }) @@ -182,7 +210,14 @@ export const cleanParameters = (parameters: Parameters): CleanParameters => } }) } else { - Object.assign(result, { [key]: String(parameter) }) + /** + * If parameter is type object parameterize it + */ + Object.entries( + cleanParameters(parameter as Parameters), + ).forEach(([innerKey, innerValue]: [string, string]) => + Object.assign(result, { [`${key}.${innerKey}`]: innerValue }), + ) } return result diff --git a/src/index.ts b/src/index.ts index f2823c70..a4451f2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './http' export * from './mws' export * from './sections/sellers' +export * from './sections/subscriptions' export * from './sections/orders' export * from './sections/products/products' export * from './sections/fulfillment-inventory' diff --git a/src/mws.ts b/src/mws.ts index 78f68623..c20f6f6e 100644 --- a/src/mws.ts +++ b/src/mws.ts @@ -4,6 +4,7 @@ import { Orders } from './sections/orders' import { Products } from './sections/products/products' import { Reports } from './sections/reports' import { Sellers } from './sections/sellers' +import { Subscriptions } from './sections/subscriptions' export class MWS { private _sellers!: Sellers @@ -16,6 +17,8 @@ export class MWS { private _reports!: Reports + private _subscriptions!: Subscriptions + constructor(private httpClient: HttpClient) {} get sellers() { @@ -57,4 +60,12 @@ export class MWS { return this._reports } + + get subscriptions() { + if (!this._subscriptions) { + this._subscriptions = new Subscriptions(this.httpClient) + } + + return this._subscriptions + } } diff --git a/src/sections/subscriptions.ts b/src/sections/subscriptions.ts new file mode 100644 index 00000000..99809f58 --- /dev/null +++ b/src/sections/subscriptions.ts @@ -0,0 +1,368 @@ +import { boolean, Codec, enumeration, exactly, GetInterface, string } from 'purify-ts' + +import { ParsingError } from '../error' +import { HttpClient, RequestMeta, Resource } from '../http' +import { ensureArray } from '../parsing' +import { getServiceStatusByResource } from './shared' + +const SUBSCRIPTIONS_API_VERSION = '2013-07-01' + +/** + * Amazon docs list these as the only possible choices for each parameters + */ +export type DeliveryChannel = 'SQS' +export type AttributeKeyValueKeys = 'sqsQueueUrl' +export type NotificationType = + | 'AnyOfferChanged' + | 'FeedProcessingFinished' + | 'FeePromotion' + | 'FulfillmentOrderStatus' + | 'ReportProcessingFinished' + +enum NotificationTypeEnum { + AnyOfferChanged = 'AnyOfferChanged', + FeedProcessingFinished = 'FeedProcessingFinished', + FeePromotion = 'FeePromotion', + FulfillmentOrderStatus = 'FulfillmentOrderStatus', + ReportProcessingFinished = 'ReportProcessingFinished', +} + +const NotificationType = enumeration(NotificationTypeEnum) + +interface MarketplaceIdAndDestinationOnlyParameters { + MarketplaceId: string + Destination: Destination +} + +interface AttributeKeyValue { + Key: AttributeKeyValueKeys + Value: string +} +interface Destination { + DeliveryChannel: DeliveryChannel + AttributeList: AttributeKeyValue[] +} +interface SubscriptionActionParameters { + MarketplaceId: string + NotificationType: NotificationType + Destination: Destination +} + +type RegisterDestinationParameters = MarketplaceIdAndDestinationOnlyParameters + +const RegisterDestinationResponse = Codec.interface({ + RegisterDestinationResponse: Codec.interface({ + RegisterDestinationResult: exactly(''), + }), +}) + +type DeregisterDestinationParameters = MarketplaceIdAndDestinationOnlyParameters + +const DeregisterDestinationResponse = Codec.interface({ + DeregisterDestinationResponse: Codec.interface({ + DeregisterDestinationResult: exactly(''), + }), +}) + +interface ListRegisteredDestinationsParameters { + MarketplaceId: string + [key: string]: string +} +enum AttribueKeyValueKeysEnum { + sqsQueueUrl = 'sqsQueueUrl', +} +const AttribueKeyValueKeys = enumeration(AttribueKeyValueKeysEnum) + +const AttribueKeyValue = Codec.interface({ + Value: string, + Key: AttribueKeyValueKeys, +}) + +enum DeliveryChannelEnum { + SQS = 'SQS', +} + +const DeliveryChannel = enumeration(DeliveryChannelEnum) + +const Destination = Codec.interface({ + DeliveryChannel, + AttributeList: ensureArray('member', AttribueKeyValue), +}) + +const ListRegisteredDestinations = Codec.interface({ + DestinationList: ensureArray('member', Destination), +}) + +type ListRegisteredDestinations = GetInterface + +const ListRegisteredDestinationsResponse = Codec.interface({ + ListRegisteredDestinationsResponse: Codec.interface({ + ListRegisteredDestinationsResult: ListRegisteredDestinations, + }), +}) + +type SendTestNotificationToDestinationParameters = MarketplaceIdAndDestinationOnlyParameters + +const SendTestNotificationToDestinationResponse = Codec.interface({ + SendTestNotificationToDestinationResponse: Codec.interface({ + SendTestNotificationToDestinationResult: exactly(''), + }), +}) + +interface Subscription { + NotificationType: NotificationType + Destination: Destination + IsEnabled: boolean +} + +interface CreateSubscriptionParameters { + MarketplaceId: string + Subscription: Subscription +} + +const CreateSubscriptionResponse = Codec.interface({ + CreateSubscriptionResponse: Codec.interface({ + CreateSubscriptionResult: exactly(''), + }), +}) + +type GetSubscriptionParameters = SubscriptionActionParameters + +const Subscription = Codec.interface({ + NotificationType, + Destination, + IsEnabled: boolean, +}) + +const GetSubscription = Codec.interface({ + Subscription, +}) + +type GetSubscription = GetInterface + +const GetSubscriptionResponse = Codec.interface({ + GetSubscriptionResponse: Codec.interface({ + GetSubscriptionResult: GetSubscription, + }), +}) + +type DeleteSubscriptionParameters = SubscriptionActionParameters + +const DeleteSubscriptionResponse = Codec.interface({ + DeleteSubscriptionResponse: Codec.interface({ + DeleteSubscriptionResult: exactly(''), + }), +}) + +interface UpdateSubscriptionParameters { + MarketplaceId: string + Subscription: Subscription +} + +const UpdateSubscriptionResponse = Codec.interface({ + UpdateSubscriptionResponse: Codec.interface({ + UpdateSubscriptionResult: exactly(''), + }), +}) +export class Subscriptions { + constructor(private httpClient: HttpClient) {} + + async updateSubscription(parameters: UpdateSubscriptionParameters): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'UpdateSubscription', + parameters: { + MarketplaceId: parameters.MarketplaceId, + Subscription: { + NotificationType: parameters.Subscription.NotificationType, + Destination: { + DeliveryChannel: parameters.Subscription.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Subscription.Destination.AttributeList, + }, + IsEnabled: parameters.Subscription.IsEnabled, + }, + }, + }) + + return UpdateSubscriptionResponse.decode(response).caseOf({ + Right: (x) => [x.UpdateSubscriptionResponse.UpdateSubscriptionResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async deleteSubscription(parameters: DeleteSubscriptionParameters): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'DeleteSubscription', + parameters: { + MarketplaceId: parameters.MarketplaceId, + NotificationType: parameters.NotificationType, + Destination: { + DeliveryChannel: parameters.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Destination.AttributeList, + }, + }, + }) + + return DeleteSubscriptionResponse.decode(response).caseOf({ + Right: (x) => [x.DeleteSubscriptionResponse.DeleteSubscriptionResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async getSubscription( + parameters: GetSubscriptionParameters, + ): Promise<[GetSubscription, RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'GetSubscription', + parameters: { + MarketplaceId: parameters.MarketplaceId, + NotificationType: parameters.NotificationType, + Destination: { + DeliveryChannel: parameters.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Destination.AttributeList, + }, + }, + }) + + return GetSubscriptionResponse.decode(response).caseOf({ + Right: (x) => [x.GetSubscriptionResponse.GetSubscriptionResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async createSubscription(parameters: CreateSubscriptionParameters): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'CreateSubscription', + parameters: { + MarketplaceId: parameters.MarketplaceId, + Subscription: { + NotificationType: parameters.Subscription.NotificationType, + Destination: { + DeliveryChannel: parameters.Subscription.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Subscription.Destination.AttributeList, + }, + IsEnabled: parameters.Subscription.IsEnabled, + }, + }, + }) + + return CreateSubscriptionResponse.decode(response).caseOf({ + Right: (x) => [x.CreateSubscriptionResponse.CreateSubscriptionResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async sendTestNotificationToDestination( + parameters: SendTestNotificationToDestinationParameters, + ): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'SendTestNotificationToDestination', + parameters: { + MarketplaceId: parameters.MarketplaceId, + Destination: { + DeliveryChannel: parameters.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Destination.AttributeList, + }, + }, + }) + + return SendTestNotificationToDestinationResponse.decode(response).caseOf({ + Right: (x) => [ + x.SendTestNotificationToDestinationResponse.SendTestNotificationToDestinationResult, + meta, + ], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async listRegisteredDestinations( + parameters: ListRegisteredDestinationsParameters, + ): Promise<[ListRegisteredDestinations, RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'ListRegisteredDestinations', + parameters, + }) + + return ListRegisteredDestinationsResponse.decode(response).caseOf({ + Right: (x) => [x.ListRegisteredDestinationsResponse.ListRegisteredDestinationsResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async deregisterDestination( + parameters: DeregisterDestinationParameters, + ): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'DeregisterDestination', + parameters: { + MarketplaceId: parameters.MarketplaceId, + Destination: { + DeliveryChannel: parameters.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Destination.AttributeList, + }, + }, + }) + + return DeregisterDestinationResponse.decode(response).caseOf({ + Right: (x) => [x.DeregisterDestinationResponse.DeregisterDestinationResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async registerDestination(parameters: RegisterDestinationParameters): Promise<['', RequestMeta]> { + const [response, meta] = await this.httpClient.request('POST', { + resource: Resource.Subscriptions, + version: SUBSCRIPTIONS_API_VERSION, + action: 'RegisterDestination', + parameters: { + MarketplaceId: parameters.MarketplaceId, + Destination: { + DeliveryChannel: parameters.Destination.DeliveryChannel, + 'AttributeList.member': parameters.Destination.AttributeList, + }, + }, + }) + + return RegisterDestinationResponse.decode(response).caseOf({ + Right: (x) => [x.RegisterDestinationResponse.RegisterDestinationResult, meta], + Left: (error) => { + throw new ParsingError(error) + }, + }) + } + + async getServiceStatus() { + return getServiceStatusByResource( + this.httpClient, + Resource.Subscriptions, + SUBSCRIPTIONS_API_VERSION, + ) + } +} diff --git a/test/integration/__recordings__/subscriptions_3417847717/should-be-able-to-query-service-status_2354862117/recording.har b/test/integration/__recordings__/subscriptions_3417847717/should-be-able-to-query-service-status_2354862117/recording.har new file mode 100644 index 00000000..9fd5fef0 --- /dev/null +++ b/test/integration/__recordings__/subscriptions_3417847717/should-be-able-to-query-service-status_2354862117/recording.har @@ -0,0 +1,122 @@ +{ + "log": { + "_recordingName": "subscriptions/should be able to query service status", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "4.2.1" + }, + "entries": [ + { + "_id": "2705f105dea6682c09dee8853cb9884d", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 304, + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "content-type", + "value": "application/x-www-form-urlencoded" + }, + { + "name": "user-agent", + "value": "@scaleleap/amazon-mws-api-sdk/1.0.0 (Language=JavaScript)" + }, + { + "name": "content-length", + "value": 304 + }, + { + "name": "host", + "value": "mws.amazonservices.ca" + } + ], + "headersSize": 285, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "params": [], + "text": "AWSAccessKeyId=x&Action=GetServiceStatus&MWSAuthToken=x&SellerId=x&Signature=py59%2BCQTQrkcSJug585cny9moQh3skeAHEIGRvTcvvQ%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-06-15T10%3A24%3A47.126Z&Version=2013-07-01" + }, + "queryString": [], + "url": "https://mws.amazonservices.ca/Subscriptions/2013-07-01" + }, + "response": { + "bodySize": 388, + "content": { + "mimeType": "text/xml", + "size": 388, + "text": "\n\n \n GREEN\n 2020-06-15T10:24:47.768Z\n \n \n 85280c95-13d8-4c4c-aaea-a50ee579de36\n \n\n" + }, + "cookies": [], + "headers": [ + { + "name": "server", + "value": "Server" + }, + { + "name": "date", + "value": "Mon, 15 Jun 2020 10:24:47 GMT" + }, + { + "name": "content-type", + "value": "text/xml" + }, + { + "name": "content-length", + "value": "388" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "x-mws-request-id", + "value": "85280c95-13d8-4c4c-aaea-a50ee579de36" + }, + { + "name": "x-mws-timestamp", + "value": "2020-06-15T10:24:47.768Z" + }, + { + "name": "x-mws-response-context", + "value": "AQIuAlMWsflB3uM3vpW4Em6NiuF5XMtPFqh/UGpWI+g+YqqtRNjCGbW3VTSRMIJ3LYPAD+EvIf0=" + }, + { + "name": "vary", + "value": "Content-Type,Accept-Encoding,X-Amzn-CDN-Cache,X-Amzn-AX-Treatment,User-Agent" + }, + { + "name": "x-amz-rid", + "value": "410N3QGRTK1M0JXXRYVQ" + } + ], + "headersSize": 437, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2020-06-15T10:24:47.136Z", + "time": 643, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 643 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/test/integration/subscriptions.test.ts b/test/integration/subscriptions.test.ts new file mode 100644 index 00000000..e97edf5c --- /dev/null +++ b/test/integration/subscriptions.test.ts @@ -0,0 +1,19 @@ +import { Subscriptions } from '../../src' +import { Config } from './config' +import { itci } from './it' + +const httpClient = new Config().createHttpClient() + +/* eslint-disable jest/no-standalone-expect */ +describe('subscriptions', () => { + itci('should be able to query service status', async () => { + expect.assertions(1) + + const subscriptions = new Subscriptions(httpClient) + + const [response] = await subscriptions.getServiceStatus() + + expect(response.Status).toMatch(/GREEN|YELLOW|RED/) + }) +}) +/* eslint-enable jest/no-standalone-expect */ diff --git a/test/unit/__fixtures__/subscriptions_create_subscription.xml b/test/unit/__fixtures__/subscriptions_create_subscription.xml new file mode 100644 index 00000000..fc22440e --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_create_subscription.xml @@ -0,0 +1,6 @@ + + + + c9bb2e77-2425-4e1a-9c85-36d00EXAMPLE + + \ No newline at end of file diff --git a/test/unit/__fixtures__/subscriptions_delete_subscription.xml b/test/unit/__fixtures__/subscriptions_delete_subscription.xml new file mode 100644 index 00000000..8d17abfb --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_delete_subscription.xml @@ -0,0 +1,6 @@ + + + + 2d7db8a1-8974-4541-9c9b-f882dEXAMPLE + + \ No newline at end of file diff --git a/test/unit/__fixtures__/subscriptions_deregister_destination.xml b/test/unit/__fixtures__/subscriptions_deregister_destination.xml new file mode 100644 index 00000000..e43b74ba --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_deregister_destination.xml @@ -0,0 +1,7 @@ + + + + + String + + diff --git a/test/unit/__fixtures__/subscriptions_get_subscription.xml b/test/unit/__fixtures__/subscriptions_get_subscription.xml new file mode 100644 index 00000000..e6156472 --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_get_subscription.xml @@ -0,0 +1,22 @@ + + + + AnyOfferChanged + true + + SQS + + + + https://sqs.us-east-1.amazonaws.com/51471EXAMPLE/mws_notifications + + sqsQueueUrl + + + + + + + 4012b1ae-3f31-4627-83c3-1757aEXAMPLE + + \ No newline at end of file diff --git a/test/unit/__fixtures__/subscriptions_list_registered_destinations.xml b/test/unit/__fixtures__/subscriptions_list_registered_destinations.xml new file mode 100644 index 00000000..1765818e --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_list_registered_destinations.xml @@ -0,0 +1,20 @@ + + + + + SQS + + + + https://sqs.us-east-1.amazonaws.com/51471EXAMPLE/mws_notifications + + sqsQueueUrl + + + + + + + 8329b2a1-4249-43fa-b2d3-da563EXAMPLE + + \ No newline at end of file diff --git a/test/unit/__fixtures__/subscriptions_register_destination.xml b/test/unit/__fixtures__/subscriptions_register_destination.xml new file mode 100644 index 00000000..075a0f23 --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_register_destination.xml @@ -0,0 +1,7 @@ + + + + + String + + diff --git a/test/unit/__fixtures__/subscriptions_send_test_notification_to_destination.xml b/test/unit/__fixtures__/subscriptions_send_test_notification_to_destination.xml new file mode 100644 index 00000000..a980646e --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_send_test_notification_to_destination.xml @@ -0,0 +1,6 @@ + + + + f662dae6-bde0-4e75-a53b-741abEXAMPLE + + \ No newline at end of file diff --git a/test/unit/__fixtures__/subscriptions_update_subscription.xml b/test/unit/__fixtures__/subscriptions_update_subscription.xml new file mode 100644 index 00000000..a8396df5 --- /dev/null +++ b/test/unit/__fixtures__/subscriptions_update_subscription.xml @@ -0,0 +1,6 @@ + + + + 3263ad38-d15b-4043-b48c-cbfa2EXAMPLE + + \ No newline at end of file diff --git a/test/unit/__snapshots__/reports.test.ts.snap b/test/unit/__snapshots__/reports.test.ts.snap index 0bd38a9b..5e54641c 100644 --- a/test/unit/__snapshots__/reports.test.ts.snap +++ b/test/unit/__snapshots__/reports.test.ts.snap @@ -396,6 +396,7 @@ Array [ "ReportSchedule": Object { "ReportType": "_GET_ORDERS_DATA_", "Schedule": "_30_DAYS_", + "ScheduledDate": 2009-02-20T02:10:42.000Z, }, }, Object { @@ -415,6 +416,7 @@ Array [ "ReportSchedule": Object { "ReportType": "_GET_ORDERS_DATA_", "Schedule": "_30_DAYS_", + "ScheduledDate": 2009-02-20T02:10:42.000Z, }, }, Object { diff --git a/test/unit/__snapshots__/subscriptions.test.ts.snap b/test/unit/__snapshots__/subscriptions.test.ts.snap new file mode 100644 index 00000000..cd207516 --- /dev/null +++ b/test/unit/__snapshots__/subscriptions.test.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sellers createSubscription returns the standard response if creation is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers deleteSubscription returns the standard response if delete is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers deregisterDestination returns the standard response if deregistration is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers getServiceStatus returns a parsed model when the status response is valid 1`] = ` +Array [ + Object { + "Status": "GREEN", + "Timestamp": "2020-05-06T08:22:23.582Z", + }, + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers getSubscription returns a subscription if succesful 1`] = ` +Array [ + Object { + "Subscription": Object { + "Destination": Object { + "AttributeList": Array [ + Object { + "Key": "sqsQueueUrl", + "Value": "https://sqs.us-east-1.amazonaws.com/51471EXAMPLE/mws_notifications", + }, + ], + "DeliveryChannel": "SQS", + }, + "IsEnabled": true, + "NotificationType": "AnyOfferChanged", + }, + }, + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers listRegisteredDestinations returns a list of destinations if succesful 1`] = ` +Array [ + Object { + "DestinationList": Array [ + Object { + "AttributeList": Array [ + Object { + "Key": "sqsQueueUrl", + "Value": "https://sqs.us-east-1.amazonaws.com/51471EXAMPLE/mws_notifications", + }, + ], + "DeliveryChannel": "SQS", + }, + ], + }, + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers registerDestination returns the standard response if registration is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers sendTestNotificationToDestination returns the standard response if testing is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; + +exports[`sellers updateSubscription returns the standard response if update is succesful 1`] = ` +Array [ + "", + Object { + "quotaMax": 1000, + "quotaRemaining": 999, + "quotaResetOn": 2020-04-06T10:22:23.582Z, + "requestId": "0", + "timestamp": 2020-05-06T09:22:23.582Z, + }, +] +`; diff --git a/test/unit/http.test.ts b/test/unit/http.test.ts index 9b7fc711..7da88dd0 100644 --- a/test/unit/http.test.ts +++ b/test/unit/http.test.ts @@ -174,5 +174,75 @@ describe('httpClient', () => { expect(cleanParameters(parameters)).toStrictEqual(results) }) + + it('should properly clean parameters with object arrays for attributes', () => { + expect.hasAssertions() + + const parameters = { + MarketplaceId: 'ATVPDKIKX0DER', + Destination: { + DeliveryChannel: 'SQS', + 'AttributeList.member': [ + { + Key: 'sqsQueueUrl', + // eslint-disable-next-line sonarjs/no-duplicate-string + Value: 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications', + }, + ], + }, + } + + const results = { + MarketplaceId: 'ATVPDKIKX0DER', + 'Destination.DeliveryChannel': 'SQS', + 'Destination.AttributeList.member.1.Key': 'sqsQueueUrl', + 'Destination.AttributeList.member.1.Value': + 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications', + } + + expect(cleanParameters(parameters)).toStrictEqual(results) + }) + + it('should properly clean parameters with object with an object arrays for attributes', () => { + expect.hasAssertions() + + const parameters = { + MarketplaceId: 'ATVPDKIKX0DER', + Subscriptions: { + NotificationType: 'AnyOfferChanged', + Destination: { + DeliveryChannel: 'SQS', + 'AttributeList.member': [ + { + Key: 'sqsQueueUrl', + Value: + 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications', + }, + { + Key: 'sqsQueueUrl', + Value: + 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications2', + }, + ], + }, + IsEnabled: true, + }, + } + + const results = { + MarketplaceId: 'ATVPDKIKX0DER', + 'Subscriptions.NotificationType': 'AnyOfferChanged', + 'Subscriptions.Destination.DeliveryChannel': 'SQS', + 'Subscriptions.Destination.AttributeList.member.1.Key': 'sqsQueueUrl', + 'Subscriptions.Destination.AttributeList.member.1.Value': + 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications', + 'Subscriptions.Destination.AttributeList.member.2.Key': 'sqsQueueUrl', + 'Subscriptions.Destination.AttributeList.member.2.Value': + 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications2', + 'Subscriptions.IsEnabled': 'true', + } + + expect(cleanParameters(parameters)).toStrictEqual(results) + }) }) }) diff --git a/test/unit/subscriptions.test.ts b/test/unit/subscriptions.test.ts new file mode 100644 index 00000000..a285757c --- /dev/null +++ b/test/unit/subscriptions.test.ts @@ -0,0 +1,277 @@ +import { amazonMarketplaces, HttpClient, ParsingError } from '../../src' +import { MWS } from '../../src/mws' +import { + AttributeKeyValueKeys, + DeliveryChannel, + NotificationType, +} from '../../src/sections/subscriptions' +import { getFixture } from '../utils' + +const httpConfig = { + awsAccessKeyId: '', + marketplace: amazonMarketplaces.CA, + mwsAuthToken: '', + secretKey: '', + sellerId: '', +} + +const headers = { + 'x-mws-request-id': '0', + 'x-mws-timestamp': '2020-05-06T09:22:23.582Z', + 'x-mws-quota-max': '1000', + 'x-mws-quota-remaining': '999', + 'x-mws-quota-resetson': '2020-04-06T10:22:23.582Z', +} + +const createMockHttpClient = (fixture: string) => + new MWS( + new HttpClient(httpConfig, () => + Promise.resolve({ + data: getFixture(fixture), + headers, + }), + ), + ) + +const mockMwsServiceStatus = createMockHttpClient('get_service_status') + +const mockMwsFail = new MWS( + new HttpClient(httpConfig, () => Promise.resolve({ data: '', headers: {} })), +) + +const parsingError = 'Expected an object, but received a string with value ""' + +const mockDestination = { + DeliveryChannel: 'SQS' as DeliveryChannel, + AttributeList: [ + { + Key: 'sqsQueueUrl' as AttributeKeyValueKeys, + Value: 'https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F51471EXAMPLE%2Fmws_notifications', + }, + ], +} + +const mockMarketplaceIdDestinationParameters = { + MarketplaceId: '', + Destination: mockDestination, +} + +const mockSubscription = { + NotificationType: 'AnyOfferChanged' as NotificationType, + Destination: mockDestination, + IsEnabled: true, +} + +const mockMarketplaceIdSubscriptionParameters = { + MarketplaceId: '', + Subscription: mockSubscription, +} + +const mockSubscriptionActionParameters = { + MarketplaceId: '', + NotificationType: 'AnyOfferChanged' as NotificationType, + Destination: mockDestination, +} + +describe('sellers', () => { + describe('updateSubscription', () => { + const parameters = { + MarketplaceId: '', + Subscription: mockSubscription, + } + + it('returns the standard response if update is succesful', async () => { + expect.assertions(1) + + const mockUpdateSubscription = createMockHttpClient('subscriptions_update_subscription') + + expect( + await mockUpdateSubscription.subscriptions.updateSubscription(parameters), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response isn t valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.updateSubscription(parameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('deleteSubscription', () => { + it('returns the standard response if delete is succesful', async () => { + expect.assertions(1) + + const mockDeleteSubscription = createMockHttpClient('subscriptions_delete_subscription') + + expect( + await mockDeleteSubscription.subscriptions.deleteSubscription( + mockSubscriptionActionParameters, + ), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response isn t valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.deleteSubscription(mockSubscriptionActionParameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('getSubscription', () => { + it('returns a subscription if succesful', async () => { + expect.assertions(1) + + const mockGetSubscription = createMockHttpClient('subscriptions_get_subscription') + + expect( + await mockGetSubscription.subscriptions.getSubscription(mockSubscriptionActionParameters), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response isnt valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.getSubscription(mockSubscriptionActionParameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('createSubscription', () => { + it('returns the standard response if creation is succesful', async () => { + expect.assertions(1) + + const mockCreateSubscription = createMockHttpClient('subscriptions_create_subscription') + + expect( + await mockCreateSubscription.subscriptions.createSubscription( + mockMarketplaceIdSubscriptionParameters, + ), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response isnt valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.createSubscription(mockMarketplaceIdSubscriptionParameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('sendTestNotificationToDestination', () => { + it('returns the standard response if testing is succesful', async () => { + expect.assertions(1) + + const mockListRegisteredDestinations = createMockHttpClient( + 'subscriptions_send_test_notification_to_destination', + ) + + expect( + await mockListRegisteredDestinations.subscriptions.sendTestNotificationToDestination( + mockMarketplaceIdDestinationParameters, + ), + ).toMatchSnapshot() + }) + + it("throws a parsing error when the response isn't valid", async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.sendTestNotificationToDestination( + mockMarketplaceIdDestinationParameters, + ), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('listRegisteredDestinations', () => { + const parameters = { + MarketplaceId: '', + } + + it('returns a list of destinations if succesful', async () => { + expect.assertions(1) + + const mockListRegisteredDestinations = createMockHttpClient( + 'subscriptions_list_registered_destinations', + ) + + expect( + await mockListRegisteredDestinations.subscriptions.listRegisteredDestinations(parameters), + ).toMatchSnapshot() + }) + + it("throws a parsing error when the response isn't valid", async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.listRegisteredDestinations(parameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('deregisterDestination', () => { + it('returns the standard response if deregistration is succesful', async () => { + expect.assertions(1) + + const mockDeregisterDestination = createMockHttpClient('subscriptions_deregister_destination') + + expect( + await mockDeregisterDestination.subscriptions.deregisterDestination( + mockMarketplaceIdDestinationParameters, + ), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response is not valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.deregisterDestination(mockMarketplaceIdDestinationParameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('registerDestination', () => { + it('returns the standard response if registration is succesful', async () => { + expect.assertions(1) + + const mockRegisterDestination = createMockHttpClient('subscriptions_register_destination') + + expect( + await mockRegisterDestination.subscriptions.registerDestination( + mockMarketplaceIdDestinationParameters, + ), + ).toMatchSnapshot() + }) + + it('throws a parsing error when the response is not valid', async () => { + expect.assertions(1) + + await expect(() => + mockMwsFail.subscriptions.registerDestination(mockMarketplaceIdDestinationParameters), + ).rejects.toStrictEqual(new ParsingError(parsingError)) + }) + }) + + describe('getServiceStatus', () => { + it('returns a parsed model when the status response is valid', async () => { + expect.assertions(1) + + expect(await mockMwsServiceStatus.subscriptions.getServiceStatus()).toMatchSnapshot() + }) + + it('throws a parsing error when the status response is not valid', async () => { + expect.assertions(1) + + await expect(() => mockMwsFail.sellers.getServiceStatus()).rejects.toStrictEqual( + new ParsingError(parsingError), + ) + }) + }) +})