'use strict'; var jsc = require('jsverify'), jsdom = require('jsdom-global'), cleanup = jsdom(), base64lib = require('./base64-2.1.9'), rawdeflatelib = require('./rawdeflate-0.5'), rawinflatelib = require('./rawinflate-0.3'), 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'], alnumString = a2zString.concat(['0','1','2','3','4','5','6','7','8','9']), queryString = alnumString.concat(['+','%','&','.','*','-','_']), base64String = alnumString.concat(['+','/','=']).concat( a2zString.map(function(c) { return c.toUpperCase(); }) ), // schemas supported by the whatwg-url library schemas = ['ftp','gopher','http','https','ws','wss'], supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], logFile = require('fs').createWriteStream('test.log'); global.$ = global.jQuery = require('./jquery-3.1.1'); global.sjcl = require('./sjcl-1.0.6'); global.Base64 = base64lib.Base64; global.RawDeflate = rawdeflatelib.RawDeflate; global.RawDeflate.inflate = rawinflatelib.RawDeflate.inflate; require('./privatebin'); // redirect console messages to log file console.warn = console.error = function (msg) { logFile.write(msg + '\n'); } 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 () { jsc.property( 'selection contains content of given ID', jsc.nearray(jsc.nearray(jsc.elements(alnumString))), 'nearray string', function (ids, contents) { var html = '', result = true; ids.forEach(function(item, i) { html += '
' + $.PrivateBin.Helper.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('setElementText', function () { after(function () { cleanup(); }); jsc.property( 'replaces the content of an element', jsc.nearray(jsc.nearray(jsc.elements(alnumString))), 'nearray string', 'string', function (ids, contents, replacingContent) { var html = '', result = true; ids.forEach(function(item, i) { html += '
' + $.PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '
'; }); var elements = $('').html(html); ids.forEach(function(item, i) { var id = item.join(''), element = elements.find('#' + id).first(); $.PrivateBin.Helper.setElementText(element, replacingContent); result *= replacingContent === element.text(); }); return Boolean(result); } ); }); describe('urls2links', function () { after(function () { cleanup(); }); jsc.property( 'ignores non-URL content', 'string', function (content) { var element = $('
' + content + '
'), before = element.html(); $.PrivateBin.Helper.urls2links(element); return before === element.html(); } ); jsc.property( 'replaces URLs with anchors', 'string', jsc.elements(['http', 'https', 'ftp']), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), jsc.array(jsc.elements(queryString)), 'string', function (prefix, schema, address, query, fragment, postfix) { var query = query.join(''), fragment = fragment.join(''), url = schema + '://' + address.join('') + '/?' + query + '#' + fragment, prefix = $.PrivateBin.Helper.htmlEntities(prefix), postfix = ' ' + $.PrivateBin.Helper.htmlEntities(postfix), element = $('
' + prefix + url + 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 = ''; element = $('
' + prefix + url + '
'); } $.PrivateBin.Helper.urls2links(element); return element.html() === $('
' + prefix + '' + url + '' + postfix + '
').html(); } ); jsc.property( 'replaces magnet links with anchors', 'string', jsc.array(jsc.elements(queryString)), 'string', function (prefix, query, postfix) { var url = 'magnet:?' + query.join(''), prefix = $.PrivateBin.Helper.htmlEntities(prefix), postfix = $.PrivateBin.Helper.htmlEntities(postfix), element = $('
' + prefix + url + ' ' + postfix + '
'); $.PrivateBin.Helper.urls2links(element); return element.html() === $('
' + prefix + '' + url + ' ' + postfix + '
').html(); } ); }); 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 () { 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 () { before(function () { $.PrivateBin.Helper.reset(); }); jsc.property( 'returns the URL without query & fragment', jsc.elements(schemas), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(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 = $.PrivateBin.Helper.htmlEntities(string); return !(/[<>"'`=\/]/.test(result)) && !(string.indexOf('&') > -1 && !(/&/.test(result))); } ); }); }); describe('I18n', function () { describe('translate', function () { before(function () { $.PrivateBin.I18n.reset(); }); jsc.property( 'returns message ID unchanged if no translation found', 'string', function (messageId) { messageId = messageId.replace(/%(s|d)/g, '%%'); var plurals = [messageId, messageId + 's'], fake = [messageId], result = $.PrivateBin.I18n.translate(messageId); $.PrivateBin.I18n.reset(); var alias = $.PrivateBin.I18n._(messageId); $.PrivateBin.I18n.reset(); var p_result = $.PrivateBin.I18n.translate(plurals); $.PrivateBin.I18n.reset(); var p_alias = $.PrivateBin.I18n._(plurals); $.PrivateBin.I18n.reset(); var f_result = $.PrivateBin.I18n.translate(fake); $.PrivateBin.I18n.reset(); var f_alias = $.PrivateBin.I18n._(fake); $.PrivateBin.I18n.reset(); return messageId === result && messageId === alias && messageId === p_result && messageId === p_alias && messageId === f_result && messageId === f_alias; } ); 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 translation = prefix + params[0] + postfix; params.unshift(prefix + '%s' + postfix); var result = $.PrivateBin.I18n.translate.apply(this, params); $.PrivateBin.I18n.reset(); var alias = $.PrivateBin.I18n._.apply(this, params); $.PrivateBin.I18n.reset(); return translation === result && translation === alias; } ); }); describe('getPluralForm', function () { before(function () { $.PrivateBin.I18n.reset(); }); jsc.property( 'returns valid key for plural form', jsc.elements(supportedLanguages), 'integer', function(language, n) { $.PrivateBin.I18n.reset(language); var result = $.PrivateBin.I18n.getPluralForm(n); // arabic seems to have the highest plural count with 6 forms return result >= 0 && result <= 5; } ); }); // loading of JSON via AJAX needs to be tested in the browser, this just mocks it // TODO: This needs to be tested using a browser. describe('loadTranslations', function () { before(function () { $.PrivateBin.I18n.reset(); }); jsc.property( 'downloads and handles any supported language', jsc.elements(supportedLanguages), function(language) { var clean = jsdom('', {url: 'https://privatebin.net/', cookie: ['lang=' + language]}); $.PrivateBin.I18n.reset('en'); $.PrivateBin.I18n.loadTranslations(); $.PrivateBin.I18n.reset(language, require('../i18n/' + language + '.json')); var result = $.PrivateBin.I18n.translate('en'), alias = $.PrivateBin.I18n._('en'); clean(); return language === result && language === alias; } ); }); }); describe('CryptTool', function () { describe('cipher & decipher', function () { this.timeout(20000); it('can en- and decrypt any message', function () { jsc.check(jsc.forall( 'string', 'string', 'string', function (key, password, message) { return message === $.PrivateBin.CryptTool.decipher( key, password, $.PrivateBin.CryptTool.cipher(key, password, message) ); } ), // reducing amount of checks as running 100 takes about 5 minutes {tests: 5, quiet: true}); }); // The below static unit test is included to ensure deciphering of "classic" // SJCL based pastes still works it('supports v1 ciphertext (SJCL)', function () { // Of course you can easily decipher the following texts, if you like. // Bonus points for finding their sources and hidden meanings. var paste1 = $.PrivateBin.CryptTool.decipher( '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', // -- "That's amazing. I've got the same combination on my luggage." Array.apply(0, Array(6)).map(function(_,b) { return b + 1; }).join(''), '{"iv":"4HNFIl7eYbCh6HuShctTIA==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"u0lQvePq6L0=","ct":"fGPUVrDyaVr1ZDGb+kqQ3CPEW8x4YKGfzHDmA0Vjkh250aWNe7Cnigkps9aaFVMX9AaerrTp3yZbojJtNqVGMfLdUTu+53xmZHqRKxCCqSfDNSNoW4Oxk5OVgAtRyuG4bXHDsWTXDNz2xceqzVFqhkwTwlUchrV7uuFK/XUKTNjPFM744moivIcBbfM2FOeKlIFs8RYPYuvqQhp2rMLlNGwwKh//4kykQsHMQDeSDuJl8stMQzgWR/btUBZuwNZEydkMH6IPpTdf5WTSrZ+wC2OK0GutCm4UaEe6txzaTMfu+WRVu4PN6q+N+2zljWJ1XdpVcN/i0Sv4QVMym0Xa6y0eccEhj/69o47PmExmMMeEwExImPalMNT9JUSiZdOZJ/GdzwrwoIuq1mdQR6vSH+XJ/8jXJQ7bjjJVJYXTcT0Di5jixArI2Kpp1GGlGVFbLgPugwU1wczg+byqeDOAECXRRnQcogeaJtVcRwXwfy4j3ORFcblYMilxyHqKBewcYPRVBGtBs50cVjSIkAfR84rnc1nfvnxK/Gmm+4VBNHI6ODWNpRolVMCzXjbKYnV3Are5AgSpsTqaGl41VJGpcco6cAwi4K0Bys1seKR+bLSdUgqRrkEqSRSdu3/VTu9HhEk8an0rjTE4CBB5/LMn16p0TGLoOb32odKFIEtpanVvLjeyiVMvSxcgYLNnTi/5FiaAC4pJxRD+AZHedU1FICUeEXxIcac/4E5qjkHjX9SpQtLl80QLIVnjNliZm7QLB/nKu7W8Jb0+/CiTdV3Q9LhxlH4ciprnX+W0B00BKYFHnL9jRVzKdXhf1EHydbXMAfpCjHAXIVCkFakJinQBDIIw/SC6Yig0u0ddEID2B7LYAP1iE4RZwzTrxCB+ke2jQr8c20Jj6u6ShFOPC9DCw9XupZ4HAalVG00kSgjus+b8zrVji3/LKEhb4EBzp1ctBJCFTeXwej8ZETLoXTylev5dlwZSYAbuBPPcbFR/xAIPx3uDabd1E1gTqUc68ICIGhd197Mb2eRWiSvHr5SPsASerMxId6XA6+iQlRiI+NDR+TGVNmCnfxSlyPFMOHGTmslXOGIqGfBR8l4ft8YVZ70lCwmwTuViGc75ULSf9mM57/LmRzQFMYQtvI8IFK9JaQEMY5xz0HLtR4iyQUUdwR9e0ytBNdWF2a2WPDEnJuY/QJo4GzTlgv4QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}' ), paste2 = $.PrivateBin.CryptTool.decipher( 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', '', // no password '{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks":256,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"jN6CjbQMJCM=","ct":"kYYMo5DFG1+w0UHiYXT5pdV0IUuXxzOlslkW/c3DRCbGFROCVkAskHce7HoRczee1N9c5MhHjVMJUIZE02qIS8UyHdJ/GqcPVidTUcj9rnDNWsTXkjVv8jCwHS/cwmAjDTWpwp5ThECN+ov/wNp/NdtTj8Qj7f/T3rfZIOCWfwLH9s4Des35UNcUidfPTNQ1l0Gm0X+r98CCUSYZjQxkZc6hRZBLPQ8EaNVooUwd5eP4GiYlmSDNA0wOSA+5isPYxomVCt+kFf58VBlNhpfNi7BLYAUTPpXT4SfH5drR9+C7NTeZ+tTCYjbU94PzYItOpu8vgnB1/a6BAM5h3m9w+giUb0df4hgTWeZnZxLjo5BN8WV+kdTXMj3/Vv0gw0DQrDcCuX/cBAjpy3lQGwlAN1vXoOIyZJUjMpQRrOLdKvLB+zcmVNtGDbgnfP2IYBzk9NtodpUa27ne0T0ZpwOPlVwevsIVZO224WLa+iQmmHOWDFFpVDlS0t0fLfOk7Hcb2xFsTxiCIiyKMho/IME1Du3X4e6BVa3hobSSZv0rRtNgY1KcyYPrUPW2fxZ+oik3y9SgGvb7XpjVIta8DWlDWRfZ9kzoweWEYqz9IA8Xd373RefpyuWI25zlHoX3nwljzsZU6dC//h/Dt2DNr+IAvKO3+u23cWoB9kgcZJ2FJuqjLvVfCF+OWcig7zs2pTYJW6Rg6lqbBCxiUUlae6xJrjfv0pzD2VYCLY7v1bVTagppwKzNI3WaluCOrdDYUCxUSe56yd1oAoLPRVbYvomRboUO6cjQhEknERyvt45og2kORJOEJayHW+jZgR0Y0jM3Nk17ubpij2gHxNx9kiLDOiCGSV5mn9mV7qd3HHcOMSykiBgbyzjobi96LT2dIGLeDXTIdPOog8wyobO4jWq0GGs0vBB8oSYXhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZMZtmnYpGAtAPg7AUG"}' ); if (!paste1.includes('securely packed in iron') || !paste2.includes('Sol is right')) { throw Error('v1 (SJCL based) pastes could not be deciphered'); } }); }); }); describe('Model', function () { describe('getPasteId', function () { before(function () { $.PrivateBin.Model.reset(); }); jsc.property( 'returns the query string without separator, if any', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(queryString)), 'string', function (schema, address, query, fragment) { var queryString = query.join(''), clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/?' + queryString + '#' + fragment }), result = $.PrivateBin.Model.getPasteId(); $.PrivateBin.Model.reset(); clean(); return queryString === result; } ); jsc.property( 'throws exception on empty query string', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), 'string', function (schema, address, fragment) { var clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/#' + fragment }), result = false; try { $.PrivateBin.Model.getPasteId(); } catch(err) { result = true; } $.PrivateBin.Model.reset(); clean(); return result; } ); }); describe('getPasteKey', function () { jsc.property( 'returns the fragment of the URL', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), jsc.nearray(jsc.elements(base64String)), function (schema, address, query, fragment) { var fragmentString = fragment.join(''), clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/?' + query.join('') + '#' + fragmentString }), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); return fragmentString === result; } ); jsc.property( 'returns the fragment stripped of trailing query parts', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), jsc.nearray(jsc.elements(base64String)), jsc.array(jsc.elements(queryString)), function (schema, address, query, fragment, trail) { var fragmentString = fragment.join(''), clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/?' + query.join('') + '#' + fragmentString + '&' + trail.join('') }), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); return fragmentString === result; } ); jsc.property( 'throws exception on empty fragment of the URL', jsc.nearray(jsc.elements(a2zString)), jsc.nearray(jsc.elements(a2zString)), jsc.array(jsc.elements(queryString)), function (schema, address, query) { var clean = jsdom('', { url: schema.join('') + '://' + address.join('') + '/?' + query.join('') }), result = false; try { $.PrivateBin.Model.getPasteKey(); } catch(err) { result = true; } $.PrivateBin.Model.reset(); clean(); return result; } ); }); });