From 867b8ea2020cceab29aa2fa17e48fb184ac19975 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Sun, 10 Nov 2019 06:40:31 +0100 Subject: [PATCH] - WIP: implement data pre-processor for #101 - implement checks for #101 - add some specs --- test/js-specs.js | 99 ++++++++++++++++++++++++++++++------- test/mocks/Console.mock.js | 11 +++++ test/mocks/FormData.mock.js | 41 +++++++++++++++ www/advanced-http.js | 5 +- www/dependency-validator.js | 22 +++++++++ www/helpers.js | 31 ++++++++++-- www/messages.js | 6 ++- 7 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 test/mocks/Console.mock.js create mode 100644 test/mocks/FormData.mock.js create mode 100644 www/dependency-validator.js diff --git a/test/js-specs.js b/test/js-specs.js index 823c944..6ba07ee 100644 --- a/test/js-specs.js +++ b/test/js-specs.js @@ -2,6 +2,9 @@ const chai = require('chai'); const mock = require('mock-require'); const should = chai.should(); +const ConsoleMock = require('./mocks/Console.mock'); +const FormDataMock = require('./mocks/FormData.mock'); + describe('Advanced HTTP public interface', function () { const messages = require('../www/messages'); @@ -17,7 +20,7 @@ describe('Advanced HTTP public interface', function () { const errorCodes = require('../www/error-codes'); const WebStorageCookieStore = require('../www/local-storage-store')(ToughCookie, lodash); const cookieHandler = require('../www/cookie-handler')(null, ToughCookie, WebStorageCookieStore); - const helpers = require('../www/helpers')(jsUtil, cookieHandler, messages, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, cookieHandler, messages, errorCodes); const urlUtil = require('../www/url-util')(jsUtil); return { exec: noop, cookieHandler, urlUtil: urlUtil, helpers, globalConfigs, errorCodes }; @@ -265,13 +268,13 @@ describe('Common helpers', function () { const init = require('../www/helpers'); init.debug = true; - const helpers = init(null, null, null); + const helpers = init(null, null, null, null, null, null); it('merges empty header sets correctly', () => { helpers.mergeHeaders({}, {}).should.eql({}); }); - it('merges ssimple header sets without collision correctly', () => { + it('merges simple header sets without collision correctly', () => { helpers.mergeHeaders({ a: 1 }, { b: 2 }).should.eql({ a: 1, b: 2 }); }); @@ -282,13 +285,13 @@ describe('Common helpers', function () { describe('getCookieHeader(url)', function () { it('resolves cookie header correctly when no cookie is set #198', () => { - const helpers = require('../www/helpers')(null, { getCookieString: () => '' }, null); + const helpers = require('../www/helpers')(null, null, { getCookieString: () => '' }, null); helpers.getCookieHeader('http://ilkimen.net').should.eql({}); }); it('resolves cookie header correctly when a cookie is set', () => { - const helpers = require('../www/helpers')(null, { getCookieString: () => 'cookie=value' }, null); + const helpers = require('../www/helpers')(null, null, { getCookieString: () => 'cookie=value' }, null); helpers.getCookieHeader('http://ilkimen.net').should.eql({ Cookie: 'cookie=value' }); }); @@ -297,7 +300,7 @@ describe('Common helpers', function () { describe('checkClientAuthOptions()', function () { const jsUtil = require('../www/js-util'); const messages = require('../www/messages'); - const helpers = require('../www/helpers')(jsUtil, null, messages); + const helpers = require('../www/helpers')(null, jsUtil, null, messages); it('returns options object with empty values when mode is "none" and no options are given', () => { helpers.checkClientAuthOptions('none').should.eql({ @@ -362,7 +365,7 @@ describe('Common helpers', function () { describe('handleMissingOptions()', function () { const jsUtil = require('../www/js-util'); const messages = require('../www/messages'); - const helpers = require('../www/helpers')(jsUtil, null, messages); + const helpers = require('../www/helpers')(null, jsUtil, null, messages); const mockGlobals = { headers: {}, serializer: 'urlencoded', @@ -394,7 +397,7 @@ describe('Common helpers', function () { }; it('does not change response data if it is an ArrayBuffer', () => { - const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes); const buffer = new ArrayBuffer(5); const handler = helpers.injectRawResponseHandler( 'arraybuffer', @@ -406,7 +409,7 @@ describe('Common helpers', function () { it('does not change response data if it is a Blob', () => { const fakeJsUtil = { getTypeOf: () => 'Blob' }; - const helpers = require('../www/helpers')(fakeJsUtil, null, messages, null, errorCodes); + const helpers = require('../www/helpers')(null, fakeJsUtil, null, messages, null, errorCodes); const handler = helpers.injectRawResponseHandler( 'blob', response => response.data.should.be.equal('fakeData') @@ -416,7 +419,7 @@ describe('Common helpers', function () { }); it('does not change response data if response type is "text"', () => { - const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes); const example = 'exampleText'; const handler = helpers.injectRawResponseHandler( 'text', @@ -428,7 +431,7 @@ describe('Common helpers', function () { it('handles response type "json" correctly', () => { const fakeData = { myString: 'bla', myNumber: 10 }; - const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes); const handler = helpers.injectRawResponseHandler( 'json', response => response.data.should.be.eql(fakeData) @@ -438,7 +441,7 @@ describe('Common helpers', function () { }); it('handles response type "arraybuffer" correctly', () => { - const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes); const handler = helpers.injectRawResponseHandler( 'arraybuffer', response => response.data.should.be.equal('fakeArrayBuffer') @@ -448,7 +451,7 @@ describe('Common helpers', function () { }); it('handles response type "blob" correctly', () => { - const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes); const handler = helpers.injectRawResponseHandler( 'blob', (response) => { @@ -462,7 +465,7 @@ describe('Common helpers', function () { }); it('calls failure callback when post-processing fails', () => { - const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes); const handler = helpers.injectRawResponseHandler( 'json', null, @@ -476,10 +479,10 @@ describe('Common helpers', function () { }); }); - describe('checkUploadFileOptions()', function() { + describe('checkUploadFileOptions()', function () { const jsUtil = require('../www/js-util'); const messages = require('../www/messages'); - const helpers = require('../www/helpers')(jsUtil, null, messages, null, null); + const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, null); it('checks valid file options correctly', () => { const opts = { @@ -508,4 +511,66 @@ describe('Common helpers', function () { (() => helpers.checkUploadFileOptions(['file://path/to/file.png'], [1])).should.throw(messages.NAMES_TYPE_MISMATCH); }); }); -}) + + describe('getProcessedData()', function () { + const jsUtil = require('../www/js-util'); + const messages = require('../www/messages'); + const dependencyValidator = require('../www/dependency-validator')(FormDataMock, null, messages); + const helpers = require('../www/helpers')({ FormData: FormDataMock }, jsUtil, null, messages, null, null, dependencyValidator); + + it('throws an error when given data does not match allowed data types', () => { + (() => helpers.getProcessedData('myString', 'urlencoded')).should.throw(messages.TYPE_MISMATCH_DATA); + (() => helpers.getProcessedData('myString', 'json')).should.throw(messages.TYPE_MISMATCH_DATA); + (() => helpers.getProcessedData({}, 'utf8')).should.throw(messages.TYPE_MISMATCH_DATA); + }); + + it('throws an error when needed Web API is not available', () => { + const helpers = require('../www/helpers')({}, jsUtil, null, messages, null, null); + (() => helpers.getProcessedData(null, 'multipart')).should.throw(`${messages.INSTANCE_TYPE_NOT_SUPPORTED} FormData`); + }); + + it('throws an error when given data does not match allowed instance types', () => { + (() => helpers.getProcessedData('myString', 'multipart')).should.throw(messages.INSTANCE_TYPE_MISMATCH_DATA); + }); + + it('processes data correctly when serializer "utf8" is configured', () => { + helpers.getProcessedData('myString', 'utf8').should.be.eql({text: 'myString'}); + }); + + it('processes data correctly when serializer "multipart" is configured', () => { + helpers.getProcessedData(new FormDataMock(), 'multipart'); + }); + }); +}); + +describe('Dependency Validator', function () { + const messages = require('../www/messages'); + + describe('logWarnings()', function () { + it('logs a warning message if FormData API is not supported', function () { + const console = new ConsoleMock(); + + require('../www/dependency-validator')(undefined, console, messages).logWarnings(); + + console.messageList.length.should.be.equal(1); + console.messageList[0].type.should.be.equal('warn'); + console.messageList[0].message.should.be.eql([messages.MISSING_FORMDATA_API]); + }); + + it('logs a warning message if FormData.entries() API is not supported', function () { + const console = new ConsoleMock(); + + require('../www/dependency-validator')({}, console, messages).logWarnings(); + + console.messageList.length.should.be.equal(1); + console.messageList[0].type.should.be.equal('warn'); + console.messageList[0].message.should.be.eql([messages.MISSING_FORMDATA_ENTRIES_API]); + }); + }); + + describe('checkFormDataApi()', function () { + it('throws an error if FormData.entries() API is not supported', function () { + (() => require('../www/dependency-validator')(null, null, messages).checkFormDataApi()).should.throw(messages.MISSING_FORMDATA_ENTRIES_API); + }); + }); +}); diff --git a/test/mocks/Console.mock.js b/test/mocks/Console.mock.js new file mode 100644 index 0000000..1498b20 --- /dev/null +++ b/test/mocks/Console.mock.js @@ -0,0 +1,11 @@ +module.exports = class ConsoleMock { + constructor() { + this.messageList = []; + } + + debug(...message) { this.messageList.push({ type: 'debug', message }); } + error(...message) { this.messageList.push({ type: 'error', message }); } + log(...message) { this.messageList.push({ type: 'log', message }); } + info(...message) { this.messageList.push({ type: 'info', message }); } + warn(...message) { this.messageList.push({ type: 'warn', message }); } +} diff --git a/test/mocks/FormData.mock.js b/test/mocks/FormData.mock.js new file mode 100644 index 0000000..186c3c7 --- /dev/null +++ b/test/mocks/FormData.mock.js @@ -0,0 +1,41 @@ +module.exports = class FormDataMock { + append() { + throw new Error('Not implemented in FormDataMock.'); + } + + delete() { + throw new Error('Not implemented in FormDataMock.'); + } + + entries() { + throw new Error('Not implemented in FormDataMock.'); + } + + forEach() { + throw new Error('Not implemented in FormDataMock.'); + } + + get() { + throw new Error('Not implemented in FormDataMock.'); + } + + getAll() { + throw new Error('Not implemented in FormDataMock.'); + } + + has() { + throw new Error('Not implemented in FormDataMock.'); + } + + keys() { + throw new Error('Not implemented in FormDataMock.'); + } + + set() { + throw new Error('Not implemented in FormDataMock.'); + } + + values() { + throw new Error('Not implemented in FormDataMock.'); + } +}; diff --git a/www/advanced-http.js b/www/advanced-http.js index ee5faac..a5174a8 100644 --- a/www/advanced-http.js +++ b/www/advanced-http.js @@ -14,8 +14,11 @@ var ToughCookie = require(pluginId + '.tough-cookie'); var lodash = require(pluginId + '.lodash'); var WebStorageCookieStore = require(pluginId + '.local-storage-store')(ToughCookie, lodash); var cookieHandler = require(pluginId + '.cookie-handler')(window.localStorage, ToughCookie, WebStorageCookieStore); -var helpers = require(pluginId + '.helpers')(jsUtil, cookieHandler, messages, base64, errorCodes); +var dependencyValidator = require(pluginId + '.dependency-validator')(window.FormData, window.console, messages); +var helpers = require(pluginId + '.helpers')(window, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator); var urlUtil = require(pluginId + '.url-util')(jsUtil); var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes); +dependencyValidator.logWarnings(); + module.exports = publicInterface; diff --git a/www/dependency-validator.js b/www/dependency-validator.js new file mode 100644 index 0000000..7775322 --- /dev/null +++ b/www/dependency-validator.js @@ -0,0 +1,22 @@ +module.exports = function init(FormData, console, messages) { + var interface = { + checkFormDataApi: checkFormDataApi, + logWarnings: logWarnings, + }; + + return interface; + + function logWarnings() { + if (!FormData) { + console.warn(messages.MISSING_FORMDATA_API); + } else if (!FormData.prototype || !FormData.prototype.entries) { + console.warn(messages.MISSING_FORMDATA_ENTRIES_API); + } + } + + function checkFormDataApi() { + if (!FormData || !FormData.prototype || !FormData.prototype.entries) { + throw new Error(messages.MISSING_FORMDATA_ENTRIES_API); + } + } +}; diff --git a/www/helpers.js b/www/helpers.js index 1562357..bdab928 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -1,5 +1,5 @@ -module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCodes) { - var validSerializers = ['urlencoded', 'json', 'utf8']; +module.exports = function init(global, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator) { + var validSerializers = ['urlencoded', 'json', 'utf8', 'multipart']; var validCertModes = ['default', 'nocheck', 'pinned', 'legacy']; var validClientAuthModes = ['none', 'systemstore', 'buffer']; var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download']; @@ -270,7 +270,7 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod entry.isFile = rawEntry.isFile; entry.name = rawEntry.name; entry.fullPath = rawEntry.fullPath; - entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == window.PERSISTENT ? 'persistent' : 'temporary')); + entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == global.PERSISTENT ? 'persistent' : 'temporary')); entry.nativeURL = rawEntry.nativeURL; return entry; @@ -363,16 +363,31 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod return ['String']; case 'urlencoded': return ['Object']; - default: + case 'json': return ['Array', 'Object']; + default: + return []; } } + function getAllowedInstanceType(dataSerializer) { + return dataSerializer === 'multipart' ? 'FormData' : null; + } + function getProcessedData(data, dataSerializer) { var currentDataType = jsUtil.getTypeOf(data); var allowedDataTypes = getAllowedDataTypes(dataSerializer); + var allowedInstanceType = getAllowedInstanceType(dataSerializer); - if (allowedDataTypes.indexOf(currentDataType) === -1) { + if (allowedInstanceType && !global[allowedInstanceType]) { + throw new Error(messages.INSTANCE_TYPE_NOT_SUPPORTED + ' ' + allowedInstanceType); + } + + if (allowedInstanceType && !(data instanceof global[allowedInstanceType])) { + throw new Error(messages.INSTANCE_TYPE_MISMATCH_DATA + ' ' + allowedInstanceType); + } + + if (!allowedInstanceType && allowedDataTypes.indexOf(currentDataType) === -1) { throw new Error(messages.TYPE_MISMATCH_DATA + ' ' + allowedDataTypes.join(', ')); } @@ -380,6 +395,12 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod data = { text: data }; } + if (dataSerializer === 'multipart') { + dependencyValidator.checkFormDataApi(); + + // TODO + } + return data; } diff --git a/www/messages.js b/www/messages.js index 55489c3..1fec793 100644 --- a/www/messages.js +++ b/www/messages.js @@ -2,6 +2,8 @@ module.exports = { ADDING_COOKIES_NOT_SUPPORTED: 'advanced-http: "setHeader" does not support adding cookies, please use "setCookie" function instead', EMPTY_FILE_PATHS: 'advanced-http: "filePaths" option array must not be empty, ', EMPTY_NAMES: 'advanced-http: "names" option array must not be empty, ', + INSTANCE_TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following instance types:', + INSTANCE_TYPE_NOT_SUPPORTED: 'advanced-http: this webview does not support following Web API which is needed for this plugin:', INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or undefined, ', INVALID_CLIENT_AUTH_MODE: 'advanced-http: invalid client certificate authentication mode, supported modes are:', INVALID_CLIENT_AUTH_OPTIONS: 'advanced-http: invalid client certificate authentication options, needs to be an dictionary style object', @@ -17,8 +19,10 @@ module.exports = { INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value, ', MANDATORY_FAIL: 'advanced-http: missing mandatory "onFail" callback function', MANDATORY_SUCCESS: 'advanced-http: missing mandatory "onSuccess" callback function', + MISSING_FORMDATA_API: 'advanced-http: FormData API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/FormData-API-requirements for more info.', + MISSING_FORMDATA_ENTRIES_API: 'advanced-http: This webview does not implement FormData API specification correctly, FormData.entries() is missing. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/FormData-API-requirements for more info.', POST_PROCESSING_FAILED: 'advanced-http: an error occured during post processing response:', - TYPE_MISMATCH_DATA: 'advanced-http: "data" option supports only following data types:', + TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following data types:', TYPE_MISMATCH_FILE_PATHS: 'advanced-http: "filePaths" option needs to be an string array, ', TYPE_MISMATCH_HEADERS: 'advanced-http: "headers" option needs to be an dictionary style object with string values, ', TYPE_MISMATCH_NAMES: 'advanced-http: "names" option needs to be an string array, ',