From 75fac0e5945fa9bc16371613d902391b96339bdc Mon Sep 17 00:00:00 2001 From: bill bittner <bittner.w@gmail.com> Date: Fri, 8 Dec 2017 17:50:47 -0800 Subject: [PATCH] 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); + }); + }); + }); + }); +});