From dfd906900b2b0234f05abfc809074d8fff473f0b Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 13 Dec 2017 07:40:48 +0100 Subject: [PATCH] started to split humongous test.js into separate files --- js/common.js | 109 ++++++++++++++++++ js/test/Helper.js | 277 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 js/common.js create mode 100644 js/test/Helper.js diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..4454447 --- /dev/null +++ b/js/common.js @@ -0,0 +1,109 @@ +'use strict'; + +exports.a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m', + 'n','o','p','q','r','s','t','u','v','w','x','y','z']; +exports.alnumString = exports.a2zString.concat(['0','1','2','3','4','5','6','7','8','9']); +exports.queryString = exports.alnumString.concat(['+','%','&','.','*','-','_']); +exports.base64String = exports.alnumString.concat(['+','/','=']).concat( + exports.a2zString.map(function(c) { + return c.toUpperCase(); + }) +); +// schemas supported by the whatwg-url library +exports.schemas = ['ftp','gopher','http','https','ws','wss']; +exports.supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh']; +exports.mimeTypes = ['image/png', 'application/octet-stream']; + +global.jsc = require('jsverify'); +global.jsdom = require('jsdom-global'); +global.cleanup = global.jsdom(); +global.fs = require('fs'); + +/** + * character to HTML entity lookup table + * + * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} + */ +var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + }, + logFile = fs.createWriteStream('test.log'), + mimeFile = fs.createReadStream('/etc/mime.types'), + mimeLine = ''; + +global.$ = global.jQuery = require('./jquery-3.1.1'); +global.sjcl = require('./sjcl-1.0.6'); +global.Base64 = require('./base64-2.1.9').Base64; +global.RawDeflate = require('./rawdeflate-0.5').RawDeflate; +global.RawDeflate.inflate = require('./rawinflate-0.3').RawDeflate.inflate; +require('./prettify'); +global.prettyPrint = window.PR.prettyPrint; +global.prettyPrintOne = window.PR.prettyPrintOne; +global.showdown = require('./showdown-1.6.1'); +global.DOMPurify = require('./purify.min'); +require('./bootstrap-3.3.7'); +require('./privatebin'); + +// redirect console messages to log file +console.info = console.warn = console.error = function () { + logFile.write(Array.prototype.slice.call(arguments).join('') + '\n'); +} + +// populate mime types from environment +mimeFile.on('data', function(data) { + mimeLine += data; + var index = mimeLine.indexOf('\n'); + while (index > -1) { + var line = mimeLine.substring(0, index); + mimeLine = mimeLine.substring(index + 1); + parseMime(line); + index = mimeLine.indexOf('\n'); + } +}); + +mimeFile.on('end', function() { + if (mimeLine.length > 0) { + parseMime(mimeLine); + } +}); + +function parseMime(line) { + // ignore comments + var index = line.indexOf('#'); + if (index > -1) { + line = line.substring(0, index); + } + + // ignore bits after tabs + index = line.indexOf('\t'); + if (index > -1) { + line = line.substring(0, index); + } + if (line.length > 0) { + exports.mimeTypes.push(line); + } +} + +/** + * convert all applicable characters to HTML entities + * + * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} + * @name htmlEntities + * @function + * @param {string} str + * @return {string} escaped HTML + */ +exports.htmlEntities = function(str) { + return String(str).replace( + /[&<>"'`=\/]/g, function(s) { + return entityMap[s]; + }); +} + diff --git a/js/test/Helper.js b/js/test/Helper.js new file mode 100644 index 0000000..81ea54c --- /dev/null +++ b/js/test/Helper.js @@ -0,0 +1,277 @@ +'use strict'; +var common = require('../common'); + +describe('Helper', function () { + describe('secondsToHuman', function () { + after(function () { + cleanup(); + }); + + jsc.property('returns an array with a number and a word', 'integer', function (number) { + var result = $.PrivateBin.Helper.secondsToHuman(number); + return Array.isArray(result) && + result.length === 2 && + result[0] === parseInt(result[0], 10) && + typeof result[1] === 'string'; + }); + jsc.property('returns seconds on the first array position', 'integer 59', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === number; + }); + jsc.property('returns seconds on the second array position', 'integer 59', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'second'; + }); + jsc.property('returns minutes on the first array position', 'integer 60 3599', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60); + }); + jsc.property('returns minutes on the second array position', 'integer 60 3599', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'minute'; + }); + jsc.property('returns hours on the first array position', 'integer 3600 86399', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60)); + }); + jsc.property('returns hours on the second array position', 'integer 3600 86399', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'hour'; + }); + jsc.property('returns days on the first array position', 'integer 86400 5184000', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24)); + }); + jsc.property('returns days on the second array position', 'integer 86400 5184000', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'day'; + }); + // max safe integer as per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5 + jsc.property('returns months on the first array position', 'integer 5184000 9007199254740991', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30)); + }); + jsc.property('returns months on the second array position', 'integer 5184000 9007199254740991', function (number) { + return $.PrivateBin.Helper.secondsToHuman(number)[1] === 'month'; + }); + }); + + // this test is not yet meaningful using jsdom, as it does not contain getSelection support. + // TODO: This needs to be tested using a browser. + describe('selectText', function () { + this.timeout(30000); + jsc.property( + 'selection contains content of given ID', + jsc.nearray(jsc.nearray(jsc.elements(common.alnumString))), + 'nearray string', + function (ids, contents) { + var html = '', + result = true; + ids.forEach(function(item, i) { + html += '
' + common.htmlEntities(contents[i] || contents[0]) + '
'; + }); + var clean = jsdom(html); + ids.forEach(function(item, i) { + $.PrivateBin.Helper.selectText(item.join('')); + // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet. + // Once there is one, uncomment the line below to actually check the result. + //result *= (contents[i] || contents[0]) === window.getSelection().toString(); + }); + clean(); + return Boolean(result); + } + ); + }); + + describe('urls2links', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'ignores non-URL content', + 'string', + function (content) { + return content === $.PrivateBin.Helper.urls2links(content); + } + ); + jsc.property( + 'replaces URLs with anchors', + 'string', + jsc.elements(['http', 'https', 'ftp']), + jsc.nearray(jsc.elements(common.a2zString)), + jsc.array(jsc.elements(common.queryString)), + jsc.array(jsc.elements(common.queryString)), + 'string', + function (prefix, schema, address, query, fragment, postfix) { + var query = query.join(''), + fragment = fragment.join(''), + url = schema + '://' + address.join('') + '/?' + query + '#' + fragment, + prefix = common.htmlEntities(prefix), + postfix = ' ' + common.htmlEntities(postfix); + + // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. � or &#x + if ( + query.slice(-1) === '&' && + (parseInt(fragment.substring(0, 1), 10) >= 0 || fragment.charAt(0) === 'x' ) + ) + { + url = schema + '://' + address.join('') + '/?' + query.substring(0, query.length - 1); + postfix = ''; + } + + return prefix + '' + url + '' + postfix === $.PrivateBin.Helper.urls2links(prefix + url + postfix); + } + ); + jsc.property( + 'replaces magnet links with anchors', + 'string', + jsc.array(jsc.elements(common.queryString)), + 'string', + function (prefix, query, postfix) { + var url = 'magnet:?' + query.join('').replace(/^&+|&+$/gm,''), + prefix = common.htmlEntities(prefix), + postfix = common.htmlEntities(postfix); + return prefix + '' + url + ' ' + postfix === $.PrivateBin.Helper.urls2links(prefix + url + ' ' + postfix); + } + ); + }); + + describe('sprintf', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'replaces %s in strings with first given parameter', + 'string', + '(small nearray) string', + 'string', + function (prefix, params, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + params[0] = params[0].replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var result = prefix + params[0] + postfix; + params.unshift(prefix + '%s' + postfix); + return result === $.PrivateBin.Helper.sprintf.apply(this, params); + } + ); + jsc.property( + 'replaces %d in strings with first given parameter', + 'string', + '(small nearray) nat', + 'string', + function (prefix, params, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var result = prefix + params[0] + postfix; + params.unshift(prefix + '%d' + postfix); + return result === $.PrivateBin.Helper.sprintf.apply(this, params); + } + ); + jsc.property( + 'replaces %d in strings with 0 if first parameter is not a number', + 'string', + '(small nearray) falsy', + 'string', + function (prefix, params, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var result = prefix + '0' + postfix; + params.unshift(prefix + '%d' + postfix); + return result === $.PrivateBin.Helper.sprintf.apply(this, params) + } + ); + jsc.property( + 'replaces %d and %s in strings in order', + 'string', + 'nat', + 'string', + 'string', + 'string', + function (prefix, uint, middle, string, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + middle = middle.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var params = [prefix + '%d' + middle + '%s' + postfix, uint, string], + result = prefix + uint + middle + string + postfix; + return result === $.PrivateBin.Helper.sprintf.apply(this, params); + } + ); + jsc.property( + 'replaces %d and %s in strings in reverse order', + 'string', + 'nat', + 'string', + 'string', + 'string', + function (prefix, uint, middle, string, postfix) { + prefix = prefix.replace(/%(s|d)/g, '%%'); + middle = middle.replace(/%(s|d)/g, '%%'); + postfix = postfix.replace(/%(s|d)/g, '%%'); + var params = [prefix + '%s' + middle + '%d' + postfix, string, uint], + result = prefix + string + middle + uint + postfix; + return result === $.PrivateBin.Helper.sprintf.apply(this, params); + } + ); + }); + + describe('getCookie', function () { + this.timeout(30000); + jsc.property( + 'returns the requested cookie', + 'nearray asciinestring', + 'nearray asciistring', + function (labels, values) { + var selectedKey = '', selectedValue = '', + cookieArray = [], + count = 0; + labels.forEach(function(item, i) { + // deliberatly using a non-ascii key for replacing invalid characters + var key = item.replace(/[\s;,=]/g, Array(i+2).join('£')), + value = (values[i] || values[0]).replace(/[\s;,=]/g, ''); + cookieArray.push(key + '=' + value); + if (Math.random() < 1 / i || selectedKey === key) + { + selectedKey = key; + selectedValue = value; + } + }); + var clean = jsdom('', {cookie: cookieArray}), + result = $.PrivateBin.Helper.getCookie(selectedKey); + clean(); + return result === selectedValue; + } + ); + }); + + describe('baseUri', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'returns the URL without query & fragment', + jsc.elements(common.schemas), + jsc.nearray(jsc.elements(common.a2zString)), + jsc.array(jsc.elements(common.queryString)), + 'string', + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', {url: expected + '?' + query.join('') + '#' + fragment}), + result = $.PrivateBin.Helper.baseUri(); + $.PrivateBin.Helper.reset(); + clean(); + return expected === result; + } + ); + }); + + describe('htmlEntities', function () { + after(function () { + cleanup(); + }); + + jsc.property( + 'removes all HTML entities from any given string', + 'string', + function (string) { + var result = common.htmlEntities(string); + return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); + } + ); + }); +}); +