From 75fac0e5945fa9bc16371613d902391b96339bdc Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 8 Dec 2017 17:50:47 -0800 Subject: [PATCH 01/15] added testing package and wrote some publish tests --- controllers/publishController.js | 42 ++++++++ helpers/publishHelpers.js | 160 +++++++++++++++---------------- package.json | 5 +- routes/api-routes.js | 58 ++--------- test/publishApiTests.js | 76 +++++++++++++++ 5 files changed, 209 insertions(+), 132 deletions(-) create mode 100644 test/publishApiTests.js diff --git a/controllers/publishController.js b/controllers/publishController.js index d832c814..8202d02c 100644 --- a/controllers/publishController.js +++ b/controllers/publishController.js @@ -2,6 +2,7 @@ const logger = require('winston'); const db = require('../models'); const lbryApi = require('../helpers/lbryApi.js'); const publishHelpers = require('../helpers/publishHelpers.js'); +const config = require('../config/speechConfig.js'); module.exports = { publish (publishParams, fileName, fileType) { @@ -84,4 +85,45 @@ module.exports = { }); }); }, + checkClaimNameAvailability (name) { + return new Promise((resolve, reject) => { + // find any records where the name is used + db.File.findAll({ where: { name } }) + .then(result => { + if (result.length >= 1) { + const claimAddress = config.wallet.lbryClaimAddress; + // filter out any results that were not published from spee.ch's wallet address + const filteredResult = result.filter((claim) => { + return (claim.address === claimAddress); + }); + // return based on whether any non-spee.ch claims were left + if (filteredResult.length >= 1) { + resolve(false); + } else { + resolve(true); + } + } else { + resolve(true); + } + }) + .catch(error => { + reject(error); + }); + }); + }, + checkChannelAvailability (name) { + return new Promise((resolve, reject) => { + // find any records where the name is used + db.Channel.findAll({ where: { channelName: name } }) + .then(result => { + if (result.length >= 1) { + return resolve(false); + } + resolve(true); + }) + .catch(error => { + reject(error); + }); + }); + }, }; diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 25c715c1..3bab6787 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -1,41 +1,96 @@ const logger = require('winston'); const fs = require('fs'); -const db = require('../models'); const config = require('../config/speechConfig.js'); module.exports = { - validateApiPublishRequest (body, files) { - if (!body) { - throw new Error('no body found in request'); - } - if (!body.name) { + parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) { + // validate name + if (!name) { throw new Error('no name field found in request'); } - if (!files) { - throw new Error('no files found in request'); + const invalidNameCharacters = /[^A-Za-z0-9,-]/.exec(name); + if (invalidNameCharacters) { + throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"'); } - if (!files.file) { + // optional parameters + nsfw = (nsfw === 'true'); + license = license || null; + title = title || null; + description = description || null; + thumbnail = thumbnail || null; + // return results + return { + name, + nsfw, + license, + title, + description, + thumbnail, + }; + }, + parsePublishApiRequestFiles ({file}) { + // make sure a file was provided + if (!file) { throw new Error('no file with key of [file] found in request'); } - }, - validatePublishSubmission (file, claimName) { - try { - module.exports.validateFile(file); - module.exports.validateClaimName(claimName); - } catch (error) { - throw error; + if (!file.path) { + throw new Error('no file path found'); } - }, - validateFile (file) { - if (!file) { - logger.debug('publish > file validation > no file found'); - throw new Error('no file provided'); + if (!file.type) { + throw new Error('no file type found'); } - // check the file name + if (!file.size) { + throw new Error('no file type found'); + } + // validate the file name if (/'/.test(file.name)) { logger.debug('publish > file validation > file name had apostrophe in it'); throw new Error('apostrophes are not allowed in the file name'); } + // validate the file + module.exports.validateFileTypeAndSize(file); + // return results + return { + fileName: file.name, + filePath: file.path, + fileType: file.type, + }; + }, + parsePublishApiChannel ({channelName, channelPassword}, user) { + let anonymous = (channelName === null || channelName === undefined || channelName === ''); + if (user) { + channelName = user.channelName || null; + } else { + channelName = channelName || null; + } + channelPassword = channelPassword || null; + let skipAuth = false; + // case 1: publish from spee.ch, client logged in + if (user) { + skipAuth = true; + if (anonymous) { + channelName = null; + } + // case 2: publish from api or spee.ch, client not logged in + } else { + if (anonymous) { + skipAuth = true; + channelName = null; + } + } + // cleanse channel name + if (channelName) { + if (channelName.indexOf('@') !== 0) { + channelName = `@${channelName}`; + } + } + return { + channelName, + channelPassword, + skipAuth, + }; + }, + validateFileTypeAndSize (file) { // check file type and size switch (file.type) { case 'image/jpeg': @@ -64,25 +119,6 @@ module.exports = { } return file; }, - validateClaimName (claimName) { - const invalidCharacters = /[^A-Za-z0-9,-]/.exec(claimName); - if (invalidCharacters) { - throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"'); - } - }, - validateLicense (license) { - if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) { - throw new Error('Only posts with a "Public Domain" or "Creative Commons" license are eligible for publishing through spee.ch'); - } - }, - cleanseChannelName (channelName) { - if (channelName) { - if (channelName.indexOf('@') !== 0) { - channelName = `@${channelName}`; - } - } - return channelName; - }, createPublishParams (filePath, name, title, description, license, nsfw, thumbnail, channelName) { logger.debug(`Creating Publish Parameters`); // provide defaults for title @@ -131,45 +167,5 @@ module.exports = { logger.debug(`successfully deleted ${filePath}`); }); }, - checkClaimNameAvailability (name) { - return new Promise((resolve, reject) => { - // find any records where the name is used - db.File.findAll({ where: { name } }) - .then(result => { - if (result.length >= 1) { - const claimAddress = config.wallet.lbryClaimAddress; - // filter out any results that were not published from spee.ch's wallet address - const filteredResult = result.filter((claim) => { - return (claim.address === claimAddress); - }); - // return based on whether any non-spee.ch claims were left - if (filteredResult.length >= 1) { - resolve(false); - } else { - resolve(true); - } - } else { - resolve(true); - } - }) - .catch(error => { - reject(error); - }); - }); - }, - checkChannelAvailability (name) { - return new Promise((resolve, reject) => { - // find any records where the name is used - db.Channel.findAll({ where: { channelName: name } }) - .then(result => { - if (result.length >= 1) { - return resolve(false); - } - resolve(true); - }) - .catch(error => { - reject(error); - }); - }); - }, + }; diff --git a/package.json b/package.json index 9bf792f2..45891d08 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "a single-serving site that reads and publishes images to and from the LBRY blockchain", "main": "speech.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "mocha", "start": "node speech.js", "lint": "eslint .", "fix": "eslint . --fix", @@ -55,6 +55,7 @@ "eslint-plugin-promise": "3.5.0", "eslint-plugin-react": "6.10.3", "eslint-plugin-standard": "3.0.1", - "husky": "^0.13.4" + "husky": "^0.13.4", + "mocha": "^4.0.1" } } diff --git a/routes/api-routes.js b/routes/api-routes.js index 38b5b1e8..0b9434be 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -3,9 +3,9 @@ const multipart = require('connect-multiparty'); const config = require('../config/speechConfig.js'); const multipartMiddleware = multipart({uploadDir: config.files.uploadDirectory}); const db = require('../models'); -const { publish } = require('../controllers/publishController.js'); +const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js'); const { getClaimList, resolveUri } = require('../helpers/lbryApi.js'); -const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseChannelName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js'); +const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel } = require('../helpers/publishHelpers.js'); const errorHandlers = require('../helpers/errorHandlers.js'); const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js'); const { authenticateOrSkip } = require('../auth/authentication.js'); @@ -73,56 +73,18 @@ module.exports = (app) => { }); // route to run a publish request on the daemon app.post('/api/publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { - let file, fileName, filePath, fileType, name, nsfw, license, title, description, thumbnail, anonymous, skipAuth, channelName, channelPassword; - // validate that mandatory parts of the request are present + let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, skipAuth, channelName, channelPassword; + // validate the body and files of the request try { - validateApiPublishRequest(body, files); + // validateApiPublishRequest(body, files); + ({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body)); + ({fileName, filePath, fileType} = parsePublishApiRequestFiles(files)); + ({channelName, channelPassword, skipAuth} = parsePublishApiChannel(body, user)); } catch (error) { logger.debug('publish request rejected, insufficient request parameters'); - res.status(400).json({success: false, message: error.message}); - return; + return res.status(400).json({success: false, message: error.message}); } - // validate file, name, license, and nsfw - file = files.file; - fileName = file.path.substring(file.path.lastIndexOf('/') + 1); - filePath = file.path; - fileType = file.type; - name = body.name; - nsfw = (body.nsfw === 'true'); - try { - validatePublishSubmission(file, name, nsfw); - } catch (error) { - logger.debug('publish request rejected'); - res.status(400).json({success: false, message: error.message}); - return; - } - // optional inputs - license = body.license || null; - title = body.title || null; - description = body.description || null; - thumbnail = body.thumbnail || null; - anonymous = (body.channelName === 'null') || (body.channelName === undefined); - if (user) { - channelName = user.channelName || null; - } else { - channelName = body.channelName || null; - } - channelPassword = body.channelPassword || null; - skipAuth = false; - // case 1: publish from spee.ch, client logged in - if (user) { - skipAuth = true; - if (anonymous) { - channelName = null; - } - // case 2: publish from api or spee.ch, client not logged in - } else { - if (anonymous) { - skipAuth = true; - channelName = null; - } - } - channelName = cleanseChannelName(channelName); + logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); // check channel authorization authenticateOrSkip(skipAuth, channelName, channelPassword) diff --git a/test/publishApiTests.js b/test/publishApiTests.js new file mode 100644 index 00000000..5f923099 --- /dev/null +++ b/test/publishApiTests.js @@ -0,0 +1,76 @@ +const assert = require('assert'); + +describe('Array', function () { + describe('indexOf()', function () { + it('should return -1 when the value is not present', function () { + assert.equal(-1, [1, 2, 3].indexOf(4)); + }); + }); +}); + +describe('controllers', function () { + describe('api/publish', function () { + describe('publishHelpers.js', function () { + const publishHelpers = require('../helpers/publishHelpers.js'); + + describe('#parsePublishApiRequestBody()', function () { + it('should throw an error if no body', function () { + assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, null), Error); + }); + it('should throw an error if no body.name', function () { + const bodyNoName = {}; + assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, bodyNoName), Error); + }); + it('should throw an error if no body.name', function () { + const body = { + name: 'bob', + }; + assert.doesNotThrow(publishHelpers.parsePublishApiRequestBody.bind(this, body), Error); + }); + }); + + describe('#parsePublishApiRequestFiles()', function () { + it('should throw an error if no files', function () { + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); + }); + it('should throw an error if no files.file', function () { + const filesNoFile = {}; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); + }); + it('should throw an error if file.size is too large', function () { + const filesTooBig = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'image/jpg', + size: 10000001, + }, + }; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); + }); + it('should throw error if not an accepted file type', function () { + const filesNoProblems = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'someType/ext', + size: 10000000, + }, + }; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + it('should throw NO error if no problems', function () { + const filesNoProblems = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'image/jpg', + size: 10000000, + }, + }; + assert.doesNotThrow(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + }); + }); + }); +}); -- 2.45.2 From aafade848edcb24f49a1eeaa8e020caf2b43d989 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Mon, 11 Dec 2017 11:22:59 -0800 Subject: [PATCH 02/15] updating channel check functions --- helpers/publishHelpers.js | 25 +++++-------------------- routes/api-routes.js | 7 +++---- test/publishApiTests.js | 31 ++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 3bab6787..3b4b0f92 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -57,27 +57,12 @@ module.exports = { }; }, parsePublishApiChannel ({channelName, channelPassword}, user) { + // anonymous if no channel name provided let anonymous = (channelName === null || channelName === undefined || channelName === ''); + // if a channel name is provided, get password from the user token if (user) { - channelName = user.channelName || null; - } else { - channelName = channelName || null; - } - channelPassword = channelPassword || null; - let skipAuth = false; - // case 1: publish from spee.ch, client logged in - if (user) { - skipAuth = true; - if (anonymous) { - channelName = null; - } - // case 2: publish from api or spee.ch, client not logged in - } else { - if (anonymous) { - skipAuth = true; - channelName = null; - } - } + channelPassword = user.channelPassword; + } ; // cleanse channel name if (channelName) { if (channelName.indexOf('@') !== 0) { @@ -85,9 +70,9 @@ module.exports = { } } return { + anonymous, channelName, channelPassword, - skipAuth, }; }, validateFileTypeAndSize (file) { diff --git a/routes/api-routes.js b/routes/api-routes.js index 0b9434be..68331d71 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -73,21 +73,20 @@ module.exports = (app) => { }); // route to run a publish request on the daemon app.post('/api/publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { - let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, skipAuth, channelName, channelPassword; + let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, anonymous, channelName, channelPassword; // validate the body and files of the request try { // validateApiPublishRequest(body, files); ({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body)); ({fileName, filePath, fileType} = parsePublishApiRequestFiles(files)); - ({channelName, channelPassword, skipAuth} = parsePublishApiChannel(body, user)); + ({anonymous, channelName, channelPassword} = parsePublishApiChannel(body, user)); } catch (error) { logger.debug('publish request rejected, insufficient request parameters'); return res.status(400).json({success: false, message: error.message}); } - logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); // check channel authorization - authenticateOrSkip(skipAuth, channelName, channelPassword) + authenticateOrSkip(anonymous, channelName, channelPassword) .then(authenticated => { if (!authenticated) { throw new Error('Authentication failed, you do not have access to that channel'); diff --git a/test/publishApiTests.js b/test/publishApiTests.js index 5f923099..6cb431b0 100644 --- a/test/publishApiTests.js +++ b/test/publishApiTests.js @@ -1,14 +1,6 @@ const assert = require('assert'); -describe('Array', function () { - describe('indexOf()', function () { - it('should return -1 when the value is not present', function () { - assert.equal(-1, [1, 2, 3].indexOf(4)); - }); - }); -}); - -describe('controllers', function () { +describe('api', function () { describe('api/publish', function () { describe('publishHelpers.js', function () { const publishHelpers = require('../helpers/publishHelpers.js'); @@ -71,6 +63,27 @@ describe('controllers', function () { assert.doesNotThrow(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); }); }); + + describe('#parsePublishApiChannel()', function () { + it('should return a channel name if one is provided', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); + }); + it('should return a password if one is provided', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); + }); + it('should return a channel name if one is provided in req.user', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); + }); + it('should return a password if one is provided in req.user', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + it('should return anonymous === true if meant to be anonymous even if req.user is filled', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + it('should return anonymous === false a channel is provided', function () { + // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + }); }); }); }); -- 2.45.2 From e32e74123687eb9c8e2a54e695ce1a0797692fa4 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 07:24:29 -0800 Subject: [PATCH 03/15] added end to end tests with chai --- .eslintignore | 3 +- .eslintrc | 2 +- package.json | 6 +- test/end-to-end/end-to-end.tests.js | 81 ++++++++++++++++++++++++++ test/helpers/publishHelpers.test.js | 68 ++++++++++++++++++++++ test/publishApiTests.js | 89 ----------------------------- 6 files changed, 157 insertions(+), 92 deletions(-) create mode 100644 test/end-to-end/end-to-end.tests.js create mode 100644 test/helpers/publishHelpers.test.js delete mode 100644 test/publishApiTests.js diff --git a/.eslintignore b/.eslintignore index 18d5e871..07d60219 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules/ -public/ \ No newline at end of file +public/ +test diff --git a/.eslintrc b/.eslintrc index 25af7e9b..4a69de57 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,7 @@ ], "semi": [ "error", - "always", + "always", { "omitLastInOneLineBlock": true } ], "key-spacing": [ diff --git a/package.json b/package.json index 45891d08..27c23f39 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "a single-serving site that reads and publishes images to and from the LBRY blockchain", "main": "speech.js", "scripts": { - "test": "mocha", + "test": "mocha --recursive", "start": "node speech.js", "lint": "eslint .", "fix": "eslint . --fix", @@ -40,6 +40,8 @@ "nodemon": "^1.11.0", "passport": "^0.4.0", "passport-local": "^1.0.0", + "request": "^2.83.0", + "request-promise": "^4.2.2", "sequelize": "^4.1.0", "sequelize-cli": "^3.0.0-3", "sleep": "^5.1.1", @@ -48,6 +50,8 @@ "winston-slack-webhook": "billbitt/winston-slack-webhook" }, "devDependencies": { + "chai": "^4.1.2", + "chai-http": "^3.0.0", "eslint": "3.19.0", "eslint-config-standard": "10.2.1", "eslint-plugin-import": "^2.2.0", diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js new file mode 100644 index 00000000..6f797279 --- /dev/null +++ b/test/end-to-end/end-to-end.tests.js @@ -0,0 +1,81 @@ +const chai = require('chai'); +const expect = chai.expect; +const chaiHttp = require('chai-http'); +const host = 'http://dev1.spee.ch'; + +chai.use(chaiHttp); + +function testFor200StatusResponse (host, url, timeout) { + return it(`should receive a status code 200 within ${timeout}ms`, function (done) { + chai.request(host) + .get(url) + .end(function (err, res) { + expect(err).to.be.null; + expect(res).to.have.status(200); + done(); + }); + }).timeout(timeout); +} + +function testShowRequestFor200StatusResponse (host, url, timeout) { + return it(`should receive a status code 200 within ${timeout}ms`, function (done) { + chai.request(host) + .get(url) + .set('accept', 'text/html') + .end(function (err, res) { + expect(err).to.be.null; + expect(res).to.have.status(200); + done(); + }); + }).timeout(timeout); +} + +describe('end-to-end', function () { + describe('serve requests not from browser', function () { + const claimUrl = '/doitlive.jpg'; + const claimUrlWithShortClaimId = '/d/doitlive.jpg'; + const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg'; + + describe(claimUrl, function () { + testFor200StatusResponse(host, claimUrl, 10000); + }); + describe(claimUrlWithShortClaimId, function () { + testFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + describe(claimUrlWithLongClaimId, function () { + testFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + }); + + describe('show requests from browser', function () { + const claimUrl = '/doitlive'; + const claimUrlWithShortClaimId = '/d/doitlive'; + const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive'; + + describe(claimUrl, function () { + testShowRequestFor200StatusResponse(host, claimUrl, 10000); + }); + describe(claimUrlWithShortClaimId, function () { + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + describe(claimUrlWithLongClaimId, function () { + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + }); + + describe('serve requests browser (show lite)', function () { + const claimUrl = '/doitlive.jpg'; + const claimUrlWithShortClaimId = '/d/doitlive.jpg'; + const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg'; + + describe(claimUrl, function () { + testShowRequestFor200StatusResponse(host, claimUrl, 10000); + }); + describe(claimUrlWithShortClaimId, function () { + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + describe(claimUrlWithLongClaimId, function () { + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + }); + }); +}); diff --git a/test/helpers/publishHelpers.test.js b/test/helpers/publishHelpers.test.js new file mode 100644 index 00000000..ad42ec15 --- /dev/null +++ b/test/helpers/publishHelpers.test.js @@ -0,0 +1,68 @@ +const assert = require('assert'); + +describe('publishHelpers.js', function () { + const publishHelpers = require('../../helpers/publishHelpers.js'); + + describe('#parsePublishApiRequestBody()', function () { + it('should throw an error if no body', function () { + assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, null), Error); + }); + it('should throw an error if no body.name', function () { + const bodyNoName = {}; + assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, bodyNoName), Error); + }); + it('should throw an error if no body.name', function () { + const body = { + name: 'bob', + }; + assert.doesNotThrow(publishHelpers.parsePublishApiRequestBody.bind(this, body), Error); + }); + }); + + describe('#parsePublishApiRequestFiles()', function () { + it('should throw an error if no files', function () { + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); + }); + it('should throw an error if no files.file', function () { + const filesNoFile = {}; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); + }); + it('should throw an error if file.size is too large', function () { + const filesTooBig = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'image/jpg', + size: 10000001, + }, + }; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); + }); + it('should throw error if not an accepted file type', function () { + const filesNoProblems = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'someType/ext', + size: 10000000, + }, + }; + assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + it('should throw NO error if no problems', function () { + const filesNoProblems = { + file: { + name: 'file.jpg', + path: '/path/to/file.jpg', + type: 'image/jpg', + size: 10000000, + }, + }; + assert.doesNotThrow(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + }); + }); + + describe('#parsePublishApiChannel()', function () { + it('should pass the tests I write here'); + }); +}); diff --git a/test/publishApiTests.js b/test/publishApiTests.js deleted file mode 100644 index 6cb431b0..00000000 --- a/test/publishApiTests.js +++ /dev/null @@ -1,89 +0,0 @@ -const assert = require('assert'); - -describe('api', function () { - describe('api/publish', function () { - describe('publishHelpers.js', function () { - const publishHelpers = require('../helpers/publishHelpers.js'); - - describe('#parsePublishApiRequestBody()', function () { - it('should throw an error if no body', function () { - assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, null), Error); - }); - it('should throw an error if no body.name', function () { - const bodyNoName = {}; - assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, bodyNoName), Error); - }); - it('should throw an error if no body.name', function () { - const body = { - name: 'bob', - }; - assert.doesNotThrow(publishHelpers.parsePublishApiRequestBody.bind(this, body), Error); - }); - }); - - describe('#parsePublishApiRequestFiles()', function () { - it('should throw an error if no files', function () { - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); - }); - it('should throw an error if no files.file', function () { - const filesNoFile = {}; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); - }); - it('should throw an error if file.size is too large', function () { - const filesTooBig = { - file: { - name: 'file.jpg', - path: '/path/to/file.jpg', - type: 'image/jpg', - size: 10000001, - }, - }; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); - }); - it('should throw error if not an accepted file type', function () { - const filesNoProblems = { - file: { - name: 'file.jpg', - path: '/path/to/file.jpg', - type: 'someType/ext', - size: 10000000, - }, - }; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); - }); - it('should throw NO error if no problems', function () { - const filesNoProblems = { - file: { - name: 'file.jpg', - path: '/path/to/file.jpg', - type: 'image/jpg', - size: 10000000, - }, - }; - assert.doesNotThrow(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); - }); - }); - - describe('#parsePublishApiChannel()', function () { - it('should return a channel name if one is provided', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); - }); - it('should return a password if one is provided', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); - }); - it('should return a channel name if one is provided in req.user', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); - }); - it('should return a password if one is provided in req.user', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); - }); - it('should return anonymous === true if meant to be anonymous even if req.user is filled', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); - }); - it('should return anonymous === false a channel is provided', function () { - // assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); - }); - }); - }); - }); -}); -- 2.45.2 From 98da3af7d3acfa6b8d94d5aa88beca7c021886da Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 09:51:26 -0800 Subject: [PATCH 04/15] updated publish api channel parse function --- helpers/publishHelpers.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 3b4b0f92..14d23e96 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -57,20 +57,25 @@ module.exports = { }; }, parsePublishApiChannel ({channelName, channelPassword}, user) { - // anonymous if no channel name provided - let anonymous = (channelName === null || channelName === undefined || channelName === ''); - // if a channel name is provided, get password from the user token - if (user) { - channelPassword = user.channelPassword; - } ; - // cleanse channel name + // if no channel name provided, publish will be anonymous + // if a channel name is provided... if (channelName) { + // make sure a password was provided + if (!channelPassword) { + throw new Error('Channel name provided without password'); + } + // if request comes from the client and is logged in + // ensure it is the same channel and get the password + if (user) { + channelName = user.channelName; + channelPassword = user.channelPassword; + } ; + // add the @ if the channel name is missing it if (channelName.indexOf('@') !== 0) { channelName = `@${channelName}`; } } return { - anonymous, channelName, channelPassword, }; -- 2.45.2 From 604d3d91e5fe54fbf12d7596033dba397ed9611c Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 10:12:21 -0800 Subject: [PATCH 05/15] updated /claim-publish route name --- routes/api-routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api-routes.js b/routes/api-routes.js index fd49283e..dd39a075 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -124,7 +124,7 @@ module.exports = (app) => { }); }); // route to run a publish request on the daemon - app.post('/api/publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { + app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, anonymous, channelName, channelPassword; // validate the body and files of the request try { -- 2.45.2 From 94c2fcca4cdd482ff6cc15e4204f71fa46297dd9 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 10:26:51 -0800 Subject: [PATCH 06/15] updated authentication for publish api --- auth/authentication.js | 4 ++-- helpers/publishHelpers.js | 1 - routes/api-routes.js | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/auth/authentication.js b/auth/authentication.js index f23d702e..22e391b2 100644 --- a/auth/authentication.js +++ b/auth/authentication.js @@ -34,9 +34,9 @@ module.exports = { }); }); }, - authenticateOrSkip (skipAuth, channelName, channelPassword) { + authenticateIfNoUserToken (channelName, channelPassword, user) { return new Promise((resolve, reject) => { - if (skipAuth) { + if (user) { return resolve(true); } return resolve(module.exports.authenticateChannelCredentials(channelName, channelPassword)); diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 5803a04c..c1a179fd 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -68,7 +68,6 @@ module.exports = { // ensure it is the same channel and get the password if (user) { channelName = user.channelName; - channelPassword = user.channelPassword; } ; // add the @ if the channel name is missing it if (channelName.indexOf('@') !== 0) { diff --git a/routes/api-routes.js b/routes/api-routes.js index dd39a075..57414bcf 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -7,7 +7,7 @@ const { checkClaimNameAvailability, checkChannelAvailability, publish } = requir const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js'); const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel } = require('../helpers/publishHelpers.js'); const errorHandlers = require('../helpers/errorHandlers.js'); -const { authenticateOrSkip } = require('../auth/authentication.js'); +const { authenticateIfNoUserToken } = require('../auth/authentication.js'); function addGetResultsToFileData (fileInfo, getResult) { fileInfo.fileName = getResult.file_name; @@ -125,20 +125,20 @@ module.exports = (app) => { }); // route to run a publish request on the daemon app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { - let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, anonymous, channelName, channelPassword; + let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword; // validate the body and files of the request try { // validateApiPublishRequest(body, files); ({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body)); ({fileName, filePath, fileType} = parsePublishApiRequestFiles(files)); - ({anonymous, channelName, channelPassword} = parsePublishApiChannel(body, user)); + ({channelName, channelPassword} = parsePublishApiChannel(body, user)); } catch (error) { logger.debug('publish request rejected, insufficient request parameters'); return res.status(400).json({success: false, message: error.message}); } logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); // check channel authorization - authenticateOrSkip(anonymous, channelName, channelPassword) + authenticateIfNoUserToken(channelName, channelPassword, user) .then(authenticated => { if (!authenticated) { throw new Error('Authentication failed, you do not have access to that channel'); -- 2.45.2 From 592fbb79d8b2f59943d749f35acd129ca833f1e5 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 10:30:32 -0800 Subject: [PATCH 07/15] updated publish channel parser to watch for anonymous --- helpers/publishHelpers.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index c1a179fd..7e0cd904 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -57,7 +57,12 @@ module.exports = { }; }, parsePublishApiChannel ({channelName, channelPassword}, user) { + logger.debug('publish api parser input:', {channelName, channelPassword, user}); // if no channel name provided, publish will be anonymous + // if anonymous or '' provided, publish will be anonymous (even if client is logged in) + if (channelName === 'anonymous' || channelName === '') { + channelName = null; + } // if a channel name is provided... if (channelName) { // make sure a password was provided -- 2.45.2 From 9a93b3cc8df4da022717c425c86abffa643c5a3b Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 11:02:04 -0800 Subject: [PATCH 08/15] tested basic client scenarios --- helpers/publishHelpers.js | 14 +++++--------- public/assets/js/publishFileFunctions.js | 11 +++++++---- routes/api-routes.js | 1 + 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 7e0cd904..65370250 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -58,19 +58,15 @@ module.exports = { }, parsePublishApiChannel ({channelName, channelPassword}, user) { logger.debug('publish api parser input:', {channelName, channelPassword, user}); - // if no channel name provided, publish will be anonymous // if anonymous or '' provided, publish will be anonymous (even if client is logged in) - if (channelName === 'anonymous' || channelName === '') { - channelName = null; - } // if a channel name is provided... if (channelName) { - // make sure a password was provided - if (!channelPassword) { - throw new Error('Channel name provided without password'); + // make sure a password was provided if no user token is provided + if (!user && !channelPassword) { + throw new Error('Unauthenticated channel name provided without password'); } - // if request comes from the client and is logged in - // ensure it is the same channel and get the password + // if request comes from the client with a token + // ensure this publish uses that channel name if (user) { channelName = user.channelName; } ; diff --git a/public/assets/js/publishFileFunctions.js b/public/assets/js/publishFileFunctions.js index 15a4c5a9..f1255d70 100644 --- a/public/assets/js/publishFileFunctions.js +++ b/public/assets/js/publishFileFunctions.js @@ -78,17 +78,20 @@ const publishFileFunctions = { const licenseInput = document.getElementById('publish-license'); const nsfwInput = document.getElementById('publish-nsfw'); const thumbnailInput = document.getElementById('claim-thumbnail-input'); - - return { + const channelName = this.returnNullOrChannel(); + let metadata = { name: nameInput.value.trim(), - channelName: this.returnNullOrChannel(), title: titleInput.value.trim(), description: descriptionInput.value.trim(), license: licenseInput.value.trim(), nsfw: nsfwInput.checked, type: stagedFiles[0].type, thumbnail: thumbnailInput.value.trim(), + }; + if (channelName) { + metadata['channelName'] = channelName; } + return metadata; }, appendDataToFormData: function (file, metadata) { var fd = new FormData(); @@ -143,7 +146,7 @@ const publishFileFunctions = { publishStagedFile: function (event) { event.preventDefault(); // prevent default so this script can handle submission const metadata = this.createMetadata(); - const that = this; // note: necessary ? + const that = this; const fileSelectionInputError = document.getElementById('input-error-file-selection'); const claimNameError = document.getElementById('input-error-claim-name'); const channelSelectError = document.getElementById('input-error-channel-select'); diff --git a/routes/api-routes.js b/routes/api-routes.js index 57414bcf..9df1bcb9 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -125,6 +125,7 @@ module.exports = (app) => { }); // route to run a publish request on the daemon app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { + logger.debug('api/claim-publish body:', body); let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword; // validate the body and files of the request try { -- 2.45.2 From 4a75ac71279c33ee48ffe9e4af5a4e2b2d0b3c00 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 11:19:10 -0800 Subject: [PATCH 09/15] passed second set of user tests --- auth/authentication.js | 2 +- public/assets/js/publishFileFunctions.js | 2 -- routes/api-routes.js | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/auth/authentication.js b/auth/authentication.js index 22e391b2..7feb7c0f 100644 --- a/auth/authentication.js +++ b/auth/authentication.js @@ -36,7 +36,7 @@ module.exports = { }, authenticateIfNoUserToken (channelName, channelPassword, user) { return new Promise((resolve, reject) => { - if (user) { + if (user || !channelName) { return resolve(true); } return resolve(module.exports.authenticateChannelCredentials(channelName, channelPassword)); diff --git a/public/assets/js/publishFileFunctions.js b/public/assets/js/publishFileFunctions.js index f1255d70..41922e08 100644 --- a/public/assets/js/publishFileFunctions.js +++ b/public/assets/js/publishFileFunctions.js @@ -135,8 +135,6 @@ const publishFileFunctions = { } else { that.showFilePublishFailure(JSON.parse(xhr.response).message); } - } else { - console.log('xhr.readyState', xhr.readyState, 'xhr.status', xhr.status); } }; // Initiate a multipart/form-data upload diff --git a/routes/api-routes.js b/routes/api-routes.js index 9df1bcb9..b6472216 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -137,7 +137,6 @@ module.exports = (app) => { logger.debug('publish request rejected, insufficient request parameters'); return res.status(400).json({success: false, message: error.message}); } - logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`); // check channel authorization authenticateIfNoUserToken(channelName, channelPassword, user) .then(authenticated => { -- 2.45.2 From 9f3a8fd27417a13473f5be42417051656120c0e1 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 12:21:33 -0800 Subject: [PATCH 10/15] exchaned assert library for chai package --- test/end-to-end/end-to-end.tests.js | 2 +- test/helpers/publishHelpers.test.js | 25 ++++++++++--------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 6f797279..0c81d5c9 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -1,7 +1,7 @@ const chai = require('chai'); const expect = chai.expect; const chaiHttp = require('chai-http'); -const host = 'http://dev1.spee.ch'; +const { host } = require('../../config/speechConfig.js').site; chai.use(chaiHttp); diff --git a/test/helpers/publishHelpers.test.js b/test/helpers/publishHelpers.test.js index ad42ec15..358e989a 100644 --- a/test/helpers/publishHelpers.test.js +++ b/test/helpers/publishHelpers.test.js @@ -1,31 +1,26 @@ -const assert = require('assert'); +const chai = require('chai'); +const expect = chai.expect; describe('publishHelpers.js', function () { const publishHelpers = require('../../helpers/publishHelpers.js'); describe('#parsePublishApiRequestBody()', function () { it('should throw an error if no body', function () { - assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, null), Error); + expect(publishHelpers.parsePublishApiRequestBody.bind(this, null)).to.throw(); }); it('should throw an error if no body.name', function () { const bodyNoName = {}; - assert.throws(publishHelpers.parsePublishApiRequestBody.bind(this, bodyNoName), Error); - }); - it('should throw an error if no body.name', function () { - const body = { - name: 'bob', - }; - assert.doesNotThrow(publishHelpers.parsePublishApiRequestBody.bind(this, body), Error); + expect(publishHelpers.parsePublishApiRequestBody.bind(this, bodyNoName)).to.throw(); }); }); describe('#parsePublishApiRequestFiles()', function () { it('should throw an error if no files', function () { - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, null), Error); + expect(publishHelpers.parsePublishApiRequestFiles.bind(this, null)).to.throw(); }); it('should throw an error if no files.file', function () { const filesNoFile = {}; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile), Error); + expect(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoFile)).to.throw(); }); it('should throw an error if file.size is too large', function () { const filesTooBig = { @@ -36,10 +31,10 @@ describe('publishHelpers.js', function () { size: 10000001, }, }; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig), Error); + expect(publishHelpers.parsePublishApiRequestFiles.bind(this, filesTooBig)).to.throw(); }); it('should throw error if not an accepted file type', function () { - const filesNoProblems = { + const filesWrongType = { file: { name: 'file.jpg', path: '/path/to/file.jpg', @@ -47,7 +42,7 @@ describe('publishHelpers.js', function () { size: 10000000, }, }; - assert.throws(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + expect(publishHelpers.parsePublishApiRequestFiles.bind(this, filesWrongType)).to.throw(); }); it('should throw NO error if no problems', function () { const filesNoProblems = { @@ -58,7 +53,7 @@ describe('publishHelpers.js', function () { size: 10000000, }, }; - assert.doesNotThrow(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems), Error); + expect(publishHelpers.parsePublishApiRequestFiles.bind(this, filesNoProblems)).to.not.throw(); }); }); -- 2.45.2 From 91ebb8f6c62508b6a7b4584ef1b9cfe61b703c81 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 12:28:02 -0800 Subject: [PATCH 11/15] refactored timeout in end to end tests --- test/end-to-end/end-to-end.tests.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 0c81d5c9..142b39fd 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -2,10 +2,11 @@ const chai = require('chai'); const expect = chai.expect; const chaiHttp = require('chai-http'); const { host } = require('../../config/speechConfig.js').site; +const timeout = 600000; chai.use(chaiHttp); -function testFor200StatusResponse (host, url, timeout) { +function testFor200StatusResponse (host, url) { return it(`should receive a status code 200 within ${timeout}ms`, function (done) { chai.request(host) .get(url) @@ -17,7 +18,7 @@ function testFor200StatusResponse (host, url, timeout) { }).timeout(timeout); } -function testShowRequestFor200StatusResponse (host, url, timeout) { +function testShowRequestFor200StatusResponse (host, url) { return it(`should receive a status code 200 within ${timeout}ms`, function (done) { chai.request(host) .get(url) @@ -37,13 +38,13 @@ describe('end-to-end', function () { const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg'; describe(claimUrl, function () { - testFor200StatusResponse(host, claimUrl, 10000); + testFor200StatusResponse(host, claimUrl); }); describe(claimUrlWithShortClaimId, function () { - testFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testFor200StatusResponse(host, claimUrlWithShortClaimId); }); describe(claimUrlWithLongClaimId, function () { - testFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testFor200StatusResponse(host, claimUrlWithShortClaimId); }); }); @@ -53,13 +54,13 @@ describe('end-to-end', function () { const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive'; describe(claimUrl, function () { - testShowRequestFor200StatusResponse(host, claimUrl, 10000); + testShowRequestFor200StatusResponse(host, claimUrl); }); describe(claimUrlWithShortClaimId, function () { - testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId); }); describe(claimUrlWithLongClaimId, function () { - testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId); }); }); @@ -69,13 +70,13 @@ describe('end-to-end', function () { const claimUrlWithLongClaimId = '/ca3023187e901df9e9aabd95d6ae09b6cc69b3f0/doitlive.jpg'; describe(claimUrl, function () { - testShowRequestFor200StatusResponse(host, claimUrl, 10000); + testShowRequestFor200StatusResponse(host, claimUrl); }); describe(claimUrlWithShortClaimId, function () { - testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId); }); describe(claimUrlWithLongClaimId, function () { - testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId, 10000); + testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId); }); }); }); -- 2.45.2 From c52a5f87e4c2c7c9fe94ed48940543f9bad1d998 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 14:10:34 -0800 Subject: [PATCH 12/15] wrote a publish end-to-end test --- helpers/publishHelpers.js | 1 + routes/api-routes.js | 3 ++- test/end-to-end/end-to-end.tests.js | 37 ++++++++++++++++++++++++---- test/mock-files/bird.jpeg | Bin 0 -> 91961 bytes testpage.html | 17 ------------- 5 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 test/mock-files/bird.jpeg delete mode 100644 testpage.html diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 65370250..233410db 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -29,6 +29,7 @@ module.exports = { }; }, parsePublishApiRequestFiles ({file}) { + logger.debug('file', file); // make sure a file was provided if (!file) { throw new Error('no file with key of [file] found in request'); diff --git a/routes/api-routes.js b/routes/api-routes.js index b6472216..bd9ac3e1 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -126,6 +126,7 @@ module.exports = (app) => { // route to run a publish request on the daemon app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => { logger.debug('api/claim-publish body:', body); + logger.debug('api/claim-publish files:', files); let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword; // validate the body and files of the request try { @@ -134,7 +135,7 @@ module.exports = (app) => { ({fileName, filePath, fileType} = parsePublishApiRequestFiles(files)); ({channelName, channelPassword} = parsePublishApiChannel(body, user)); } catch (error) { - logger.debug('publish request rejected, insufficient request parameters'); + logger.debug('publish request rejected, insufficient request parameters', error); return res.status(400).json({success: false, message: error.message}); } // check channel authorization diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 142b39fd..40dd4e66 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -2,12 +2,14 @@ const chai = require('chai'); const expect = chai.expect; const chaiHttp = require('chai-http'); const { host } = require('../../config/speechConfig.js').site; -const timeout = 600000; +const requestTimeout = 20000; +const publishTimeout = 120000; +const fs = require('fs'); chai.use(chaiHttp); function testFor200StatusResponse (host, url) { - return it(`should receive a status code 200 within ${timeout}ms`, function (done) { + return it(`should receive a status code 200 within ${requestTimeout}ms`, function (done) { chai.request(host) .get(url) .end(function (err, res) { @@ -15,11 +17,11 @@ function testFor200StatusResponse (host, url) { expect(res).to.have.status(200); done(); }); - }).timeout(timeout); + }).timeout(requestTimeout); } function testShowRequestFor200StatusResponse (host, url) { - return it(`should receive a status code 200 within ${timeout}ms`, function (done) { + return it(`should receive a status code 200 within ${requestTimeout}ms`, function (done) { chai.request(host) .get(url) .set('accept', 'text/html') @@ -28,7 +30,7 @@ function testShowRequestFor200StatusResponse (host, url) { expect(res).to.have.status(200); done(); }); - }).timeout(timeout); + }).timeout(requestTimeout); } describe('end-to-end', function () { @@ -79,4 +81,29 @@ describe('end-to-end', function () { testShowRequestFor200StatusResponse(host, claimUrlWithShortClaimId); }); }); + + describe('publish', function () { + const publishUrl = '/api/claim-publish'; + const name = 'test-name2'; + const filePath = './test/mock-files/bird.jpeg'; + const fileName = 'byrd.jpeg'; + + describe(publishUrl, function () { + it(`should receive a status code 200 within ${publishTimeout}ms`, function (done) { + chai.request(host) + .post(publishUrl) + .type('form') + .attach('file', fs.readFileSync(filePath), fileName) + .field('name', name) + .end(function (err, res) { + // expect(err).to.be.null; + expect(res).to.have.status(200); + done(); + }); + }).timeout(publishTimeout); + }); + + }); + + }); diff --git a/test/mock-files/bird.jpeg b/test/mock-files/bird.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cd1b140c088304eccada462bf131e00757f20c78 GIT binary patch literal 91961 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 z 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 - - - - Test Page - - - - - - - - - - - - -- 2.45.2 From 7cde7354350f54f249174a7cab4c9d48670d04d6 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Fri, 15 Dec 2017 14:11:42 -0800 Subject: [PATCH 13/15] renamed unit test folder --- test/{helpers => unit}/publishHelpers.test.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{helpers => unit}/publishHelpers.test.js (100%) diff --git a/test/helpers/publishHelpers.test.js b/test/unit/publishHelpers.test.js similarity index 100% rename from test/helpers/publishHelpers.test.js rename to test/unit/publishHelpers.test.js -- 2.45.2 From 07d0031f1cf5ca519e9f24f48845c7d5229f8585 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Mon, 18 Dec 2017 14:18:42 -0800 Subject: [PATCH 14/15] updated publish test to choose dynamic name --- test/end-to-end/end-to-end.tests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 40dd4e66..5a016d10 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -84,7 +84,8 @@ describe('end-to-end', function () { describe('publish', function () { const publishUrl = '/api/claim-publish'; - const name = 'test-name2'; + const date = new Date(); + const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`; const filePath = './test/mock-files/bird.jpeg'; const fileName = 'byrd.jpeg'; -- 2.45.2 From 5ade0ec85622a6c4160f75600268d18a97bcaf04 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Mon, 18 Dec 2017 15:23:23 -0800 Subject: [PATCH 15/15] added flag for tests requiring lbc --- README.md | 5 +++++ package.json | 1 + test/end-to-end/end-to-end.tests.js | 4 ++-- test/{mock-files => mock-data}/bird.jpeg | Bin 4 files changed, 8 insertions(+), 2 deletions(-) rename test/{mock-files => mock-data}/bird.jpeg (100%) diff --git a/README.md b/README.md index 5669b79c..14b3f048 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ spee.ch is a single-serving site that reads and publishes images and videos to a * To run hot, use `nodemon` instead of `node` * visit [localhost:3000](http://localhost:3000) +## Tests +* Spee.ch uses `mocha` with `chai` for testing. +* To run all tests that do not require LBC, run `npm test -- --grep @usesLbc --invert` +* To run all tests, including those that require LBC (like publishing), simply run `npm test` + ## API #### GET diff --git a/package.json b/package.json index 27c23f39..3a326898 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "speech.js", "scripts": { "test": "mocha --recursive", + "test-all": "mocha --recursive", "start": "node speech.js", "lint": "eslint .", "fix": "eslint . --fix", diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 5a016d10..d26d5853 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -86,11 +86,11 @@ describe('end-to-end', function () { const publishUrl = '/api/claim-publish'; const date = new Date(); const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`; - const filePath = './test/mock-files/bird.jpeg'; + const filePath = './test/mock-data/bird.jpeg'; const fileName = 'byrd.jpeg'; describe(publishUrl, function () { - it(`should receive a status code 200 within ${publishTimeout}ms`, function (done) { + it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) { chai.request(host) .post(publishUrl) .type('form') diff --git a/test/mock-files/bird.jpeg b/test/mock-data/bird.jpeg similarity index 100% rename from test/mock-files/bird.jpeg rename to test/mock-data/bird.jpeg -- 2.45.2