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);
+        });
+      });
+    });
+  });
+});