Skip to content

Commit

Permalink
feat(FRC): Add Complaince to FRC 7234 until Section 4
Browse files Browse the repository at this point in the history
Add FRC File and tests for it and implement test until section 4

wip #1
  • Loading branch information
luanmuniz committed Jun 15, 2018
1 parent 10be25e commit 053d545
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 100 deletions.
100 changes: 14 additions & 86 deletions index.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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);
}

};
Expand All @@ -124,5 +54,3 @@ module.exports = cacheValidation.init;
if(process.env.NODE_ENV === 'test') {
module.exports.testObject = cacheValidation;
}


90 changes: 90 additions & 0 deletions lib/frc.js
Original file line number Diff line number Diff line change
@@ -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;
122 changes: 122 additions & 0 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 16 additions & 0 deletions test/frc.hasUnallowedParam.test.js
Original file line number Diff line number Diff line change
@@ -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' });
});
});
19 changes: 19 additions & 0 deletions test/frc.isFRCComplaint.test.js
Original file line number Diff line number Diff line change
@@ -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' });
});
});
Loading

0 comments on commit 053d545

Please sign in to comment.