diff --git a/adwords/report.js b/adwords/report.js index 4b48347..890f6d0 100644 --- a/adwords/report.js +++ b/adwords/report.js @@ -6,10 +6,11 @@ const request = require('request'); const util = require('util'); -const fs = require('fs'); const AdwordsConstants = require('./constants'); const AdwordsReportBuilder = require('./report-builder'); const AdwordsAuth = require('./auth'); +const https = require('https'); +const querystring = require('querystring'); class AdwordsReport { @@ -43,12 +44,9 @@ class AdwordsReport { return callback(error); } - request({ - uri: 'https://adwords.google.com/api/adwords/reportdownload/' + apiVersion, - method: 'POST', - headers: headers, - form: this.buildReportBody(report) - }, (error, response, body) => { + let params = this.buildParams(apiVersion, headers, report); + + request(params, (error, response, body) => { if (error || this.reportBodyContainsError(report, body)) { error = error || body; if (-1 !== error.toString().indexOf(AdwordsConstants.OAUTH_ERROR) && retryRequest) { @@ -62,6 +60,58 @@ class AdwordsReport { }); } + /** + * Builds parameters. + * + * @param {string} apiVersion The api version + * @param {Object} headers The headers + * @param {Object} report The report + * @return {Object} The parameters. + */ + buildParams(apiVersion, headers, report) { + return { + uri: 'https://adwords.google.com/api/adwords/reportdownload/' + apiVersion, + hostname: 'adwords.google.com', + port: 443, + path: '/api/adwords/reportdownload/' + apiVersion, + method: 'POST', + headers: headers, + form: this.buildReportBody(report) + }; + } + + /** + * Streams a report. + * + * @param {string} apiVersion The api version + * @param {Object} report The report + * @param {Function} callback The callback + * @return {boolean} { description_of_the_return_value } + * + * @throws {Object} { If there is an error in the call } + */ + streamReport(apiVersion, report, callback) { + report = report || {}; + apiVersion = apiVersion || AdwordsConstants.DEFAULT_ADWORDS_VERSION; + + this.getHeaders(report.additionalHeaders, (error, headers) => { + if (error) { return callback(error); } + + let params = this.buildParams(apiVersion, headers, report); + + let postData = querystring.stringify(params.form); + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + headers['Content-Length'] = postData.length; + + let request = https.request(params, (response) => { + callback(response); + }); + + request.write(postData); + request.end(); + }); + } + /** * Determines if the body contains an error message * @param report {object} the report object diff --git a/package-lock.json b/package-lock.json index e6eb997..2bb9e15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,41 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz", + "integrity": "sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz", + "integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==", + "dev": true, + "requires": { + "@sinonjs/samsam": "2.1.0" + }, + "dependencies": { + "@sinonjs/samsam": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz", + "integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==", + "dev": true, + "requires": { + "array-from": "^2.1.1" + } + } + } + }, + "@sinonjs/samsam": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.2.tgz", + "integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==", + "dev": true + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -15,6 +50,12 @@ "json-schema-traverse": "^0.3.0" } }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -123,7 +164,7 @@ }, "commander": { "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, @@ -509,6 +550,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz", + "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", + "dev": true + }, "jwa": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", @@ -533,11 +580,23 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, + "lolex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.0.0.tgz", + "integrity": "sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ==", + "dev": true + }, "lru-cache": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", @@ -568,7 +627,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -576,13 +635,13 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -608,6 +667,16 @@ "supports-color": "5.4.0" } }, + "mock-req": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mock-req/-/mock-req-0.2.0.tgz", + "integrity": "sha1-dJRGgE0sAGFpNC7nvmu6HP/VNMI=" + }, + "mock-res": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mock-res/-/mock-res-0.5.0.tgz", + "integrity": "sha1-mDaL6wnfdT9k9m2U5VNql7NqJDA=" + }, "moment": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", @@ -618,6 +687,27 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "nise": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.6.tgz", + "integrity": "sha512-1GedetLKzmqmgwabuMSqPsT7oumdR77SBpDfNNJhADRIeA3LN/2RVqR4fFqwvzhAqcTef6PPCzQwITE/YQ8S8A==", + "dev": true, + "requires": { + "@sinonjs/formatio": "3.0.0", + "just-extend": "^3.0.0", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0", + "text-encoding": "^0.6.4" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, "node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", @@ -652,10 +742,27 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -803,6 +910,34 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "sinon": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.1.1.tgz", + "integrity": "sha512-iYagtjLVt1vN3zZY7D8oH7dkjNJEjLjyuzy8daX5+3bbQl8gaohrheB9VfH1O3L6LKuue5WTJvFluHiuZ9y3nQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.2.0", + "@sinonjs/formatio": "^3.0.0", + "@sinonjs/samsam": "^2.1.2", + "diff": "^3.5.0", + "lodash.get": "^4.4.2", + "lolex": "^3.0.0", + "nise": "^1.4.6", + "supports-color": "^5.5.0", + "type-detect": "^4.0.8" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "soap": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/soap/-/soap-0.24.0.tgz", @@ -879,6 +1014,12 @@ "has-flag": "^3.0.0" } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -901,6 +1042,12 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 88bbf21..f8dee6e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "xmlbuilder": "^10.0.0" }, "devDependencies": { - "mocha": "^5.2.0" + "mocha": "^5.2.0", + "mock-req": "^0.2.0", + "mock-res": "^0.5.0", + "sinon": "^7.1.1" } } diff --git a/test/adwords/services/reporting.js b/test/adwords/services/reporting.js index ac05cce..7352181 100644 --- a/test/adwords/services/reporting.js +++ b/test/adwords/services/reporting.js @@ -4,10 +4,99 @@ */ const _ = require('lodash'); const AdwordsReport = require('../../../index').AdwordsReport; +const sinon = require('sinon'); +const https = require('https'); +const http = require('http'); +const os = require('os'); +const path = require ('path'); +const assert = require('assert'); +const MockReq = require('mock-req'); +const MockRes = require('mock-res'); describe('ReportService', function() { - let config = require('./adwordsuser-config'); + + describe('streamReport', function() { + let post; + + beforeEach(() => { + post = sinon.stub(https, 'request'); + }); + + afterEach(() => { + https.request.restore(); + }); + + it('streamReport success', function(done) { + let report = new AdwordsReport({}); + + sinon.stub(report, 'getHeaders').yields(null, {}); + sinon.stub(report, 'buildParams').returns({}); + + let responseBody = "Header1,Header2,Header3\n1,2,3\n"; + + let response = new MockRes(); + + let request = new MockReq({ + method: 'POST' + }); + + let dataSpy = sinon.spy(); + response.on('data', dataSpy); + + post.yields(response).returns(request); + + let callback = (res) => { + assert.equal(res, response); + done(); + }; + + report.streamReport('v201809', { header: 'test' }, callback); + }); + + it('should error out on getHeaders', function(done){ + let report = new AdwordsReport({}); + + sinon.stub(report, 'getHeaders').yields({ error: 'error'}, {}); + + let callback = function(_error, status) { + if (!status) { + return done(); + } + + done(new Error('Should have errored with bad access token')); + }; + + report.streamReport('v201809', { header: 'test' }, callback); + }); + + it('should error out on post', function(done) { + let report = new AdwordsReport({}); + + sinon.stub(report, 'getHeaders').yields(null, {}); + sinon.stub(report, 'buildParams').returns({}); + + let callback = function(_error, status) { + if (!status) { + return done(); + } + + done(new Error('Should have errored')); + }; + + let response = new MockRes(); + response.statusCode = 400; + + let request = new MockReq({ + method: 'POST' + }); + + post.yields(response).returns(request); + + report.streamReport('v201809', { header: 'test' }, callback); + }); + }); + if (!config) { return console.log('Adwords User not configured, skipping ReportService Service tests'); } @@ -117,5 +206,4 @@ describe('ReportService', function() { }, done); }); }); - });