From 053d54571d4e852db7735000a0382b32d1d0714d Mon Sep 17 00:00:00 2001 From: Luan Date: Fri, 15 Jun 2018 17:31:27 -0300 Subject: [PATCH] feat(FRC): Add Complaince to FRC 7234 until Section 4 Add FRC File and tests for it and implement test until section 4 wip #1 --- index.js | 100 ++------------ lib/frc.js | 90 +++++++++++++ lib/helpers.js | 122 ++++++++++++++++++ test/frc.hasUnallowedParam.test.js | 16 +++ test/frc.isFRCComplaint.test.js | 19 +++ ...> helpers.convertStringIntoObject.test.js} | 8 +- test/helpers.convertToString.test.js | 19 +++ ...helpers.hasDuplicatedConfiguration.test.js | 17 +++ ...ric.test.js => helpers.isNumberic.test.js} | 8 +- ...nfig.test.js => index.applyConfig.test.js} | 2 +- test/{init.test.js => index.init.test.js} | 24 +++- 11 files changed, 325 insertions(+), 100 deletions(-) create mode 100644 lib/frc.js create mode 100644 lib/helpers.js create mode 100644 test/frc.hasUnallowedParam.test.js create mode 100644 test/frc.isFRCComplaint.test.js rename test/{convertStringIntoObject.test.js => helpers.convertStringIntoObject.test.js} (62%) create mode 100644 test/helpers.convertToString.test.js create mode 100644 test/helpers.hasDuplicatedConfiguration.test.js rename test/{isNumberic.test.js => helpers.isNumberic.test.js} (92%) rename test/{applyConfig.test.js => index.applyConfig.test.js} (95%) rename test/{init.test.js => index.init.test.js} (52%) diff --git a/index.js b/index.js index e5814fa..d76ab0f 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,7 @@ -const cacheValidation = { +const Helpers = require('./lib/helpers'); +const FRC = require('./lib/frc'); - allCaches: [ 'public', 'private', 'no-cache', 'only-if-cached' ], - allExpirationPrefix: [ 'max-age', 's-maxage', 'max-stale', 'min-fresh', 'stale-while-revalidate', 'stale-if-error' ], - allOtherCacheConfig: [ 'no-store', 'no-transform' ], - allRevalidations: [ 'must-revalidate', 'proxy-revalidate', 'immutable' ], +const cacheValidation = { applyConfig(finalObject, config) { if(!config) { @@ -15,105 +13,37 @@ const cacheValidation = { } if(config && config.returnString) { - return cacheValidation.convertToString(finalObject); + return Helpers.convertToString(finalObject); } return false; }, - convertStringIntoObject(cacheString) { - const cacheArray = cacheString.split(', '); - const cacheObject = {}; - let hasError = false; - - if(!cacheArray.length || (cacheArray.length === 1 && cacheString.includes(' '))) { - return { error: 'Cache string not valid' }; - } - - cacheArray.forEach(config => { - if(!config.includes('=')) { - cacheObject[config] = true; - return true; - } - - const expirationConfig = config.split('='); - const expirationNumber = expirationConfig[1]; - - if(!cacheValidation.isNumeric(expirationNumber)) { - hasError = true; - } - - cacheObject[expirationConfig[0]] = parseInt(expirationNumber, 10); - return true; - }); - - if(hasError) { - return { error: 'Expect to find a number for configuration but found something else' }; - } - - return cacheObject; - }, - - convertToString(objectToConvert) { - let finalString = ''; - - Object.keys(objectToConvert).forEach(thisParam => { - if(finalString !== '') { - finalString += ', '; - } - - if(objectToConvert[thisParam] === true) { - finalString += `${thisParam}`; - return true; - } - - finalString += `${thisParam}=${objectToConvert[thisParam]}`; - }); - - return finalString; - }, - - hasUnallowedParam(object) { - let hasError = false; - const allAllowedParams = [ - ...cacheValidation.allCaches, - ...cacheValidation.allExpirationPrefix, - ...cacheValidation.allOtherCacheConfig, - ...cacheValidation.allRevalidations - ]; - - Object.keys(object).forEach(config => { - if(!allAllowedParams.includes(config)) { - hasError = true; - } - }); - - return hasError; - }, - init(cacheParam, config) { if(!cacheParam) { - return { error: 'No cache to validate' }; + return { error: 'No cache to validate. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }; } let cacheObject = cacheParam; if(typeof cacheParam === 'string') { - cacheObject = cacheValidation.convertStringIntoObject(cacheParam); + if(Helpers.hasDuplicatedConfiguration(cacheParam)) { + return { error: 'No duplicated configuration is allowed' }; + } + + cacheObject = Helpers.convertStringIntoObject(cacheParam); } if(cacheObject.error) { return cacheObject; } - if(cacheValidation.hasUnallowedParam(cacheObject)) { - return { error: 'Invalid Cache config' }; + const isFRCComplaint = FRC.isFRCComplaint(cacheObject); + + if(isFRCComplaint.error) { + return isFRCComplaint; } return cacheValidation.applyConfig(cacheObject, config); - }, - - isNumeric(varToVerify) { - return !isNaN(parseFloat(varToVerify)) && !Array.isArray(varToVerify) && Number.isInteger(Number(varToVerify)) && (Number(varToVerify) === 0 || Number(varToVerify) >= 1); } }; @@ -124,5 +54,3 @@ module.exports = cacheValidation.init; if(process.env.NODE_ENV === 'test') { module.exports.testObject = cacheValidation; } - - diff --git a/lib/frc.js b/lib/frc.js new file mode 100644 index 0000000..3c47231 --- /dev/null +++ b/lib/frc.js @@ -0,0 +1,90 @@ +const FRC = { + + allCaches: [ 'public', 'private', 'no-cache', 'only-if-cached' ], + allExpirationPrefix: [ 'max-age', 's-maxage', 'max-stale', 'min-fresh', 'stale-while-revalidate', 'stale-if-error' ], + allOtherCacheConfig: [ 'no-store', 'no-transform' ], + allRevalidations: [ 'must-revalidate', 'proxy-revalidate', 'immutable' ], + + /** + * THis function check to see if there is any unallowed parameter. See that the allowed ones are in these arrays above + * + * @param {Object} cacheObject - An Cache string trasnformed in object + * @returns {Boolean} - Return true if all params are allowed + */ + hasUnallowedParam(cacheObject) { + let hasError = false; + const allAllowedParams = [ + ...FRC.allCaches, + ...FRC.allExpirationPrefix, + ...FRC.allOtherCacheConfig, + ...FRC.allRevalidations + ]; + + Object.keys(cacheObject).forEach(config => { + if(!allAllowedParams.includes(config)) { + hasError = { error: `Unallowed paramemeter "${config}" found. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details` }; + } + }); + + return hasError; + }, + + /** + * This function check to see if the params are FRC Cpmplaint + * + * @param {Object} cacheObject - An Cache string trasnformed in object + * @returns {Boolean} Return true if the cache config is FRC Complaint + */ + isFRCComplaint(cacheObject) { + let isComplaint = true; + const hasUnallowedParam = FRC.hasUnallowedParam(cacheObject); + + if(hasUnallowedParam && hasUnallowedParam.error) { + isComplaint = hasUnallowedParam; + } + + if(!FRC.checkIfCacheIsEnabled(cacheObject)) { + isComplaint = { error: 'The params "no-store" or "private" must appear alone' }; + } + + return isComplaint; + }, + + /** + * This function check for complaint with FRC 7234 Section 1.2.1 + * + * @param {Number} cacheTime - An integer representing the time in secons + * @returns {Number} 2147483648 or the params itself + */ + maxCacheTime(cacheTime) { + if(cacheTime > 2147483648) { // eslint-disable-line no-magic-numbers + return 2147483648; // eslint-disable-line no-magic-numbers + } + + return cacheTime; + }, + + /** + * This function check for complaint with FRC 7234 Section 3 + * Is says that if no-store or private is set, there should be no cache + * + * @param {Object} cacheObject - An Cache string trasnformed in object + * @returns {Boolean} Return true if the varibles appear alone for they are not set + */ + checkIfCacheIsEnabled(cacheObject) { // eslint-disable-line sort-keys + const allCacheKeys = Object.keys(cacheObject); + + if(allCacheKeys.includes('no-store') && allCacheKeys.length > 1) { + return false; + } + + if(allCacheKeys.includes('private') && allCacheKeys.length > 1) { + return false; + } + + return true; + } + +}; + +module.exports = FRC; diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..3989a32 --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,122 @@ +const FRC = require('./frc'); + +const Helpers = { + + /** + * Converts a string with comma separation keys and = separetor value in an Object + * Where anything described as "Key=Value" will become { key: value }, anything represented only as "Key" will become { key: true } + * + * Ot only accept numbers as Values and empty valued keys + * If you send anything that ins't valid it will return an error object + * + * @example 'one, two=200, four, five=500' + * // returns { one: true, two: 2000, four: true, five: 500 } + * + * @param {String} cacheString - A String to be converted + * @returns {Object|Error} An Object with the result or a object with an error + */ + convertStringIntoObject(cacheString) { + const cacheArray = cacheString.split(', '); + const cacheObject = {}; + let hasError = false; + + if(!cacheArray.length || (cacheArray.length === 1 && cacheString.includes(' ')) || typeof cacheString !== 'string') { + return { error: 'Cache string not valid. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }; + } + + cacheArray.forEach(config => { + if(!config.includes('=')) { + cacheObject[config] = true; + return true; + } + + const expirationConfig = config.split('='); + let expirationNumber = parseInt(expirationConfig[1], 10); + + if(!Helpers.isNumeric(expirationConfig[1])) { + hasError = true; + } + + expirationNumber = FRC.maxCacheTime(expirationNumber); + + cacheObject[expirationConfig[0]] = expirationNumber; + return true; + }); + + if(hasError) { + return { error: 'Expect to find a number for configuration but found something else. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }; + } + + return cacheObject; + }, + + /** + * Converts an object into strings with comma separation. + * Where anything is described as "Key=Value", Boolean True only as "Key" and Boolean false is not represented + * If you send anything that ins't an object it will return an empty string + * + * @example { one: true, two: 200, three: false, four: true, five: 5000 } + * // returns 'one, two=200, four, five=5000' + * + * @param {Any} objectToConvert - Object to be converted + * @returns {String} An string with the result of the convertion + */ + convertToString(objectToConvert) { + let finalString = ''; + + if(objectToConvert && typeof objectToConvert !== 'object') { + return finalString; + } + + Object.keys(objectToConvert).forEach(thisParam => { + if(finalString !== '') { + finalString += ', '; + } + + if(objectToConvert[thisParam] === true) { + finalString += `${thisParam}`; + return true; + } + + if(objectToConvert[thisParam] === false) { + return true; + } + + finalString += `${thisParam}=${objectToConvert[thisParam]}`; + }); + + return finalString; + }, + + /** + * This function check to see if there is any duplicated configurations + * + * @param {Object} cacheString - A Cache String + * @returns {Boolean} - Return true if there are not duplicates + */ + hasDuplicatedConfiguration(cacheString) { // eslint-disable-line sort-keys + const allParamsArray = []; + + cacheString.split(', ').forEach(config => { + allParamsArray.push(config.split('=')[0]); + }); + + const filteredArray = [ ...new Set(allParamsArray) ]; // eslint-disable-line + + return filteredArray.length !== allParamsArray.length; + }, + + /** + * Check to see if the value is a numeric value. It can be a string or anything else if it can be converter to a number + * Also it doesn't allow numbers with floatation point and only greater than zero ones + * + * @param {Any} varToVerify - Value to verify + * @returns {Boolean} True if is a numeric value greater than zero and not decimal + */ + isNumeric(varToVerify) { + return !isNaN(parseFloat(varToVerify)) && !Array.isArray(varToVerify) && Number.isInteger(Number(varToVerify)) && (Number(varToVerify) === 0 || Number(varToVerify) >= 1); + } + +}; + +module.exports = Helpers; diff --git a/test/frc.hasUnallowedParam.test.js b/test/frc.hasUnallowedParam.test.js new file mode 100644 index 0000000..e2e5306 --- /dev/null +++ b/test/frc.hasUnallowedParam.test.js @@ -0,0 +1,16 @@ +const hasUnallowedParam = require('../lib/frc').hasUnallowedParam; + +describe('hasUnallowedParam', () => { + it('should have a global object', () => { + expect(hasUnallowedParam).toBeDefined(); + }); + + it('Should return false if isnt anything wrong', () => { + expect(hasUnallowedParam({ 'max-age': 36000, public: true })).toEqual(false); + expect(hasUnallowedParam({ 'no-store': true })).toEqual(false); + }); + + it('Should return error without = and without number value', () => { + expect(hasUnallowedParam({ publics: true })).toEqual({ error: 'Unallowed paramemeter "publics" found. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); + }); +}); diff --git a/test/frc.isFRCComplaint.test.js b/test/frc.isFRCComplaint.test.js new file mode 100644 index 0000000..7363655 --- /dev/null +++ b/test/frc.isFRCComplaint.test.js @@ -0,0 +1,19 @@ +const isFRCComplaint = require('../lib/frc').isFRCComplaint; + +describe('isFRCComplaint', () => { + it('should have a global object', () => { + expect(isFRCComplaint).toBeDefined(); + }); + + it('Should return true', () => { + expect(isFRCComplaint({ 'max-age': 36000, public: true })).toEqual(true); + expect(isFRCComplaint({ 'no-store': true })).toEqual(true); + expect(isFRCComplaint({ private: true })).toEqual(true); + }); + + it('Should return error because no-store and private inst alone', () => { + expect(isFRCComplaint({ 'max-age': 3600, private: true })).toEqual({ error: 'The params "no-store" or "private" must appear alone' }); + expect(isFRCComplaint({ 'max-age': 3600, 'no-store': true })).toEqual({ error: 'The params "no-store" or "private" must appear alone' }); + expect(isFRCComplaint({ 'no-store': true, private: true })).toEqual({ error: 'The params "no-store" or "private" must appear alone' }); + }); +}); diff --git a/test/convertStringIntoObject.test.js b/test/helpers.convertStringIntoObject.test.js similarity index 62% rename from test/convertStringIntoObject.test.js rename to test/helpers.convertStringIntoObject.test.js index b922390..59cb366 100644 --- a/test/convertStringIntoObject.test.js +++ b/test/helpers.convertStringIntoObject.test.js @@ -1,16 +1,16 @@ -const convertStringIntoObject = require('../index').testObject.convertStringIntoObject; +const convertStringIntoObject = require('../lib/helpers').convertStringIntoObject; describe('convertStringIntoObject', () => { - it('should have a global convertStringIntoObject object', () => { + it('should have a global object', () => { expect(convertStringIntoObject).toBeDefined(); }); it('Should return error without commas', () => { - expect(convertStringIntoObject('max-age=3600 must-revalidate')).toEqual({ error: 'Cache string not valid' }); + expect(convertStringIntoObject('max-age=3600 must-revalidate')).toEqual({ error: 'Cache string not valid. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); }); it('Should return error without = and without number value', () => { - expect(convertStringIntoObject('max-age=asd')).toEqual({ error: 'Expect to find a number for configuration but found something else' }); + expect(convertStringIntoObject('max-age=asd')).toEqual({ error: 'Expect to find a number for configuration but found something else. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); }); it('Should convert correctly', () => { diff --git a/test/helpers.convertToString.test.js b/test/helpers.convertToString.test.js new file mode 100644 index 0000000..7929562 --- /dev/null +++ b/test/helpers.convertToString.test.js @@ -0,0 +1,19 @@ +const convertToString = require('../lib/helpers').convertToString; + +describe('convertToString', () => { + it('should have a global object', () => { + expect(convertToString).toBeDefined(); + }); + + it('Should return empty string if param is not object', () => { + expect(convertToString('max-age=3600 must-revalidate')).toEqual(''); + }); + + it('Should return ignore if param has a key with false as value', () => { + expect(convertToString({ one: false, two: 3600 })).toEqual('two=3600'); + }); + + it('Should convert string corretly', () => { + expect(convertToString({ one: true, two: 3600, three: true, four: 1000 })).toEqual('one, two=3600, three, four=1000'); // eslint-disable-line sort-keys + }); +}); diff --git a/test/helpers.hasDuplicatedConfiguration.test.js b/test/helpers.hasDuplicatedConfiguration.test.js new file mode 100644 index 0000000..c28deaf --- /dev/null +++ b/test/helpers.hasDuplicatedConfiguration.test.js @@ -0,0 +1,17 @@ +const hasDuplicatedConfiguration = require('../lib/helpers').hasDuplicatedConfiguration; + +describe('hasDuplicatedConfiguration', () => { + it('should have a global object', () => { + expect(hasDuplicatedConfiguration).toBeDefined(); + }); + + it('Should return false if there is no duplicated', () => { + expect(hasDuplicatedConfiguration('max-age=3600, must-revalidate')).toEqual(false); + }); + + it('Should return true, if there is duplicated values', () => { + expect(hasDuplicatedConfiguration('max-age=3600, must-revalidate, must-revalidate')).toEqual(true); + expect(hasDuplicatedConfiguration('max-age=3600, must-revalidate, max-age=3600')).toEqual(true); + expect(hasDuplicatedConfiguration('max-age=3600, must-revalidate, max-age=36000')).toEqual(true); + }); +}); diff --git a/test/isNumberic.test.js b/test/helpers.isNumberic.test.js similarity index 92% rename from test/isNumberic.test.js rename to test/helpers.isNumberic.test.js index ec845a2..14052d2 100644 --- a/test/isNumberic.test.js +++ b/test/helpers.isNumberic.test.js @@ -1,7 +1,7 @@ -const isNumeric = require('../index').testObject.isNumeric; +const isNumeric = require('../lib/helpers').isNumeric; describe('isNumeric', () => { - it('should have a global isNumeric object', () => { + it('should have a global object', () => { expect(isNumeric).toBeDefined(); }); @@ -19,6 +19,8 @@ describe('isNumeric', () => { it('should return true and false for max / min numbers', () => { expect(isNumeric(Number.MAX_VALUE)).toBeTruthy(); expect(isNumeric(Number.MIN_VALUE)).toBeFalsy(); + expect(isNumeric(2147483648)).toBeTruthy(); // eslint-disable-line no-magic-numbers + expect(isNumeric(2147483649)).toBeTruthy(); // eslint-disable-line no-magic-numbers }); it('should return true for hexadecimals', () => { @@ -55,7 +57,7 @@ describe('isNumeric', () => { expect(isNumeric('1,1,1,1')).toBeFalsy(); }); - it('should return false for empty / whitespace',() => { + it('should return false for empty / whitespace', () => { expect(isNumeric()).toBeFalsy(); expect(isNumeric('')).toBeFalsy(); expect(isNumeric(' ')).toBeFalsy(); diff --git a/test/applyConfig.test.js b/test/index.applyConfig.test.js similarity index 95% rename from test/applyConfig.test.js rename to test/index.applyConfig.test.js index 4243588..58ee47c 100644 --- a/test/applyConfig.test.js +++ b/test/index.applyConfig.test.js @@ -1,7 +1,7 @@ const applyConfig = require('../index').testObject.applyConfig; describe('applyConfig', () => { - it('Should have a global applyConfig object', () => { + it('Should have a global object', () => { expect(applyConfig).toBeFunction(); }); diff --git a/test/init.test.js b/test/index.init.test.js similarity index 52% rename from test/init.test.js rename to test/index.init.test.js index e7d7031..f42c3af 100644 --- a/test/init.test.js +++ b/test/index.init.test.js @@ -1,16 +1,22 @@ const init = require('../index').testObject.init; describe('init', () => { - it('should have a global init object', () => { + it('should have a global object', () => { expect(init).toBeFunction(); }); it('should return an error if not pass a parameter', () => { - expect(init()).toEqual({ error: 'No cache to validate' }); + expect(init()).toEqual({ error: 'No cache to validate. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); }); - it('should return an error if pass a parameter that\'s not a cahce config', () => { - expect(init('asd')).toEqual({ error: 'Invalid Cache config' }); + it('should return an error if pass a parameter that\'s not a cache config', () => { + expect(init('asd')).toEqual({ error: 'Unallowed paramemeter "asd" found. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); + }); + + it('should return an error 2147483648 if passed a number grater than that', () => { + expect(init('max-age=2147483649')).toEqual(true); + expect(init('max-age=2147483650', { returnObject: true })).toEqual({ 'max-age': 2147483648 }); + expect(init('max-age=2147483651', { returnString: true })).toEqual('max-age=2147483648'); }); it('should return correctly if a string is sent', () => { @@ -24,6 +30,12 @@ describe('init', () => { expect(init('max-age=36000, public', { returnString: true })).toEqual('max-age=36000, public'); }); + it('should return duplicated error', () => { + expect(init('max-age=3600, must-revalidate, must-revalidate')).toEqual({ error: 'No duplicated configuration is allowed' }); + expect(init('max-age=3600, must-revalidate, max-age=3600')).toEqual({ error: 'No duplicated configuration is allowed' }); + expect(init('max-age=3600, must-revalidate, max-age=36000')).toEqual({ error: 'No duplicated configuration is allowed' }); + }); + it('should return correctly if an object is sent', () => { expect(init({ 'max-age': 36000 })).toEqual(true); expect(init({ 'max-age': 36000 }, { returnObject: true })).toEqual({ 'max-age': 36000 }); @@ -36,7 +48,7 @@ describe('init', () => { }); it('Should return an error from parsing the initial param', () => { - expect(init('max-age=asd')).toEqual({ error: 'Expect to find a number for configuration but found something else' }); - expect(init('max-age=3600 must-revalidate')).toEqual({ error: 'Cache string not valid' }); + expect(init('max-age=asd')).toEqual({ error: 'Expect to find a number for configuration but found something else. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); + expect(init('max-age=3600 must-revalidate')).toEqual({ error: 'Cache string not valid. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details' }); }); });