diff --git a/.dockerignore b/.dockerignore index a67706a4..9c2801b3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ assets docs public test -coverage \ No newline at end of file +coverage +.nyc_output \ No newline at end of file diff --git a/.nsprc b/.nsprc deleted file mode 100644 index e7831e90..00000000 --- a/.nsprc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "exceptions": ["https://nodesecurity.io/advisories/534"] -} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index c4ae50c4..5fdb0b0c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ dist assets/*.js +coverage \ No newline at end of file diff --git a/app/api.js b/app/api.js index 5986ff9f..585cf1e9 100644 --- a/app/api.js +++ b/app/api.js @@ -145,9 +145,6 @@ function download(id, keychain) { if (authHeader) { keychain.nonce = parseNonce(authHeader); } - if (xhr.status === 404) { - return reject(new Error('notfound')); - } if (xhr.status !== 200) { return reject(new Error(xhr.status)); } diff --git a/app/fileManager.js b/app/fileManager.js index b198177b..a788e1b8 100644 --- a/app/fileManager.js +++ b/app/fileManager.js @@ -188,7 +188,7 @@ export default function(state, emitter) { } console.error(err); state.transfer = null; - const location = err.message === 'notfound' ? '/404' : '/error'; + const location = err.message === '404' ? '/404' : '/error'; if (location === '/error') { state.raven.captureException(err); metrics.stoppedDownload({ size, err }); diff --git a/app/fileReceiver.js b/app/fileReceiver.js index b7462c0b..9976ed1a 100644 --- a/app/fileReceiver.js +++ b/app/fileReceiver.js @@ -59,7 +59,7 @@ export default class FileReceiver extends Nanobus { return; } - async download() { + async download(noSave = false) { this.state = 'downloading'; this.emit('progress', this.progress); try { @@ -78,11 +78,13 @@ export default class FileReceiver extends Nanobus { if (this.cancelled) { throw new Error(0); } - await saveFile({ - plaintext, - name: decodeURIComponent(this.fileInfo.name), - type: this.fileInfo.type - }); + if (!noSave) { + await saveFile({ + plaintext, + name: decodeURIComponent(this.fileInfo.name), + type: this.fileInfo.type + }); + } this.msg = 'downloadFinish'; this.state = 'complete'; return; diff --git a/app/ownedFile.js b/app/ownedFile.js index dafb1d4d..387bf95c 100644 --- a/app/ownedFile.js +++ b/app/ownedFile.js @@ -16,7 +16,7 @@ export default class OwnedFile { this.ownerToken = obj.ownerToken; this.dlimit = obj.dlimit || 1; this.dtotal = obj.dtotal || 0; - this.keychain = new Keychain(obj.secretKey); + this.keychain = new Keychain(obj.secretKey, obj.nonce); this._hasPassword = !!obj.hasPassword; } @@ -59,6 +59,7 @@ export default class OwnedFile { if (e.message === '404') { this.dtotal = this.dlimit; } + // ignore other errors } } diff --git a/circle.yml b/circle.yml index a386c272..b6b5e4f6 100644 --- a/circle.yml +++ b/circle.yml @@ -24,5 +24,5 @@ test: override: - npm run build - npm run lint - - npm run test:ci + - npm test - nsp check diff --git a/package-lock.json b/package-lock.json index d3ffa980..90087fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -386,6 +386,12 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -683,6 +689,17 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-istanbul": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz", + "integrity": "sha1-Z2DN2Xf0EdPhdbsGTyvDJ9mbK24=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "istanbul-lib-instrument": "1.9.2", + "test-exclude": "4.2.0" + } + }, "babel-plugin-syntax-async-functions": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", @@ -3942,6 +3959,29 @@ "webpack-sources": "1.1.0" } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "fast-deep-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", @@ -3975,6 +4015,15 @@ "websocket-driver": "0.7.0" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -6027,12 +6076,6 @@ "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", "dev": true }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6720,6 +6763,27 @@ "isarray": "1.0.0" } }, + "istanbul-lib-coverage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz", + "integrity": "sha512-tZYA0v5A7qBSsOzcebJJ/z3lk3oSzH62puG78DbBA1+zupipX2CakDyiPV3pOb8He+jBwVimuwB0dTnh38hX0w==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz", + "integrity": "sha512-nz8t4HQ2206a/3AXi+NHFWEa844DMpPsgbcUteJbt1j8LX1xg56H9rOMnhvcvVvPbW60qAIyrSk44H8ZDqaSSA==", + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.1.2", + "semver": "5.5.0" + } + }, "jest-get-type": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", @@ -6936,53 +7000,6 @@ "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=", "dev": true }, - "jszip": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz", - "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==", - "dev": true, - "requires": { - "core-js": "2.3.0", - "es6-promise": "3.0.2", - "lie": "3.1.1", - "pako": "1.0.6", - "readable-stream": "2.0.6" - }, - "dependencies": { - "core-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", - "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", - "dev": true - }, - "es6-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", - "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - } - } - } - }, "just-extend": { "version": "1.1.27", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", @@ -7084,15 +7101,6 @@ "type-check": "0.3.2" } }, - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "dev": true, - "requires": { - "immediate": "3.0.6" - } - }, "lint-staged": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-6.1.1.tgz", @@ -7509,6 +7517,12 @@ "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", "dev": true }, + "loglevelnext": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.3.tgz", + "integrity": "sha512-OCxd/b78TijTB4b6zVqLbMrxhebyvdZKwqpL0VHUZ0pYhavXaPD4l6Xrr4n5xqTYWiqtb0i7ikSoJY/myQ/Org==", + "dev": true + }, "lolex": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz", @@ -10672,6 +10686,12 @@ "resolved": "https://registry.npmjs.org/pelo/-/pelo-0.1.0.tgz", "integrity": "sha512-+oVixa69fxS/X+3s1DaSJVQLG/ukHfjK2pHCmpIgjRChp73lnAfbqOYZ0MEo5C5yVkYeUJSoWAcRK0lx0hvOjQ==" }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -13876,6 +13896,12 @@ "ipaddr.js": "1.5.2" } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, "proxyquire": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", @@ -13967,6 +13993,22 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, + "puppeteer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.1.0.tgz", + "integrity": "sha512-Up+EiDVtc+EueFUzEFi4JqTlkXdC4pjMng0W9s/uqg2HP7+EHJiIe8OtwP7I+Jk7rI1kS5WIVpqx46aihnLuJQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "extract-zip": "1.6.6", + "https-proxy-agent": "2.1.1", + "mime": "1.4.1", + "progress": "2.0.0", + "proxy-from-env": "1.0.0", + "rimraf": "2.6.2", + "ws": "3.3.3" + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -14809,29 +14851,6 @@ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", "dev": true }, - "selenium-webdriver": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", - "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", - "dev": true, - "requires": { - "jszip": "3.1.5", - "rimraf": "2.6.2", - "tmp": "0.0.30", - "xml2js": "0.4.17" - }, - "dependencies": { - "tmp": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", - "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - } - } - }, "selfsigned": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.2.tgz", @@ -16025,6 +16044,109 @@ "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", "dev": true }, + "test-exclude": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.0.tgz", + "integrity": "sha512-8hMFzjxbPv6xSlwGhXSvOMJ/vTy3bkng+2pxmf6E1z6VF7I9nIyNfvHtaw+NBPgvz647gADBbMSbwLfZYppT/w==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, "testpilot-ga": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/testpilot-ga/-/testpilot-ga-0.3.0.tgz", @@ -16300,6 +16422,12 @@ } } }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, "unassert": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/unassert/-/unassert-1.5.1.tgz", @@ -16471,6 +16599,12 @@ "querystring": "0.2.0" } }, + "url-join": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", + "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", + "dev": true + }, "url-parse": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", @@ -16734,22 +16868,24 @@ } }, "webpack-dev-middleware": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", - "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz", + "integrity": "sha512-tj5LLD9r4tDuRIDa5Mu9lnY2qBBehAITv6A9irqXhw/HQquZgTx3BCd57zYbU2gMDnncA49ufK2qVQSbaKJwOw==", "dev": true, "requires": { + "loud-rejection": "1.6.0", "memory-fs": "0.4.1", - "mime": "1.6.0", + "mime": "2.2.0", "path-is-absolute": "1.0.1", "range-parser": "1.2.0", - "time-stamp": "2.0.0" + "url-join": "2.0.5", + "webpack-log": "1.1.2" }, "dependencies": { "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", + "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==", "dev": true } } @@ -16873,6 +17009,12 @@ } } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -16969,6 +17111,19 @@ "has-flag": "2.0.0" } }, + "webpack-dev-middleware": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", + "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "dev": true, + "requires": { + "memory-fs": "0.4.1", + "mime": "1.6.0", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "time-stamp": "2.0.0" + } + }, "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", @@ -17007,6 +17162,55 @@ } } }, + "webpack-log": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.1.2.tgz", + "integrity": "sha512-B53SD4N4BHpZdUwZcj4st2QT7gVfqZtqHDruC1N+K2sciq0Rt/3F1Dx6RlylVkcrToMLTaiaeT48k9Lq4iDVDA==", + "dev": true, + "requires": { + "chalk": "2.3.1", + "log-symbols": "2.2.0", + "loglevelnext": "1.0.3", + "uuid": "3.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, "webpack-manifest-plugin": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz", @@ -17178,6 +17382,17 @@ "mkdirp": "0.5.1" } }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, "x-is-function": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/x-is-function/-/x-is-function-1.0.4.tgz", @@ -17264,6 +17479,15 @@ "camelcase": "4.1.0" } }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, "yo-yoify": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/yo-yoify/-/yo-yoify-4.3.0.tgz", diff --git a/package.json b/package.json index 07e1e27e..24f0f1d2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "private": true, "scripts": { "precommit": "lint-staged", + "prepush": "npm test", "clean": "rimraf dist", "build": "npm run clean && webpack -p", "lint": "npm-run-all lint:*", @@ -23,8 +24,9 @@ "changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges", "contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS", "release": "npm-run-all contributors changelog", - "test": "mocha test/unit", - "test:ci": "nyc mocha --reporter=min test/unit", + "test": "npm-run-all test:*", + "test:backend": "nyc mocha --reporter=min test/unit", + "test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js && nyc report --reporter=html", "start": "cross-env NODE_ENV=development webpack-dev-server", "prod": "node server/prod.js", "cover": "nyc --reporter=html mocha test/unit" @@ -43,7 +45,7 @@ }, "nyc": { "reporter": [ - "text-summary" + "text" ], "cache": true }, @@ -53,6 +55,7 @@ "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", + "babel-plugin-istanbul": "^4.1.5", "babel-plugin-yo-yoify": "^1.0.2", "babel-preset-env": "^1.6.1", "babel-preset-es2015": "^6.24.1", @@ -88,11 +91,11 @@ "postcss-loader": "^2.1.0", "prettier": "^1.10.2", "proxyquire": "^1.8.0", + "puppeteer": "^1.1.0", "raven-js": "^3.22.2", "redis-mock": "^0.21.0", "require-from-string": "^2.0.1", "rimraf": "^2.6.2", - "selenium-webdriver": "^3.6.0", "sinon": "^4.3.0", "string-hash": "^1.1.3", "stylelint": "^9.0.0", @@ -102,6 +105,7 @@ "testpilot-ga": "^0.3.0", "val-loader": "^1.1.0", "webpack": "^3.11.0", + "webpack-dev-middleware": "^2.0.6", "webpack-dev-server": "2.9.1", "webpack-manifest-plugin": "^1.3.2", "webpack-unassert-loader": "^1.2.0" diff --git a/server/dev.js b/server/dev.js index a21bb65d..babb9b12 100644 --- a/server/dev.js +++ b/server/dev.js @@ -2,11 +2,13 @@ const assets = require('../common/assets'); const locales = require('../common/locales'); const routes = require('./routes'); const pages = require('./routes/pages'); +const tests = require('../test/frontend/routes'); module.exports = function(app, devServer) { assets.setMiddleware(devServer.middleware); locales.setMiddleware(devServer.middleware); routes(app); + tests(app); // webpack-dev-server routes haven't been added yet // so wait for next tick to add 404 handler process.nextTick(() => app.use(pages.notfound)); diff --git a/test/frontend/.eslintrc.yml b/test/frontend/.eslintrc.yml index 07a266de..c9dca39e 100644 --- a/test/frontend/.eslintrc.yml +++ b/test/frontend/.eslintrc.yml @@ -1,2 +1,8 @@ env: - browser: true \ No newline at end of file + browser: true + +parserOptions: + sourceType: module + +rules: + node/no-unsupported-features: off \ No newline at end of file diff --git a/test/frontend/driver.js b/test/frontend/driver.js deleted file mode 100644 index 3e16d347..00000000 --- a/test/frontend/driver.js +++ /dev/null @@ -1,22 +0,0 @@ -const webdriver = require('selenium-webdriver'); -const path = require('path'); -const until = webdriver.until; - -const driver = new webdriver.Builder().forBrowser('firefox').build(); - -driver.get(path.join('file:///', __dirname, '/frontend.test.html')); -driver.wait(until.titleIs('Mocha Tests')); -driver.wait(until.titleMatches(/^[0-9]$/)); - -driver.getTitle().then(title => { - driver.quit().then(() => { - if (title === '0') { - console.log('Frontend tests have passed.'); - } else { - throw new Error( - 'Frontend tests are failing. ' + - 'Please open the frontend.test.html file in a browser.' - ); - } - }); -}); diff --git a/test/frontend/frontend.bundle.js b/test/frontend/frontend.bundle.js deleted file mode 100644 index 2f043245..00000000 --- a/test/frontend/frontend.bundle.js +++ /dev/null @@ -1,22 +0,0 @@ -class FakeFile extends Blob { - constructor(name, data, opt) { - super(data, opt); - this.name = name; - } -} - -window.Raven = { - captureException: function(err) { - console.error(err, err.stack); - } -}; - -window.FakeFile = FakeFile; -window.FileSender = require('../../app/fileSender'); -window.FileReceiver = require('../../app/fileReceiver'); -window.sinon = require('sinon'); -window.server = window.sinon.fakeServer.create(); -window.assert = require('assert'); -const utils = require('../../app/utils'); -window.b64ToArray = utils.b64ToArray; -window.arrayToB64 = utils.arrayToB64; diff --git a/test/frontend/frontend.test.html b/test/frontend/frontend.test.html deleted file mode 100644 index 757d8eb1..00000000 --- a/test/frontend/frontend.test.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Mocha Tests - - - - - -
- - - - - - - - \ No newline at end of file diff --git a/test/frontend/frontend.test.js b/test/frontend/frontend.test.js deleted file mode 100644 index b2241330..00000000 --- a/test/frontend/frontend.test.js +++ /dev/null @@ -1,230 +0,0 @@ -const FileSender = window.FileSender; -const FileReceiver = window.FileReceiver; -const FakeFile = window.FakeFile; -const assert = window.assert; -const server = window.server; -const b64ToArray = window.b64ToArray; -const sinon = window.sinon; - -let file; -let encryptedIV; -let secretKey; -let originalBlob; - -describe('File Sender', function() { - before(function() { - server.respondImmediately = true; - server.respondWith('POST', '/upload', function(request) { - const reader = new FileReader(); - reader.readAsArrayBuffer(request.requestBody.get('data')); - - reader.onload = function(event) { - file = this.result; - }; - - const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']); - request.respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - url: 'some url', - id: responseObj.id, - delete: responseObj.delete - }) - ); - }); - }); - - it('Should get a loading event emission', function() { - const file = new FakeFile('hello_world.txt', ['This is some data.']); - const fs = new FileSender(file); - let testLoading = true; - - fs.on('loading', isStillLoading => { - assert(!(!testLoading && isStillLoading)); - testLoading = isStillLoading; - }); - - return fs - .upload() - .then(info => { - assert(info); - assert(!testLoading); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }); - - it('Should get a encrypting event emission', function() { - const file = new FakeFile('hello_world.txt', ['This is some data.']); - const fs = new FileSender(file); - let testEncrypting = true; - - fs.on('encrypting', isStillEncrypting => { - assert(!(!testEncrypting && isStillEncrypting)); - testEncrypting = isStillEncrypting; - }); - - return fs - .upload() - .then(info => { - assert(info); - assert(!testEncrypting); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }); - - it('Should encrypt a file properly', function(done) { - const newFile = new FakeFile('hello_world.txt', ['This is some data.']); - const fs = new FileSender(newFile); - fs.upload().then(info => { - const key = info.secretKey; - secretKey = info.secretKey; - const IV = info.fileId; - encryptedIV = info.fileId; - - const readRaw = new FileReader(); - readRaw.onload = function(event) { - const rawArray = new Uint8Array(this.result); - originalBlob = rawArray; - window.crypto.subtle - .importKey( - 'jwk', - { - kty: 'oct', - k: key, - alg: 'A128GCM', - ext: true - }, - { - name: 'AES-GCM' - }, - true, - ['encrypt', 'decrypt'] - ) - .then(cryptoKey => { - window.crypto.subtle - .encrypt( - { - name: 'AES-GCM', - iv: b64ToArray(IV), - tagLength: 128 - }, - cryptoKey, - rawArray - ) - .then(encrypted => { - assert( - new Uint8Array(encrypted).toString() === - new Uint8Array(file).toString() - ); - done(); - }); - }); - }; - - readRaw.readAsArrayBuffer(newFile); - }); - }); -}); - -describe('File Receiver', function() { - class FakeXHR { - constructor() { - this.response = file; - this.status = 200; - } - - static setup() { - FakeXHR.prototype.open = sinon.spy(); - FakeXHR.prototype.send = function() { - this.onload(); - }; - - FakeXHR.prototype.originalXHR = window.XMLHttpRequest; - - FakeXHR.prototype.getResponseHeader = function() { - return JSON.stringify({ - filename: 'hello_world.txt', - id: encryptedIV - }); - }; - window.XMLHttpRequest = FakeXHR; - } - - static restore() { - // originalXHR is a sinon FakeXMLHttpRequest, since - // fakeServer.create() is called in frontend.bundle.js - window.XMLHttpRequest.prototype.originalXHR.restore(); - } - } - - const cb = function(done) { - if ( - file === undefined || - encryptedIV === undefined || - secretKey === undefined - ) { - assert.fail( - 'Please run file sending tests before trying to receive the files.' - ); - done(); - } - - FakeXHR.setup(); - done(); - }; - - before(cb); - - after(function() { - FakeXHR.restore(); - }); - - it('Should decrypt properly', function() { - const fr = new FileReceiver(); - location.hash = secretKey; - return fr - .download() - .then(([decrypted, name]) => { - assert(name); - assert( - new Uint8Array(decrypted).toString() === - new Uint8Array(originalBlob).toString() - ); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }); - - it('Should emit decrypting events', function() { - const fr = new FileReceiver(); - location.hash = secretKey; - - let testDecrypting = true; - - fr.on('decrypting', isStillDecrypting => { - assert(!(!testDecrypting && isStillDecrypting)); - testDecrypting = isStillDecrypting; - }); - - return fr - .download() - .then(([decrypted, name]) => { - assert(decrypted); - assert(name); - assert(!testDecrypting); - }) - .catch(err => { - console.log(err, err.stack); - assert.fail(); - }); - }); -}); diff --git a/test/frontend/index.js b/test/frontend/index.js new file mode 100644 index 00000000..fa37cc5e --- /dev/null +++ b/test/frontend/index.js @@ -0,0 +1,16 @@ +const fs = require('fs'); +const path = require('path'); + +function kv(f) { + return `require('./tests/${f}')`; +} + +module.exports = function() { + const files = fs.readdirSync(path.join(__dirname, 'tests')); + const code = files.map(kv).join(';\n'); + return { + code, + dependencies: files.map(f => require.resolve('./tests/' + f)), + cacheable: false + }; +}; diff --git a/test/frontend/routes.js b/test/frontend/routes.js new file mode 100644 index 00000000..3aa9649b --- /dev/null +++ b/test/frontend/routes.js @@ -0,0 +1,47 @@ +const html = require('choo/html'); +const assets = require('../../common/assets'); + +module.exports = function(app) { + app.get('/mocha.css', function(req, res) { + res.sendFile(require.resolve('mocha/mocha.css')); + }); + app.get('/mocha.js', function(req, res) { + res.sendFile(require.resolve('mocha/mocha.js')); + }); + app.get('/test', function(req, res) { + res.send( + html` + + + + + + + + + + + + +
+ + + + `.toString() + ); + }); +}; diff --git a/test/frontend/runner.js b/test/frontend/runner.js new file mode 100644 index 00000000..d474b2d8 --- /dev/null +++ b/test/frontend/runner.js @@ -0,0 +1,63 @@ +/* eslint-disable no-undef, no-process-exit */ +const fs = require('fs'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const puppeteer = require('puppeteer'); +const webpack = require('webpack'); +const config = require('../../webpack.config'); +const middleware = require('webpack-dev-middleware'); +const express = require('express'); +const devRoutes = require('../../server/dev'); +const app = express(); + +const wpm = middleware(webpack(config), { logLevel: 'silent' }); +app.use(wpm); +devRoutes(app, { middleware: wpm }); + +function onConsole(msg) { + // excluding 'log' because mocha uses it to write the json output + if (msg.type() !== 'log') { + console.error(msg.text()); + } +} + +const server = app.listen(async function() { + let exitCode = -1; + const browser = await puppeteer.launch(); + try { + const page = await browser.newPage(); + page.on('console', onConsole); + page.on('pageerror', console.log.bind(console)); + await page.goto(`http://127.0.0.1:${server.address().port}/test`); + await page.waitFor(() => typeof runner.testResults !== 'undefined', { + timeout: 5000 + }); + const results = await page.evaluate(() => runner.testResults); + const coverage = await page.evaluate(() => __coverage__); + if (coverage) { + const dir = path.resolve(__dirname, '../../.nyc_output'); + mkdirp.sync(dir); + fs.writeFileSync( + path.resolve(dir, 'frontend.json'), + JSON.stringify(coverage) + ); + } + const stats = results.stats; + exitCode = stats.failures; + console.log(`${stats.passes} passing (${stats.duration}ms)\n`); + if (stats.failures) { + console.log('Failures:\n'); + for (const f of results.failures) { + console.log(`${f.fullTitle}`); + console.log(` ${f.err.stack}\n`); + } + } + } catch (e) { + console.log(e); + } finally { + browser.close(); + server.close(() => { + process.exit(exitCode); + }); + } +}); diff --git a/test/frontend/tests/api-tests.js b/test/frontend/tests/api-tests.js new file mode 100644 index 00000000..e11c47b9 --- /dev/null +++ b/test/frontend/tests/api-tests.js @@ -0,0 +1,26 @@ +import assert from 'assert'; +import * as api from '../../../app/api'; +import Keychain from '../../../app/keychain'; + +const encoder = new TextEncoder(); +const plaintext = encoder.encode('hello world!'); +const metadata = { + name: 'test.txt', + type: 'text/plain' +}; + +describe('API', function() { + describe('uploadFile', function() { + it('returns file info on success', async function() { + const keychain = new Keychain(); + const encrypted = await keychain.encryptFile(plaintext); + const meta = await keychain.encryptMetadata(metadata); + const verifierB64 = await keychain.authKeyB64(); + const up = api.uploadFile(encrypted, meta, verifierB64, keychain); + const result = await up.result; + assert.ok(result.url); + assert.ok(result.id); + assert.ok(result.ownerToken); + }); + }); +}); diff --git a/test/frontend/tests/fileSender-tests.js b/test/frontend/tests/fileSender-tests.js new file mode 100644 index 00000000..f7a5ea31 --- /dev/null +++ b/test/frontend/tests/fileSender-tests.js @@ -0,0 +1,17 @@ +import assert from 'assert'; +import FileSender from '../../../app/fileSender'; + +// FileSender uses a File in real life but a Blob works for testing +const blob = new Blob(['hello world!'], { type: 'text/plain' }); +blob.name = 'text.txt'; + +describe('FileSender', function() { + describe('upload', function() { + it('returns an OwnedFile on success', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + assert.ok(file.id); + assert.equal(file.name, blob.name); + }); + }); +}); diff --git a/test/frontend/tests/keychain-tests.js b/test/frontend/tests/keychain-tests.js new file mode 100644 index 00000000..fb62e48a --- /dev/null +++ b/test/frontend/tests/keychain-tests.js @@ -0,0 +1,41 @@ +import assert from 'assert'; +import Keychain from '../../../app/keychain'; + +describe('Keychain', function() { + describe('setPassword', function() { + it('changes the authKey', async function() { + const k = new Keychain(); + const original = await k.authKeyB64(); + k.setPassword('foo', 'some://url'); + const pwd = await k.authKeyB64(); + assert.notEqual(pwd, original); + }); + }); + + describe('encrypt / decrypt file', function() { + it('can decrypt text it encrypts', async function() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const text = 'hello world!'; + const k = new Keychain(); + const ciphertext = await k.encryptFile(enc.encode(text)); + assert.notEqual(dec.decode(ciphertext), text); + const plaintext = await k.decryptFile(ciphertext); + assert.equal(dec.decode(plaintext), text); + }); + }); + + describe('encrypt / decrypt metadata', function() { + it('can decrypt metadata it encrypts', async function() { + const k = new Keychain(); + const meta = { + name: 'foo', + type: 'bar/baz' + }; + const ciphertext = await k.encryptMetadata(meta); + const result = await k.decryptMetadata(ciphertext); + assert.equal(result.name, meta.name); + assert.equal(result.type, meta.type); + }); + }); +}); diff --git a/test/frontend/tests/workflow-tests.js b/test/frontend/tests/workflow-tests.js new file mode 100644 index 00000000..539aa40d --- /dev/null +++ b/test/frontend/tests/workflow-tests.js @@ -0,0 +1,133 @@ +import assert from 'assert'; +import FileSender from '../../../app/fileSender'; +import FileReceiver from '../../../app/fileReceiver'; + +const headless = /Headless/.test(navigator.userAgent); +const noSave = !headless; // only run the saveFile code if headless + +// FileSender uses a File in real life but a Blob works for testing +const blob = new Blob(['hello world!'], { type: 'text/plain' }); +blob.name = 'test.txt'; + +describe('Upload / Download flow', function() { + it('can only download once by default', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: file.keychain.nonce, + requiresPassword: false + }); + await fr.getMetadata(); + await fr.download(noSave); + try { + await fr.download(noSave); + assert.fail('downloaded again'); + } catch (e) { + assert.equal(e.message, '404'); + } + }); + + it('downloads with the correct password', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + await file.setPassword('magic'); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + url: file.url, + nonce: file.keychain.nonce, + requiresPassword: true, + password: 'magic' + }); + await fr.getMetadata(); + await fr.download(noSave); + assert.equal(fr.state, 'complete'); + }); + + it('blocks invalid passwords from downloading', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + await file.setPassword('magic'); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + url: file.url, + nonce: file.keychain.nonce, + requiresPassword: true, + password: 'password' + }); + try { + await fr.getMetadata(); + assert.fail('got metadata with bad password'); + } catch (e) { + assert.equal(e.message, '401'); + } + try { + // We can't decrypt without IV from metadata + // but let's try to download anyway + await fr.download(); + assert.fail('downloaded file with bad password'); + } catch (e) { + assert.equal(e.message, '401'); + } + }); + + it('retries a bad nonce', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: null, // oops + requiresPassword: false + }); + await fr.getMetadata(); + assert.equal(fr.fileInfo.name, blob.name); + }); + + it('can allow multiple downloads', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: file.keychain.nonce, + requiresPassword: false + }); + await file.changeLimit(2); + await fr.getMetadata(); + await fr.download(noSave); + await file.updateDownloadCount(); + assert.equal(file.dtotal, 1); + + await fr.download(noSave); + await file.updateDownloadCount(); + assert.equal(file.dtotal, 2); + try { + await fr.download(noSave); + assert.fail('downloaded too many times'); + } catch (e) { + assert.equal(e.message, '404'); + } + }); + + it('can delete the file before download', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: file.keychain.nonce, + requiresPassword: false + }); + await file.del(); + try { + await fr.getMetadata(); + assert.fail('file still exists'); + } catch (e) { + assert.equal(e.message, '404'); + } + }); +}); diff --git a/webpack.config.js b/webpack.config.js index 3f818d8b..0767d8ad 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,12 +11,19 @@ const regularJSOptions = { plugins: ['yo-yoify'] }; +const entry = { + vendor: ['babel-polyfill', 'fluent'], + app: ['./app/main.js'], + style: ['./app/main.css'] +}; + +if (IS_DEV) { + entry.tests = ['./test/frontend/index.js']; + regularJSOptions.plugins.push('istanbul'); +} + module.exports = { - entry: { - vendor: ['babel-polyfill', 'fluent'], - app: ['./app/main.js'], - style: ['./app/main.css'] - }, + entry, output: { filename: '[name].[chunkhash:8].js', path: path.resolve(__dirname, 'dist'), @@ -126,6 +133,10 @@ module.exports = { './build/fluent_loader' ] }, + { + test: require.resolve('./test/frontend/index.js'), + use: ['babel-loader', 'val-loader'] + }, { test: require.resolve('./build/generate_asset_map.js'), use: ['babel-loader', 'val-loader']