├── .gitignore
├── .nvmrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples
└── example-item-search.js
├── lib
├── apac.js
├── locale.js
├── operation-helper.js
├── operation-helper.specs.js
├── request-signature-helper.js
├── request-signature-helper.specs.js
├── throttler.js
└── throttler.specs.js
├── package.json
└── test
├── acceptance
└── OperationHelper.specs.js
├── assign-globals.js
└── mocha.opts
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .project
3 | node_modules
4 | # Logs and databases #
5 | ######################
6 | *.log
7 | # OS generated files #
8 | ######################
9 | .DS_Store*
10 | ehthumbs.db
11 | Icon?
12 | Thumbs.db
13 | .env
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 4.4.1
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | - "5.1"
5 | - "4"
6 | - "4.0"
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 3.0.2
2 |
3 | - Mindor documentation updates
4 | - Update dependencies
5 |
6 | ## 3.0.0
7 |
8 | This is only a major version bump because we are not longer returning promises when using the
9 | callback interface.
10 |
11 | - Allow overriding the http request scheme (see https://github.com/dmcquay/node-apac/pull/75)
12 | - When providing a callback, a promise will no longer be returned. You must pick one or the other.
13 | The purpose of this is to avoid unhandle rejection warnings.
14 | - Upgrade `xml2js` from `0.4.16` to `0.4.17`
15 |
16 | ## 2.0.2
17 |
18 | - Document locales
19 |
20 | ## 2.0.1
21 |
22 | - Fix throttle mechanism
23 |
24 | ## 2.0.0
25 |
26 | NOTE: In v2.0.0, we changed the default for xml2js to set explicitArray to false. Before v.2.0.0, you would get a
27 | response like this instead (note the extra arrays you have to drill into):
28 |
29 | ```javascript
30 | {
31 | ItemSearchResponse: {
32 | OperationRequest: [ [Object] ],
33 | Items: [ [Object] ]
34 | }
35 | }
36 | ```
37 |
38 | You can change back to the old behavior by setting explitArray to true like this:
39 |
40 | ```javascript
41 | var opHelper = new OperationHelper({
42 | awsId: '[YOUR AWS ID HERE]',
43 | awsSecret: '[YOUR AWS SECRET HERE]',
44 | assocId: '[YOUR ASSOCIATE TAG HERE]',
45 | xml2jsOptions: { explicitArray: true }
46 | });
47 | ```
48 |
49 | # 1.0.0
50 |
51 | - Errors are now returned as the first parameter of the callback function, instead of being processed by a seperate OnError function.
52 |
53 | ## 0.0.3 - 0.0.14
54 |
55 | - Changes were not documented, refer to GitHub commits.
56 |
57 | ## 0.0.2
58 |
59 | - Added parsing of XML results using xml2js. Thanks pierrel.
60 |
61 | ## 0.0.1
62 |
63 | - Initial release
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2010 Dustin McQuay. All rights reserved.
2 | Permission is hereby granted, free of charge, to any person obtaining a copy
3 | of this software and associated documentation files (the "Software"), to
4 | deal in the Software without restriction, including without limitation the
5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
6 | sell copies of the Software, and to permit persons to whom the Software is
7 | furnished to do so, subject to the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included in
10 | all copies or substantial portions of the Software.
11 |
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
18 | IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/dmcquay/node-apac)
2 |
3 | # node-apac - Node.js client for the Amazon Product Advertising API.
4 |
5 | apac (Amazon Product Advertising Client) will allow you to access the Amazon Product Advertising API from Node.js.
6 | [Learn more about the Amazon Product Advertising API](https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html).
7 |
8 | node-apac is just a thin wrapper around Amazon's API. The only intent is to take care of request signatures, performing
9 | the HTTP requests, processing the responses and parsing the XML. You should be able to run any operation because the
10 | operation and all parameters are passed directly to the execute method just as they will be passed to Amazon. The result
11 | is that you feel like you're working directly with the API, but you don't have to worry about some of the more tedious
12 | tasks.
13 |
14 |
15 | ## Installation
16 |
17 | Install using npm:
18 | ```bash
19 | $ npm install apac
20 | ```
21 |
22 | ## Quick Start
23 |
24 | Here's a quick example:
25 | ```javascript
26 | const {OperationHelper} = require('apac');
27 |
28 | const opHelper = new OperationHelper({
29 | awsId: '[YOUR AWS ID HERE]',
30 | awsSecret: '[YOUR AWS SECRET HERE]',
31 | assocId: '[YOUR ASSOCIATE TAG HERE]'
32 | });
33 |
34 | opHelper.execute('ItemSearch', {
35 | 'SearchIndex': 'Books',
36 | 'Keywords': 'harry potter',
37 | 'ResponseGroup': 'ItemAttributes,Offers'
38 | }).then((response) => {
39 | console.log("Results object: ", response.result);
40 | console.log("Raw response body: ", response.responseBody);
41 | }).catch((err) => {
42 | console.error("Something went wrong! ", err);
43 | });
44 | ```
45 |
46 | Example Response:
47 |
48 | ```javascript
49 | {
50 | ItemSearchResponse: {
51 | OperationRequest: [Object],
52 | Items: [Object]
53 | }
54 | }
55 | ```
56 |
57 | ## API Documentation
58 |
59 | Since we just wrap the Amazon Product Advertising API, you'll reference their API docs:
60 | [API Reference](http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?ProgrammingGuide.html)
61 |
62 | ## Obtaining credentials
63 |
64 | Sign up here: https://affiliate-program.amazon.com/gp/advertising/api/detail/main.html
65 |
66 | This will also direct you where to get your security credentials (accessKeyId and secretAccessKey)
67 |
68 | You will also need to go here: http://docs.aws.amazon.com/AWSECommerceService/latest/DG/becomingAssociate.html
69 | and click on one of the locale specific associate websites to sign up as an associate and get an associate ID,
70 | which is required for all API calls.
71 |
72 | ## Throttling / Request Limits
73 |
74 | By default, Amazon limits you to one request per second per IP. This limit increases with revenue performance. Learn
75 | more here: http://docs.aws.amazon.com/AWSECommerceService/latest/DG/TroubleshootingApplications.html
76 |
77 | To help you ensure you don't exceed the request limit, we provide an automatic throttling feature. By default, apac will
78 | not throttle. To enable throttling, set the maxRequestsPerSecond param when constructing your OperationHelper.
79 |
80 | ```javascript
81 | var opHelper = new OperationHelper({
82 | awsId: '[YOUR AWS ID HERE]',
83 | awsSecret: '[YOUR AWS SECRET HERE]',
84 | assocId: '[YOUR ASSOCIATE TAG HERE]',
85 | maxRequestsPerSecond: 1
86 | });
87 | ```
88 |
89 | ## Locales
90 |
91 | To use a locale other than the default (US), set the locale parameter.
92 |
93 | ```javascript
94 | var opHelper = new OperationHelper({
95 | awsId: '[YOUR AWS ID HERE]',
96 | awsSecret: '[YOUR AWS SECRET HERE]',
97 | assocId: '[YOUR ASSOCIATE TAG HERE]',
98 | locale: 'IT'
99 | });
100 | ```
101 |
102 | **Supported Locales**
103 |
104 | ID|Locale|Endpoint
105 | ---|---|---
106 | BR|Brazil|webservices.amazon.com.br
107 | CA|Canada|webservices.amazon.ca
108 | CN|China|webservices.amazon.cn
109 | FR|France|webservices.amazon.fr
110 | DE|Germany|webservices.amazon.de
111 | IN|India|webservices.amazon.in
112 | IT|Italy|webservices.amazon.it
113 | JP|Japan|webservices.amazon.co.jp
114 | MX|Mexico|webservices.amazon.com.mx
115 | ES|Spain|webservices.amazon.es
116 | UK|United Kingdom|webservices.amazon.co.uk
117 | US|United States|webservices.amazon.com
118 |
119 | ## Contributing
120 |
121 | Feel free to submit a pull request. If you'd like, you may discuss the change with me first by submitting an issue.
122 | Please test all your changes. Current tests are located in lib/*.specs.js (next to each file under test).
123 |
124 | Execute tests with `npm test`
125 |
126 | Execute acceptance tests with `npm run test:acceptance`.
127 | For acceptance tests, you must set these environment variables first:
128 |
129 | ```
130 | AWS_ACCESS_KEY_ID=[VALUE]
131 | AWS_SECRET_ACCESS_KEY=[VALUE]
132 | AWS_ASSOCIATE_ID=[VALUE]
133 | ```
134 |
135 | You can set these values in your environment or in `test/acceptance/.env`.
136 |
137 | ## License
138 |
139 | Copyright (c) 2016 Dustin McQuay. All rights reserved.
140 |
141 | Permission is hereby granted, free of charge, to any person
142 | obtaining a copy of this software and associated documentation
143 | files (the "Software"), to deal in the Software without
144 | restriction, including without limitation the rights to use,
145 | copy, modify, merge, publish, distribute, sublicense, and/or sell
146 | copies of the Software, and to permit persons to whom the
147 | Software is furnished to do so, subject to the following
148 | conditions:
149 |
150 | The above copyright notice and this permission notice shall be
151 | included in all copies or substantial portions of the Software.
152 |
153 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
154 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
155 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
156 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
157 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
158 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
159 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
160 | OTHER DEALINGS IN THE SOFTWARE.
161 |
--------------------------------------------------------------------------------
/examples/example-item-search.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const util = require('util')
4 | const OperationHelper = require('../lib/apac').OperationHelper
5 |
6 | let opHelper = new OperationHelper({
7 | awsId: '[YOUR AWS ID HERE]',
8 | awsSecret: '[YOUR AWS SECRET HERE]',
9 | assocId: '[YOUR ASSOCIATE TAG HERE]'
10 | })
11 |
12 | const operation = 'ItemSearch'
13 | const params = {
14 | 'SearchIndex': 'Books',
15 | 'Keywords': 'harry potter',
16 | 'ResponseGroup': 'ItemAttributes,Offers'
17 | }
18 |
19 | opHelper.execute(operation, params).then((results, responseBody) => {
20 | console.log(results)
21 | console.log(responseBody)
22 | }).catch((err) => {
23 | console.error(err)
24 | })
25 |
26 | // or if you have async/await support...
27 |
28 | try {
29 | let response = await
30 | opHelper.execute(operation, params)
31 | console.log(response.results)
32 | console.log(response.responseBody)
33 | } catch(err) {
34 | console.error(err)
35 | }
36 |
37 | // traditional callback style is also supported for backwards compatibility
38 |
39 | opHelper.execute('ItemSearch', {
40 | 'SearchIndex': 'Books',
41 | 'Keywords': 'harry potter',
42 | 'ResponseGroup': 'ItemAttributes,Offers'
43 | }, function (err, results) {
44 | console.log(results)
45 | })
--------------------------------------------------------------------------------
/lib/apac.js:
--------------------------------------------------------------------------------
1 | exports.OperationHelper = require('./operation-helper').OperationHelper
2 | exports.RequestSignatureHelper = require('./request-signature-helper').RequestSignatureHelper
3 |
--------------------------------------------------------------------------------
/lib/locale.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | // http://docs.aws.amazon.com/AWSECommerceService/latest/DG/Locales.html
4 |
5 | const ENDPOINTS_BY_LOCALE = {
6 | BR: 'webservices.amazon.com.br',
7 | CA: 'webservices.amazon.ca',
8 | CN: 'webservices.amazon.cn',
9 | FR: 'webservices.amazon.fr',
10 | DE: 'webservices.amazon.de',
11 | IN: 'webservices.amazon.in',
12 | IT: 'webservices.amazon.it',
13 | JP: 'webservices.amazon.co.jp',
14 | MX: 'webservices.amazon.com.mx',
15 | ES: 'webservices.amazon.es',
16 | UK: 'webservices.amazon.co.uk',
17 | US: 'webservices.amazon.com'
18 | }
19 |
20 | const DEFAULT_ENDPOINT = ENDPOINTS_BY_LOCALE['US']
21 |
22 | exports.getEndpointForLocale = (locale) => {
23 | return ENDPOINTS_BY_LOCALE[locale] || DEFAULT_ENDPOINT
24 | }
25 |
26 | exports.DEFAULT_ENDPOINT = DEFAULT_ENDPOINT
27 |
--------------------------------------------------------------------------------
/lib/operation-helper.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const RSH = require('./request-signature-helper').RequestSignatureHelper
4 | const Throttler = require('./throttler')
5 | const locale = require('./locale')
6 |
7 | const https = require('https')
8 | const xml2js = require('xml2js')
9 |
10 | const defaultXml2JsOptions = {
11 | explicitArray: false
12 | }
13 |
14 | class OperationHelper {
15 | constructor(params) {
16 | params = params || {}
17 |
18 | // check requried params
19 | if (typeof(params.awsId) === 'undefined') {
20 | throw new Error('Missing AWS Id param')
21 | }
22 | if (typeof(params.awsSecret) === 'undefined') {
23 | throw new Error('Missing AWS Secret param')
24 | }
25 | if (typeof(params.assocId) === 'undefined') {
26 | throw new Error('Missing Associate Id param')
27 | }
28 |
29 | // set instance variables from params
30 | this.awsId = params.awsId
31 | this.awsSecret = params.awsSecret
32 | this.assocId = params.assocId
33 | this.endPoint = params.endPoint || locale.getEndpointForLocale(params.locale)
34 | this.baseUri = params.baseUri || OperationHelper.defaultBaseUri
35 | this.xml2jsOptions = Object.assign({}, defaultXml2JsOptions, params.xml2jsOptions)
36 | this.throttler = new Throttler(params.maxRequestsPerSecond)
37 |
38 | // set version
39 | if (typeof(params.version) === 'string') OperationHelper.version = params.version
40 | }
41 |
42 | getSignatureHelper() {
43 | if (typeof(this.signatureHelper) === 'undefined') {
44 | var params = {}
45 | params[RSH.kAWSAccessKeyId] = this.awsId
46 | params[RSH.kAWSSecretKey] = this.awsSecret
47 | params[RSH.kEndPoint] = this.endPoint
48 | this.signatureHelper = new RSH(params)
49 | }
50 | return this.signatureHelper
51 | }
52 |
53 | generateParams(operation, params) {
54 | params.Service = OperationHelper.service
55 | params.Version = OperationHelper.version
56 | params.Operation = operation
57 | params.AWSAccessKeyId = this.awsId
58 | params.AssociateTag = this.assocId
59 | return params
60 | }
61 |
62 | generateUri(operation, params) {
63 | params = this.generateParams(operation, params)
64 | var helper = this.getSignatureHelper()
65 | params = helper.sign(params)
66 | var queryString = helper.canonicalize(params)
67 | return this.baseUri + '?' + queryString
68 | }
69 |
70 | execute(operation, params, callback) {
71 | const throttledAction = () => this._execute(operation, params, callback)
72 | return this.throttler.execute(throttledAction)
73 | }
74 |
75 | getQueueSize() {
76 | return this.throttler.getQueueSize()
77 | }
78 |
79 | _execute(operation, params, callback) {
80 | if (typeof(operation) === 'undefined') {
81 | throw new Error('Missing operation parameter')
82 | }
83 | if (typeof(params) === 'undefined') {
84 | params = {}
85 | }
86 |
87 | var uri = this.generateUri(operation, params)
88 | var host = this.endPoint
89 | var xml2jsOptions = this.xml2jsOptions
90 |
91 | var options = {
92 | hostname: host,
93 | path: uri,
94 | method: 'GET'
95 | }
96 |
97 | var responseBody = ''
98 |
99 | const promise = new Promise((resolve, reject) => {
100 | var request = https.request(options, function (response) {
101 | response.setEncoding('utf8')
102 |
103 | response.on('data', function (chunk) {
104 | responseBody += chunk
105 | })
106 |
107 | response.on('end', function () {
108 | xml2js.parseString(responseBody, xml2jsOptions, function (err, result) {
109 | if (callback) callback(err, result, responseBody)
110 | else if (err) reject(err)
111 | else resolve({
112 | result,
113 | responseBody
114 | })
115 | })
116 | })
117 | })
118 |
119 | request.on('error', function (err) {
120 | if (callback) callback(err)
121 | else reject(err)
122 | })
123 |
124 | request.end()
125 | })
126 |
127 | if (!callback) return promise
128 | }
129 | }
130 |
131 | OperationHelper.version = '2013-08-01'
132 | OperationHelper.service = 'AWSECommerceService'
133 | OperationHelper.defaultBaseUri = '/onca/xml'
134 |
135 | exports.OperationHelper = OperationHelper
136 |
--------------------------------------------------------------------------------
/lib/operation-helper.specs.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const https = require('https')
4 | const EventEmitter = require('events')
5 | const xml2js = require('xml2js')
6 | const proxyquire = require('proxyquire')
7 | const RSH = require('./request-signature-helper').RequestSignatureHelper
8 |
9 | const OperationHelper = require('./operation-helper').OperationHelper
10 | const locale = require('./locale')
11 |
12 | const getNowMillis = () => {
13 | return (new Date()).getTime()
14 | }
15 |
16 | describe('OperationHelper', function () {
17 | const awsId = 'testAwsId'
18 | const awsSecret = 'testAwsSecret'
19 | const assocId = 'testAssocId'
20 |
21 | let baseParams = {
22 | awsId,
23 | awsSecret,
24 | assocId
25 | }
26 |
27 | describe('#constructor', () => {
28 | it('sets correct default endpoint', () => {
29 | let opHelper = new OperationHelper(baseParams)
30 | expect(opHelper.endPoint).to.equal('webservices.amazon.com')
31 | })
32 |
33 | it('sets endpoint by locale', () => {
34 | let opHelper = new OperationHelper(Object.assign({}, baseParams, {
35 | locale: 'IT'
36 | }))
37 | expect(opHelper.endPoint).to.equal('webservices.amazon.it')
38 | })
39 |
40 | it('sets endpoint directly', () => {
41 | let opHelper = new OperationHelper(Object.assign({}, baseParams, {
42 | locale: 'IT',
43 | endPoint: 'test.endpoint.com'
44 | }))
45 | expect(opHelper.endPoint).to.equal('test.endpoint.com')
46 | })
47 | })
48 |
49 | describe('#getSignatureHelper', () => {
50 | const mockRSHInstance = {}
51 | let mockRSHConstructor
52 | let OperationHelper2, initialSignatureHelper, secondSignatureHelper
53 |
54 | before(() => {
55 | mockRSHConstructor = sinon.stub().returns(mockRSHInstance)
56 | mockRSHConstructor.kAWSAccessKeyId = RSH.kAWSAccessKeyId
57 | mockRSHConstructor.kAWSSecretKey = RSH.kAWSSecretKey
58 | mockRSHConstructor.kEndPoint = RSH.kEndPoint
59 |
60 | OperationHelper2 = proxyquire('./operation-helper', {
61 | './request-signature-helper': {
62 | RequestSignatureHelper: mockRSHConstructor
63 | }
64 | }).OperationHelper
65 |
66 | let opHelper = new OperationHelper2(baseParams)
67 |
68 | initialSignatureHelper = opHelper.getSignatureHelper()
69 | secondSignatureHelper = opHelper.getSignatureHelper()
70 | })
71 |
72 | it('should return a singleton of the signature helper', () => {
73 | expect(secondSignatureHelper).to.equal(initialSignatureHelper)
74 | })
75 |
76 | it('should construct the signature helper with the right parameters', () => {
77 | expect(mockRSHConstructor.firstCall.args[0]).to.eql({
78 | [RSH.kAWSAccessKeyId]: awsId,
79 | [RSH.kAWSSecretKey]: awsSecret,
80 | [RSH.kEndPoint]: locale.DEFAULT_ENDPOINT
81 | })
82 | })
83 | })
84 |
85 | describe('#generateParams', () => {
86 | let actual, expected
87 | const operation = 'testOperation'
88 |
89 | before(() => {
90 | expected = {
91 | Service: OperationHelper.service,
92 | Version: OperationHelper.version,
93 | Operation: operation,
94 | AWSAccessKeyId: awsId,
95 | AssociateTag: assocId
96 | }
97 |
98 | let opHelper = new OperationHelper(baseParams)
99 | actual = opHelper.generateParams(operation, {})
100 | })
101 |
102 | it('should construct the correct params', () => {
103 | expect(actual).to.eql(expected)
104 | })
105 | })
106 |
107 | describe('#generateUri', () => {
108 | const operation = 'testOperation'
109 | const params = {foo: 'bar'}
110 | let opHelper, mockSignatureHelper, actual, expected
111 |
112 | before(() => {
113 | opHelper = new OperationHelper(baseParams)
114 |
115 | sinon.stub(opHelper, 'generateParams').returnsArg(1)
116 |
117 | mockSignatureHelper = {
118 | sign: sinon.stub().returnsArg(0),
119 | canonicalize: sinon.stub().returns('canonicalParams')
120 | }
121 | sinon.stub(opHelper, 'getSignatureHelper').returns(mockSignatureHelper)
122 |
123 | actual = opHelper.generateUri(operation, params)
124 | })
125 |
126 | it('should call generateParams correctly', () => {
127 | expect(opHelper.generateParams.firstCall.args[0]).to.eql(operation)
128 | expect(opHelper.generateParams.firstCall.args[1]).to.equal(params)
129 | })
130 |
131 | it('should sign the params', () => {
132 | expect(mockSignatureHelper.sign.firstCall.args[0]).to.eql(params)
133 | })
134 |
135 | it('should canonicalize the params', () => {
136 | expect(mockSignatureHelper.canonicalize.firstCall.args[0]).to.eql(params)
137 | })
138 |
139 | it('returns the correct uri', () => {
140 | expect(actual).to.equal('/onca/xml?canonicalParams')
141 | })
142 | })
143 |
144 | describe('#execute', () => {
145 | let requestMock, responseMock, result, outputResponseBody
146 | const responseBody = 'xml'
147 | const xml2jsOptions = {foo: 'bar'}
148 | const expectedXml2jsOptions = Object.assign({explicitArray: false}, xml2jsOptions)
149 |
150 | context('happy path', () => {
151 | let opHelper
152 |
153 | beforeEach(() => {
154 | opHelper = new OperationHelper({
155 | awsId: 'testAwsId',
156 | awsSecret: 'testAwsSecret',
157 | assocId: 'testAssocId',
158 | xml2jsOptions
159 | })
160 |
161 | responseMock = new EventEmitter()
162 | responseMock.setEncoding = sinon.spy()
163 |
164 | requestMock = new EventEmitter()
165 | requestMock.end = () => {
166 | responseMock.emit('data', responseBody.substr(0, 5))
167 | responseMock.emit('data', responseBody.substr(5))
168 | responseMock.emit('end')
169 | }
170 |
171 | sinon.stub(https, 'request').returns(requestMock).callsArgWith(1, responseMock)
172 | sinon.stub(opHelper, 'generateUri').returns('testUri')
173 | sinon.spy(xml2js, 'parseString')
174 | })
175 |
176 | afterEach(() => {
177 | https.request.restore()
178 | xml2js.parseString.restore()
179 | })
180 |
181 | const doAssertions = () => {
182 | it('should create an https request with the correct options', () => {
183 | expect(https.request.callCount).to.equal(1)
184 | expect(https.request.firstCall.args[0]).to.eql({
185 | hostname: locale.DEFAULT_ENDPOINT,
186 | method: 'GET',
187 | path: 'testUri',
188 | })
189 | })
190 |
191 | it('should set the response encoding to utf8', () => {
192 | expect(responseMock.setEncoding.calledWith('utf8'))
193 | })
194 |
195 | it('should provide the raw response body', () => {
196 | expect(outputResponseBody).to.equal(responseBody)
197 | })
198 |
199 | it('should pass the xml2jsOptions to xml2js', () => {
200 | expect(xml2js.parseString.firstCall.args[1]).to.eql(expectedXml2jsOptions)
201 | })
202 |
203 | it('should parse XML and return result as object', () => {
204 | expect(result).to.eql({
205 | it: {
206 | is: {
207 | some: 'xml'
208 | }
209 | }
210 | })
211 | })
212 | }
213 |
214 | context('(traditional callback)', () => {
215 | beforeEach((done) => {
216 | opHelper.execute('ItemSearch', {
217 | 'SearchIndex': 'Books',
218 | 'Keywords': 'harry potter',
219 | 'ResponseGroup': 'ItemAttributes,Offers'
220 | }, function (err, _results, _rawResponseBody) {
221 | result = _results
222 | outputResponseBody = _rawResponseBody
223 | done()
224 | })
225 | })
226 |
227 | doAssertions()
228 | })
229 |
230 | context('(promise)', () => {
231 | beforeEach(() => {
232 | return opHelper.execute('ItemSearch', {
233 | 'SearchIndex': 'Books',
234 | 'Keywords': 'harry potter',
235 | 'ResponseGroup': 'ItemAttributes,Offers'
236 | }).then((response) => {
237 | result = response.result
238 | outputResponseBody = response.responseBody
239 | })
240 | })
241 |
242 | doAssertions()
243 | })
244 | })
245 |
246 | context('when the request has an error', () => {
247 | const error = new Error('testErrorMessage')
248 | let thrownError, opHelper
249 |
250 | beforeEach(() => {
251 | opHelper = new OperationHelper({
252 | awsId: 'testAwsId',
253 | awsSecret: 'testAwsSecret',
254 | assocId: 'testAssocId'
255 | })
256 |
257 | responseMock = new EventEmitter()
258 | responseMock.setEncoding = sinon.spy()
259 |
260 | requestMock = new EventEmitter()
261 | requestMock.end = () => {
262 | requestMock.emit('error', error)
263 | }
264 |
265 | sinon.stub(https, 'request').returns(requestMock).callsArgWith(1, responseMock)
266 | sinon.stub(opHelper, 'generateUri').returns('testUri')
267 | })
268 |
269 | afterEach(() => {
270 | https.request.restore()
271 | })
272 |
273 | context('(traditional callback)', () => {
274 | beforeEach((done) => {
275 | opHelper.execute('ItemSearch', {
276 | 'SearchIndex': 'Books',
277 | 'Keywords': 'harry potter',
278 | 'ResponseGroup': 'ItemAttributes,Offers'
279 | }, function (err) {
280 | thrownError = err
281 | done()
282 | })
283 | })
284 |
285 | it('should call the callback with the error', () => {
286 | expect(thrownError).to.equal(error)
287 | })
288 | })
289 |
290 | context('(promise)', () => {
291 | beforeEach(() => {
292 | return opHelper.execute('ItemSearch', {
293 | 'SearchIndex': 'Books',
294 | 'Keywords': 'harry potter',
295 | 'ResponseGroup': 'ItemAttributes,Offers'
296 | }).catch((err) => {
297 | thrownError = err
298 | })
299 | })
300 |
301 | it('should call the callback with the error', () => {
302 | expect(thrownError).to.equal(error)
303 | })
304 | })
305 | })
306 |
307 | context('when there is an error parsing the response', () => {
308 | const malformedResponseBody = 'xml'
309 | const testError = new Error('test error')
310 | let returnedError, opHelper
311 |
312 | beforeEach(() => {
313 | opHelper = new OperationHelper({
314 | awsId: 'testAwsId',
315 | awsSecret: 'testAwsSecret',
316 | assocId: 'testAssocId'
317 | })
318 |
319 | responseMock = new EventEmitter()
320 | responseMock.setEncoding = sinon.spy()
321 |
322 | requestMock = new EventEmitter()
323 | requestMock.end = () => {
324 | responseMock.emit('data', malformedResponseBody)
325 | responseMock.emit('end')
326 | }
327 |
328 | sinon.stub(https, 'request').returns(requestMock).callsArgWith(1, responseMock)
329 | sinon.stub(opHelper, 'generateUri').returns('testUri')
330 | sinon.stub(xml2js, 'parseString').callsArgWith(2, testError)
331 | })
332 |
333 | afterEach(() => {
334 | https.request.restore()
335 | xml2js.parseString.restore()
336 | })
337 |
338 | context('(traditional callback)', () => {
339 | beforeEach((done) => {
340 | opHelper.execute('ItemSearch', {
341 | 'SearchIndex': 'Books',
342 | 'Keywords': 'harry potter',
343 | 'ResponseGroup': 'ItemAttributes,Offers'
344 | }, function (err) {
345 | returnedError = err
346 | done()
347 | })
348 | })
349 |
350 | it('should call the callback with the error', () => {
351 | expect(returnedError).to.equal(testError)
352 | })
353 | })
354 |
355 | context('(promise)', () => {
356 | beforeEach(() => {
357 | return opHelper.execute('ItemSearch', {
358 | 'SearchIndex': 'Books',
359 | 'Keywords': 'harry potter',
360 | 'ResponseGroup': 'ItemAttributes,Offers'
361 | }).catch((err) => {
362 | returnedError = err
363 | })
364 | })
365 |
366 | it('should call the callback with the error', () => {
367 | expect(returnedError).to.equal(testError)
368 | })
369 | })
370 | })
371 |
372 | context('when throttling is necessary', () => {
373 | let opHelper, startTimeMillis
374 |
375 | beforeEach(() => {
376 | opHelper = new OperationHelper(Object.assign({}, baseParams, {
377 | maxRequestsPerSecond: 10
378 | }))
379 |
380 | const buildReqAndResp = () => {
381 | let responseMock = new EventEmitter()
382 | responseMock.setEncoding = sinon.spy()
383 |
384 | let requestMock = new EventEmitter()
385 | requestMock.end = () => {
386 | responseMock.emit('data', responseBody)
387 | responseMock.emit('end')
388 | }
389 |
390 | return {
391 | req: requestMock,
392 | res: responseMock
393 | }
394 | }
395 |
396 | sinon.stub(https, 'request')
397 | const reqRes1 = buildReqAndResp()
398 | const reqRes2 = buildReqAndResp()
399 | const reqRes3 = buildReqAndResp()
400 | https.request.onFirstCall().callsArgWith(1, reqRes1.res).returns(reqRes1.req)
401 | https.request.onSecondCall().callsArgWith(1, reqRes2.res).returns(reqRes2.req)
402 | https.request.onThirdCall().callsArgWith(1, reqRes3.res).returns(reqRes3.req)
403 |
404 | sinon.stub(opHelper, 'generateUri').returns('testUri')
405 |
406 | const operation = 'ItemSearch'
407 | const params = {
408 | 'SearchIndex': 'Books',
409 | 'Keywords': 'harry potter',
410 | 'ResponseGroup': 'ItemAttributes,Offers'
411 | }
412 |
413 | startTimeMillis = getNowMillis()
414 | return Promise.all([
415 | opHelper.execute(operation, params),
416 | opHelper.execute(operation, params),
417 | opHelper.execute(operation, params)
418 | ])
419 | })
420 |
421 | afterEach(() => {
422 | https.request.restore()
423 | })
424 |
425 | it('should take at least (1 / maxRequestsPerSecond) * (numOperations - 1) seconds to complete', () => {
426 | const durationMillis = getNowMillis() - startTimeMillis
427 | expect(durationMillis).to.be.at.least(199)
428 | expect(durationMillis).to.be.at.most(300)
429 | })
430 | })
431 | })
432 | })
433 |
--------------------------------------------------------------------------------
/lib/request-signature-helper.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | var crypto = require('crypto')
4 |
5 | class RSH {
6 | constructor(params) {
7 | this.init(params)
8 | }
9 |
10 | init(params) {
11 | // enforce required params
12 | if (typeof(params[RSH.kAWSAccessKeyId]) === 'undefined') {
13 | throw new Error('Need access key id argument')
14 | }
15 | if (typeof(params[RSH.kAWSSecretKey]) === 'undefined') {
16 | throw new Error('Need secret key argument')
17 | }
18 | if (typeof(params[RSH.kEndPoint]) === 'undefined') {
19 | throw new Error('Need end point argument')
20 | }
21 |
22 | // set params
23 | this[RSH.kAWSAccessKeyId] = params[RSH.kAWSAccessKeyId]
24 | this[RSH.kAWSSecretKey] = params[RSH.kAWSSecretKey]
25 | this[RSH.kEndPoint] = params[RSH.kEndPoint].toLowerCase()
26 | this[RSH.kRequestMethod] = params[RSH.kRequestMethod] || 'GET'
27 | this[RSH.kRequestUri] = params[RSH.kRequestUri] || '/onca/xml'
28 | }
29 |
30 | sign(params) {
31 | // append params
32 | params[RSH.kTimestampParam] = this.generateTimestamp()
33 | // generate signature
34 | var canonical = this.canonicalize(params)
35 | var stringToSign = [
36 | this[RSH.kRequestMethod],
37 | this[RSH.kEndPoint],
38 | this[RSH.kRequestUri],
39 | canonical
40 | ].join('\n')
41 | params[RSH.kSignatureParam] = this.digest(stringToSign)
42 |
43 | return params
44 | }
45 |
46 | zeroPad(num, length) {
47 | num = num + ''
48 | while (num.length < length) {
49 | num = '0' + num
50 | }
51 | return num
52 | }
53 |
54 | generateTimestamp() {
55 | var now = new Date(),
56 | year = now.getUTCFullYear(),
57 | month = this.zeroPad(now.getUTCMonth() + 1, 2),
58 | day = this.zeroPad(now.getUTCDate(), 2),
59 | hours = this.zeroPad(now.getUTCHours(), 2),
60 | mins = this.zeroPad(now.getUTCMinutes(), 2),
61 | secs = this.zeroPad(now.getUTCSeconds(), 2)
62 | return [year, month, day].join('-') + 'T' +
63 | [hours, mins, secs].join(':') + 'Z'
64 | }
65 |
66 |
67 | /**
68 | * Port of PHP rawurlencode().
69 | */
70 | escape(x) {
71 | return encodeURIComponent(x).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A')
72 | }
73 |
74 | digest(x) {
75 | var secretKey = this[RSH.kAWSSecretKey]
76 | var hmac = crypto.createHmac('sha256', secretKey)
77 | hmac.update(x)
78 | return hmac.digest('base64')
79 | }
80 |
81 | canonicalize(params) {
82 | var parts = []
83 | for (var key in params) {
84 | parts.push([this.escape(key), this.escape(params[key])].join('='))
85 | }
86 | return parts.sort().join('&')
87 | }
88 | }
89 |
90 | RSH.kAWSAccessKeyId = 'AWSAccessKeyId'
91 | RSH.kAWSSecretKey = 'AWSSecretKey'
92 | RSH.kEndPoint = 'EndPoint'
93 | RSH.kRequestMethod = 'RequestMethod'
94 | RSH.kRequestUri = 'RequestUri'
95 | RSH.kTimestampParam = 'Timestamp'
96 | RSH.kSignatureParam = 'Signature'
97 |
98 | exports.RequestSignatureHelper = RSH
--------------------------------------------------------------------------------
/lib/request-signature-helper.specs.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | var assert = require('assert'),
4 | RSH = require('./request-signature-helper').RequestSignatureHelper
5 |
6 | describe('RequestSignatureHelper', () => {
7 | let rsh
8 | const secretKey = 'this is a secret key'
9 | const accessKeyId = 'this is an access key'
10 | const endPoint = ''
11 | const params = {
12 | [RSH.kAWSAccessKeyId]: accessKeyId,
13 | [RSH.kAWSSecretKey]: secretKey,
14 | [RSH.kEndPoint]: endPoint
15 | }
16 |
17 | before(() => {
18 | rsh = new RSH(params)
19 | })
20 |
21 | it('should create an instance', () => {
22 | expect(rsh).to.be.an.instanceOf(RSH)
23 | })
24 |
25 | describe('#canonicalize', () => {
26 | it('should return a string', () => {
27 | expect(rsh.canonicalize({'a': 'b'})).to.be.a('string')
28 | })
29 |
30 | it('should produce a query string format', () => {
31 | expect(rsh.canonicalize({'a': 'b'})).to.equal('a=b')
32 | })
33 |
34 | it('should produce a query string with multiple parameters', () => {
35 | expect(rsh.canonicalize({'a': 'b', 'c': 'd'})).to.equal('a=b&c=d')
36 | })
37 |
38 | it('should sort the parameters', () => {
39 | expect(rsh.canonicalize({'f': 'b', 'a': 'q'})).to.equal('a=q&f=b')
40 | })
41 | })
42 |
43 | describe('#digest', () => {
44 | let testStr = 'this is a test string'
45 | let digest = 'faew2bQnf2IfFY9Wm8CrIL6AWWp6N+2JFOEwEmMJyKA='
46 |
47 | it('should return a string', () => {
48 | expect(rsh.digest(testStr)).to.be.a('string')
49 | })
50 |
51 | it('should create the correct digest', () => {
52 | expect(rsh.digest(testStr)).to.equal(digest)
53 | })
54 | })
55 |
56 | describe('#zeroPad', () => {
57 | it('should pad correctly', () => {
58 | expect(rsh.zeroPad(1, 3)).to.equal('001')
59 | })
60 | })
61 |
62 | describe('#generateTimestamp', () => {
63 | it('should format the timestamp correctly', () => {
64 | expect(rsh.generateTimestamp()).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/)
65 | })
66 | })
67 |
68 | describe('#sign', () => {
69 | let params
70 |
71 | before(() => {
72 | params = rsh.sign({'d': 'a', 'c': 'f'});
73 | })
74 |
75 | it('should return an object', () => {
76 | expect(params).to.be.an('object')
77 | })
78 |
79 | it('should add timestamp to the params', () => {
80 | expect(params[RSH.kTimestampParam]).to.be.a('string')
81 | expect(params[RSH.kTimestampParam]).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/)
82 | })
83 |
84 | it('should add signature to the params', () => {
85 | expect(params[RSH.kSignatureParam]).to.be.a('string')
86 | })
87 | })
88 | })
--------------------------------------------------------------------------------
/lib/throttler.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const getNowMillis = () => {
4 | return (new Date()).getTime()
5 | }
6 |
7 | class Throttler {
8 | constructor(maxRequestsPerSecond) {
9 | this.maxRequestsPerSecond = maxRequestsPerSecond || 0
10 | this._timeBetweenRequestsInMilliSeconds = 1 / this.maxRequestsPerSecond * 1000
11 | this._nextAvailableRequestMillis = getNowMillis()
12 | }
13 |
14 | execute(cb) {
15 | let nowMillis = getNowMillis()
16 | if (this.maxRequestsPerSecond === 0) {
17 | return Promise.resolve(cb())
18 | } else if (nowMillis >= this._nextAvailableRequestMillis) {
19 | this._nextAvailableRequestMillis = getNowMillis() + this._timeBetweenRequestsInMilliSeconds
20 | return Promise.resolve(cb())
21 | } else {
22 | return new Promise((resolve) => {
23 | setTimeout(() => {
24 | resolve(cb())
25 | }, this._nextAvailableRequestMillis - nowMillis)
26 | this._nextAvailableRequestMillis += this._timeBetweenRequestsInMilliSeconds
27 | })
28 | }
29 | }
30 |
31 | getQueueSize() {
32 | let nowMillis = getNowMillis()
33 | if (this.maxRequestsPerSecond === 0) {
34 | return (-1)
35 | } else if (this._nextAvailableRequestMillis <= nowMillis) {
36 | return 0
37 | } else {
38 | return Math.floor((this._nextAvailableRequestMillis - nowMillis) / (this.maxRequestsPerSecond * 1000))
39 | }
40 | }
41 | }
42 |
43 | module.exports = Throttler
44 |
--------------------------------------------------------------------------------
/lib/throttler.specs.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const Throttler = require('./throttler')
4 |
5 | describe('Throttler', function () {
6 | describe('#constructor', () => {
7 | it('sets default max requests per second', () => {
8 | let throttler = new Throttler()
9 | expect(throttler.maxRequestsPerSecond).to.equal(0)
10 | })
11 |
12 | it('computes correct time between requests in milliseconds, defaults', () => {
13 | let throttler = new Throttler()
14 | expect(throttler._timeBetweenRequestsInMilliSeconds).to.equal(Infinity)
15 | })
16 |
17 | it('computes correct next available request time, defaults, -/+1 ms tolerance', () => {
18 | let throttler = new Throttler(0)
19 | let requestTime = (new Date()).getTime()
20 |
21 | expect(throttler._nextAvailableRequestMillis).to.be.within(requestTime-1, requestTime+1)
22 | })
23 |
24 | it('sets max requests per second - 10 req/s', () => {
25 | let throttler = new Throttler(10)
26 | expect(throttler.maxRequestsPerSecond).to.equal(10)
27 | })
28 |
29 | it('computes correct time between requests in milliseconds, 10 req/sec', () => {
30 | let throttler = new Throttler(10)
31 | expect(throttler._timeBetweenRequestsInMilliSeconds).to.equal(100)
32 | })
33 |
34 | it('computes correct next available request time, 10 req/sec, -/+1 ms tolerance', () => {
35 | let throttler = new Throttler(10)
36 | let requestTime = (new Date()).getTime()
37 |
38 | expect(throttler._nextAvailableRequestMillis).to.be.within(requestTime-1, requestTime+1)
39 | })
40 | })
41 |
42 | describe('#execute', () => {
43 | it('executes passed function immediately, -/+5 ms tolerance', () => {
44 | let throttler = new Throttler()
45 | let passedFunction = () => { return true }
46 | let startTime = (new Date()).getTime()
47 | let endTime
48 |
49 | return throttler.execute(passedFunction).then(function(result) {
50 | endTime = (new Date()).getTime()
51 |
52 | expect(result).to.equal(true)
53 | expect(endTime).to.be.within(startTime-5, startTime+5)
54 | })
55 | })
56 |
57 | it('executes multiple passed functions immediately, -/+5 ms tolerance', () => {
58 | let throttler = new Throttler()
59 | let passedFunction = () => { return true }
60 | let startTime = (new Date()).getTime()
61 | let endTime
62 |
63 | return Promise.all([
64 | throttler.execute(passedFunction),
65 | throttler.execute(passedFunction),
66 | ]).then(function(result) {
67 | endTime = (new Date()).getTime()
68 |
69 | expect(result).to.deep.equal([true, true])
70 | expect(endTime).to.be.within(startTime-5, startTime+5)
71 | })
72 | })
73 |
74 | it('executes passed function with empty queue, -/+5 ms tolerance', () => {
75 | let throttler = new Throttler(1)
76 | let passedFunction = () => { return true }
77 | let startTime = (new Date()).getTime()
78 | let endTime
79 |
80 | return throttler.execute(passedFunction).then(function(result) {
81 | endTime = (new Date()).getTime()
82 |
83 | expect(result).to.equal(true)
84 | expect(endTime).to.be.within(startTime-5, startTime+5)
85 | })
86 | })
87 |
88 | it('executes passed function with one request in queue, -/+5 ms tolerance', () => {
89 | let throttler = new Throttler(10)
90 | let passedFunction = () => { return true }
91 | let startTime = (new Date()).getTime()
92 | let endTime
93 |
94 | return Promise.all([
95 | throttler.execute(passedFunction),
96 | throttler.execute(passedFunction),
97 | ]).then(function(result) {
98 | endTime = (new Date()).getTime()
99 |
100 | expect(result).to.deep.equal([true, true])
101 | expect(endTime).to.be.within(startTime+(100-5), startTime+(100+5))
102 | })
103 | })
104 | })
105 |
106 | describe('#getQueueSize', () => {
107 | it('returns -1 when throttler is disabled', () => {
108 | let throttler = new Throttler()
109 |
110 | expect(throttler.getQueueSize()).to.equal(-1)
111 | })
112 |
113 | it('returns 0 when queue is empty, nothing executing', () => {
114 | let throttler = new Throttler(1)
115 |
116 | expect(throttler.getQueueSize()).to.equal(0)
117 | })
118 |
119 | it('returns 0 when queue is empty, executing function', () => {
120 | let throttler = new Throttler(1)
121 | let wait = function(ms){
122 | let startTime = new Date().getTime();
123 | let endTime = startTime;
124 | while(endTime < startTime + ms) {
125 | endTime = new Date().getTime();
126 | }
127 | }
128 | let passedFunction = () => { wait(500); return true }
129 |
130 | throttler.execute(passedFunction)
131 |
132 | expect(throttler.getQueueSize()).to.equal(0)
133 | })
134 |
135 | it('returns 1 when queue is loaded with 1 request', () => {
136 | let throttler = new Throttler(1)
137 | let wait = function(ms){
138 | let startTime = new Date().getTime();
139 | let endTime = startTime;
140 | while(endTime < startTime + ms) {
141 | endTime = new Date().getTime();
142 | }
143 | }
144 | let passedFunction = () => { wait(500); return true }
145 |
146 | throttler.execute(passedFunction)
147 | throttler.execute(passedFunction)
148 |
149 | expect(throttler.getQueueSize()).to.equal(1)
150 | })
151 | })
152 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apac",
3 | "description": "Amazon Product Advertising API Client for Node",
4 | "version": "3.0.2",
5 | "author": "Dustin McQuay ",
6 | "scripts": {
7 | "test": "mocha lib/*.specs.js",
8 | "test:acceptance": "mocha test/acceptance/*.specs.js || true"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/dmcquay/node-apac.git"
13 | },
14 | "bugs": {
15 | "url": "http://github.com/dmcquay/node-apac/issues"
16 | },
17 | "directories": {
18 | "lib": "./lib/"
19 | },
20 | "main": "./lib/apac",
21 | "dependencies": {
22 | "xml2js": "^0.4.17"
23 | },
24 | "devDependencies": {
25 | "chai": "3.5.0",
26 | "dotenv": "4.0.0",
27 | "mocha": "3.2.0",
28 | "proxyquire": "1.7.10",
29 | "sinon": "1.17.7"
30 | },
31 | "engines": {
32 | "node": ">=4.0.0"
33 | },
34 | "licenses": [
35 | {
36 | "type": "MIT",
37 | "url": "http://github.com/dmcquay/node-apac/raw/master/LICENSE"
38 | }
39 | ],
40 | "keywords": [
41 | "Amazon Product Advertising API",
42 | "AWS"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/test/acceptance/OperationHelper.specs.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const path = require('path')
4 |
5 | const OperationHelper = require('../../').OperationHelper
6 |
7 | require('dotenv').config({
8 | silent: true,
9 | path: path.join(__dirname, '.env')
10 | })
11 |
12 | const e = process.env
13 |
14 | let config = {
15 | AWS_ACCESS_KEY_ID: e.AWS_ACCESS_KEY_ID,
16 | AWS_SECRET_ACCESS_KEY: e.AWS_SECRET_ACCESS_KEY,
17 | AWS_ASSOCIATE_ID: e.AWS_ASSOCIATE_ID
18 | }
19 |
20 | describe('OperationHelper', () => {
21 | describe('execute a typical query', () => {
22 | let result
23 |
24 | before(() => {
25 | let opHelper = new OperationHelper({
26 | awsId: config.AWS_ACCESS_KEY_ID,
27 | awsSecret: config.AWS_SECRET_ACCESS_KEY,
28 | assocId: config.AWS_ASSOCIATE_ID
29 | })
30 |
31 | return opHelper.execute('ItemSearch', {
32 | 'SearchIndex': 'Books',
33 | 'Keywords': 'harry potter',
34 | 'ResponseGroup': 'ItemAttributes,Offers'
35 | }).then((response) => {
36 | result = response.result
37 | })
38 | })
39 |
40 | it('returns a sane looking response', () => {
41 | expect(result.ItemSearchResponse).to.exist
42 | expect(result.ItemSearchResponse.Items.Item.length).to.be.at.least(1)
43 | expect(result.ItemSearchResponse.Items.Item[0].ItemAttributes.Author[0]).to.equal('J.K. Rowling')
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/test/assign-globals.js:
--------------------------------------------------------------------------------
1 | global.expect = require('chai').expect;
2 | global.sinon = require('sinon');
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require test/assign-globals.js
2 | --reporter progress
3 |
--------------------------------------------------------------------------------