├── .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 | [![Build Status](https://travis-ci.org/dmcquay/node-apac.svg?branch=master)](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 | --------------------------------------------------------------------------------