diff --git a/.vscode/launch.json b/.vscode/launch.json index 62f3296..57a819a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,19 +8,16 @@ "type": "node", "name": "Launch Jest Tests", "request": "launch", + "env": {"NODE_ENV": "test"}, "args": [ - "--runInBand" + "--runInBand", + "-i" ], "cwd": "${workspaceFolder}", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - }, - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}/server.js" + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + "runtimeExecutable": "/usr/local/bin/node" }, ] } diff --git a/README.md b/README.md index 46e885e..132b4fe 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ [![NodeJS Version](https://img.shields.io/badge/Node%20Version-%3E%3D%20v8.0.0-green.svg)](https://img.shields.io/badge/Node%20Version-%3E%3D%20v8.0.0-green.svg) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html) -**NOTE: THIS PACKAGE IS STILL IN DEVELOPMENT! RELEASE OF v1.0.0 WILL BE AROUND DECEMBER 1st, 2018.** - Blog API developed by Bytecode Digital Agency as free (as in freedom) open source software. Built in NodeJS. Available as a standalone server, or as a NPM package ## Installation @@ -35,36 +33,82 @@ For contributing to the development, fork the [GitHub repository](https://github To use the NodeJS Blog module, first, import the package ```js -const NodeBlog = require('nodejs-blog'); +const nodeBlog = require('nodejs-blog'); +const { authors, auth, users, categories, articles } = require('nodejs-blog'); ``` -or using ES6 modules +to start using the package, create a new instance of the NodeBlog class ```js -import NodeBlog from 'nodejs-blog'; +const client = 'YOUR_DB_CLIENT'; // for more info, see https://knexjs.org/ +const host = 'YOUR_DB_HOST'; +const database = 'YOUR_DB_NAME'; +const user = 'YOUR_DB_USER'; +const pass = 'YOUR_DB_PASS'; +const debug = true || false; + +const blog = nodeBlog(client, host, user, database, password, debug); ``` -to start using the package, create a new instance of the NodeBlog class +For authentication you should set the following environment (`process.env.[variable] = value`) variables, or the auth methods will not work: +``` +SALT_ROUNDS=number +JWT_SECRET=string +JWT_EXPIRES_IN_DAYS=number_of_days +``` + +Then you can use the imported functions as you wish, for example: ```js -const nodeBlogConfig = { - client: 'YOUR_DB_CLIENT', // for more info, see https://knexjs.org/ - host: 'YOUR_DB_HOST', - database: 'YOUR_DB_NAME', - user: 'YOUR_DB_USER', - pass: 'YOUR_DB_PASS', - debug: true || false, -}; -const nodeBlog = new NodeBlog(nodeBlogConfig); +const posts = await articles.list(blog); +``` + +Just send the `blog` instance as the first argument and the rest of the arguments second. This is because this way the same logic can be applied to multiple blog instances within an application. + +The available methods are: + +```js +authors.list(blog) +authors.get(blog, id) +authors.add(blog, authorObject) +authors.modify(blog, id, modifiedData) +authors.delete(blog, id) + +auth.authenticate(blog, username, password) // Returns true/false +auth.generateToken(blog, username, password) // Returns JWT, throws error if invalid credentials +auth.decode(jwt) // Returns decoded object +auth.validate(blog, username, password) // Returns true/false + +users.list(blog) +users.get(blog, id) +users.add(blog, userObject) +users.modify(blog, id, modifiedData) +users.delete(blog, id) + +categories.list(blog) +categories.get(blog, id) +categories.add(blog, categoryObject) +categories.modify(blog, id, modifiedData) +categories.delete(blog, id) + +articles.list(blog) +articles.get(blog, id) +articles.add(blog, articleObject) +// articles.modify(blog, id, modifiedData) // not available yet +articles.delete(blog, id) ``` We recommend creating a single file that will create the NodeBlog instance, and `export` this instance, and `import` in all other files where you want to use NodeJS Blog. For security reasons we recommend using environment variables for loading the configuration. This is also in compliance with the [12 factor app Config guidelines](https://12factor.net/config) -Note: NodeJS blog was made to be used with PostgreSQL, but it should(/could) also be compatible with other databases, as it uses KnexJS under the hood. +Note: NodeJS blog was made to be used with PostgreSQL, but it should(/could) also be compatible with other databases, as it uses [KnexJS](https://knexjs.org) under the hood. + +*A demo application is currently in development* + +## Running the API as a standalone service (still in development, might not work 100%) -## Running the API as a standalone service +First clone the repository and `cd` into the directory. To run NodeJS Blog as a standalone service, run `cp .env.example .env` to create the `.env` file. diff --git a/controllers/articles.js b/controllers/articles.js new file mode 100644 index 0000000..1fc9b21 --- /dev/null +++ b/controllers/articles.js @@ -0,0 +1,205 @@ +/* eslint max-len: ["error", { "code": 100 }] */ +/* eslint-disable prettier/prettier */ + +// const { knex } = require('../helpers'); + +const fieldsBase = [ + 'articles.id', + 'articles.title', + 'articles.subtitle', + 'articles.slug', + 'articles.posted_on', + 'article_content.image_url AS article_image_url', + 'article_content.summary', + 'authors.name AS author_name', + 'authors.role AS author_role', + 'authors.image_url AS author_image_url', + 'categories.name AS category_name', + 'categories.slug AS category_slug', +]; + +const calculateReadingTime = text => { + try { + const wordsPerMinute = 275; + const wordArr = text.split(' '); + const textWordAmount = wordArr.length; + const readingTimeInMinutes = Math.floor(textWordAmount / wordsPerMinute); + return readingTimeInMinutes; + } catch (err) { + return null; + } +}; + +const addReadingTimeToArticles = articles => { + const articlesWithReadingTime = articles.map(article => { + const articleContent = article.html_content; + const readingTime = calculateReadingTime(articleContent); + const readingTimeObject = { reading_time: readingTime }; + const updatedArticle = Object.assign({}, article, readingTimeObject); + return updatedArticle; + }); + return articlesWithReadingTime; +}; + +const listArticles = async knex => { + const fields = [ + ...fieldsBase, + 'article_content.html_content', + ]; + const articles = await knex + .select(fields) + .from('articles') + .join('article_content', 'article_content.article_id', '=', 'articles.id') + .join('categories', 'categories.id', '=', 'articles.category') + .join('authors', 'authors.id', '=', 'articles.author') + .where('articles.hidden', '=', false) + .andWhere('articles.posted_on', '<=', knex.raw('now()')); + const articlesWithReadingTime = addReadingTimeToArticles(articles); + return articlesWithReadingTime; +}; + +const getRelatedArticles = async (knex, id) => { + const fields = [ + ...fieldsBase, + 'article_content.html_content', + ]; + const relatedArticles = await knex + .select(fields) + .from('articles') + .join('article_content', 'article_content.article_id', '=', 'articles.id') + .join('categories', 'categories.id', '=', 'articles.category') + .join('authors', 'authors.id', '=', 'articles.author') + .join('related_articles', 'related_articles.related_article_id', '=', 'articles.id') + .where('related_articles.article_id', '=', id) + .andWhere('articles.hidden', '=', false) + .andWhere('articles.posted_on', '<=', knex.raw('now()')); + const articlesWithReadingTime = addReadingTimeToArticles(relatedArticles); + return articlesWithReadingTime; +}; + +const addRelatedArticlesToArticleObject = async (knex, id, article) => { + const relatedArticles = await getRelatedArticles(knex, id); + if (relatedArticles.length === 0) { + return article; + } + const articleWithRelatedArticles = { + ...article, + related_articles: relatedArticles, + }; + return articleWithRelatedArticles; +}; + +const getArticle = async (knex, id) => { + const fields = [ + ...fieldsBase, + 'article_content.html_content', + ]; + const articles = await knex + .select(fields) + .from('articles') // eslint-disable-next-line + .join('article_content', 'article_content.article_id', '=', 'articles.id') + .join('categories', 'categories.id', '=', 'articles.category') + .join('authors', 'authors.id', '=', 'articles.author') + .where('articles.id', '=', id); + const articlesWithReadingTime = addReadingTimeToArticles(articles); + const articleBase = articlesWithReadingTime[0]; + const article = await addRelatedArticlesToArticleObject(knex, id, articleBase); + return article; +}; + +const addToArticlesTable = async (knex, articleData) => { + const returning = ['id', 'title', 'subtitle', 'posted_on', 'slug', 'author', 'category']; + const addedArticle = await knex('articles') + .insert([articleData]) + .returning(returning); + return addedArticle[0]; +}; + +const addToArticleContentTable = async (knex, articleData) => { + const returning = ['summary', 'image_url', 'html_content']; + const addedArticle = await knex('article_content') + .insert([articleData]) + .returning(returning); + return addedArticle[0]; +}; + +const generateRelatedArticles = (id, relatedArticles) => { + const relatedArticlesObjects = relatedArticles.map(relatedArticle => { + const relatedArticleObject = { + article_id: id, + related_article_id: relatedArticle, + }; + return relatedArticleObject; + }); + return relatedArticlesObjects; +}; + +const addToRelatedArticlesTable = async (knex, id, relatedArticles) => { + if (!relatedArticles || relatedArticles.length === 0) { + return []; + } + const relatedArticlesArray = generateRelatedArticles(id, relatedArticles); + const addedRelatedArticles = await knex('related_articles') + .insert(relatedArticlesArray) + .returning('article_id', 'related_article_id'); + return addedRelatedArticles; +}; + +const addArticle = async (knex, article) => { + const articleData = { + title: article.title, + subtitle: article.subtitle, + posted_on: article.posted_on, + hidden: article.hidden, + slug: article.slug, + author: article.author, + category: article.author, + }; + const addedArticleData = await addToArticlesTable(knex, articleData); + const addedArticleId = addedArticleData.id; + const articleContentData = { + article_id: addedArticleId, + summary: article.summary, + image_url: article.image_url, + html_content: article.html_content, + }; + await addToArticleContentTable(knex, articleContentData); + const relatedArticleIds = article.related_articles; + await addToRelatedArticlesTable(knex, addedArticleId, relatedArticleIds); + const createdArticle = await getArticle(knex, addedArticleId); + return createdArticle; +}; + +// TODO: fix and test better +// eslint-disable-next-line +const modifyArticle = async (knex, id, article) => { + throw new Error('Article modification is not supported'); +}; + +const deleteArticle = async (knex, id) => { + await knex('related_articles') + .where({ article_id: id }) + .orWhere({ related_article_id: id }) + .delete(); + await knex('article_content') + .where({ article_id: id }) + .delete(); + await knex('articles') + .where({ id }) + .delete(); + return { id }; +}; + +module.exports = { + listArticles, + getRelatedArticles, + addRelatedArticlesToArticleObject, + calculateReadingTime, + addReadingTimeToArticles, + addToRelatedArticlesTable, + getArticle, + generateRelatedArticles, + addArticle, + modifyArticle, + deleteArticle, +}; diff --git a/controllers/auth.js b/controllers/auth.js index 44d7e4b..737036f 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -1,10 +1,10 @@ -const { authHelper, knex } = require('../helpers'); +const { authHelper } = require('../helpers'); const { Users } = require('./'); // eslint-disable-next-line const { checkPasswordHash, generateJWT, decodeJWT, validateJWT } = authHelper; -const getUserByUsername = async username => { +const getUserByUsername = async (knex, username) => { const user = await knex .select('*') .from('users') @@ -12,8 +12,8 @@ const getUserByUsername = async username => { return user[0]; }; -const authenticateUser = async (username, password) => { - const user = await getUserByUsername(username); +const authenticateUser = async (knex, username, password) => { + const user = await getUserByUsername(knex, username); const passwordHash = user.password; const correctPassword = await checkPasswordHash(password, passwordHash); if (correctPassword) { @@ -22,25 +22,25 @@ const authenticateUser = async (username, password) => { return false; }; -const generateTokenPayload = async username => { - const user = await getUserByUsername(username); +const generateTokenPayload = async (knex, username) => { + const user = await getUserByUsername(knex, username); const { id, email } = user; const tokenPayload = { id, username, email }; return tokenPayload; }; -const generateToken = async (username, password) => { - const isAuthenticated = await authenticateUser(username, password); +const generateToken = async (knex, username, password) => { + const isAuthenticated = await authenticateUser(knex, username, password); if (!isAuthenticated) { throw new Error('Incorrect credentials'); } - const payloadData = await generateTokenPayload(username); + const payloadData = await generateTokenPayload(knex, username); const token = generateJWT(payloadData); return token; }; -const validateToken = async token => { - let decodedToken = ''; +const validateToken = async (knex, token) => { + let decodedToken = null; // Check if token can be decoded, is valid format try { @@ -57,12 +57,16 @@ const validateToken = async token => { } // Check if user from payload exists - const tokenUserID = decodedToken.data.id; - const tokenUser = await Users.getUser(tokenUserID); - if (!tokenUser) { + try { + const tokenUserID = decodedToken.data.id; + const tokenUser = await Users.getUser(knex, tokenUserID); + if (!tokenUser) { + return false; + } + return true; + } catch (err) { return false; } - return true; }; module.exports = { diff --git a/controllers/authors.js b/controllers/authors.js index d215426..69ec064 100644 --- a/controllers/authors.js +++ b/controllers/authors.js @@ -1,11 +1,9 @@ -const { knex } = require('../helpers'); - -const listAuthors = async () => { +const listAuthors = async knex => { const authors = await knex.select('*').from('authors'); return authors; }; -const getAuthor = async id => { +const getAuthor = async (knex, id) => { const author = await knex .select('*') .from('authors') @@ -13,7 +11,7 @@ const getAuthor = async id => { return author[0]; }; -const addAuthor = async author => { +const addAuthor = async (knex, author) => { const newAuthorData = { name: author.name, image_url: author.image_url, @@ -26,11 +24,11 @@ const addAuthor = async author => { return newAuthor[0]; }; -const modifyAuthor = async (id, author) => { +const modifyAuthor = async (knex, id, author) => { // eslint-disable-next-line camelcase const { name, image_url, role } = author; const newAuthorData = { name, image_url, role }; - const oldAuthorData = getAuthor(id); + const oldAuthorData = getAuthor(knex, id); const newAuthor = Object.assign({}, { ...oldAuthorData, ...newAuthorData }); const returning = ['id', 'name', 'image_url', 'role']; const modifiedAuthor = await knex('authors') @@ -40,7 +38,7 @@ const modifyAuthor = async (id, author) => { return modifiedAuthor[0]; }; -const deleteAuthor = async id => +const deleteAuthor = async (knex, id) => new Promise(resolve => { knex('users') .where({ author_id: id }) diff --git a/controllers/categories.js b/controllers/categories.js new file mode 100644 index 0000000..becf087 --- /dev/null +++ b/controllers/categories.js @@ -0,0 +1,57 @@ +const listCategories = async knex => { + const Categories = await knex.select('*').from('categories'); + return Categories; +}; + +const getCategory = async (knex, id) => { + const category = await knex + .select('*') + .from('categories') + .where({ id }); + return category[0]; +}; + +const addCategory = async (knex, category) => { + const newCategoryData = { + name: category.name, + slug: category.slug, + }; + const returning = ['id', 'name', 'slug']; + const newCategory = await knex('categories') + .insert([newCategoryData]) + .returning(returning); + return newCategory[0]; +}; + +const modifyCategory = async (knex, id, category) => { + const { name, slug } = category; + const newCategoryData = { name, slug }; + const returning = ['id', 'name', 'slug']; + const oldCategoryData = getCategory(knex, id); + const newCategory = Object.assign( + {}, + { ...oldCategoryData, ...newCategoryData }, + ); + const modifiedCategory = await knex('categories') + .returning(returning) + .where('id', '=', id) + .update(newCategory); + return modifiedCategory[0]; +}; + +const deleteCategory = async (knex, id) => + new Promise(resolve => + knex('categories') + .returning(['id']) + .where({ id }) + .delete() + .then(data => resolve(data[0])), + ); // eslint-disable-line + +module.exports = { + listCategories, + getCategory, + addCategory, + modifyCategory, + deleteCategory, +}; diff --git a/controllers/controllers.js b/controllers/controllers.js index 275af08..322c1d0 100644 --- a/controllers/controllers.js +++ b/controllers/controllers.js @@ -1,9 +1,13 @@ const Authors = require('./authors'); const Users = require('./users'); -// const Categories = require('./categories'); -// const Posts = require('./posts'); +const Categories = require('./categories'); +const Auth = require('./auth'); +const Articles = require('./articles'); module.exports = { Authors, + Auth, Users, + Categories, + Articles, }; diff --git a/controllers/users.js b/controllers/users.js index e128d58..3876d57 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -1,11 +1,11 @@ -const { knex, authHelper } = require('../helpers'); +const { authHelper } = require('../helpers'); -const listUsers = async () => { +const listUsers = async knex => { const Users = await knex.select('*').from('users'); return Users; }; -const getUser = async id => { +const getUser = async (knex, id) => { const user = await knex .select('*') .from('users') @@ -13,7 +13,7 @@ const getUser = async id => { return user[0]; }; -const addUser = async user => { +const addUser = async (knex, user) => { const passwordHash = await authHelper.generatePasswordHash(user.password); const newUserData = { username: user.username, @@ -38,7 +38,7 @@ const addUser = async user => { return newUser[0]; }; -const modifyUser = async (id, user) => { +const modifyUser = async (knex, id, user) => { const { /* eslint-disable camelcase */ username, @@ -65,7 +65,7 @@ const modifyUser = async (id, user) => { 'password', 'author_id', ]; - const oldUserData = getUser(id); + const oldUserData = getUser(knex, id); const newUser = Object.assign({}, { ...oldUserData, ...newUserData }); const modifiedUser = await knex('users') .returning(returning) @@ -74,7 +74,7 @@ const modifyUser = async (id, user) => { return modifiedUser[0]; }; -const deleteUser = async id => +const deleteUser = async (knex, id) => new Promise(resolve => knex('users') .returning(['id']) diff --git a/database/migrations/20181030235206_initial_setup.js b/database/migrations/20181030235206_initial_setup.js index 8de8bad..4ad9af7 100644 --- a/database/migrations/20181030235206_initial_setup.js +++ b/database/migrations/20181030235206_initial_setup.js @@ -39,7 +39,7 @@ const createArticleContentTable = table => { table.integer('article_id').notNullable().unique().references('id').inTable('articles'); table.string('summary').notNullable(); table.string('image_url').notNullable(); - table.text('markdown_content').notNullable(); + table.text('html_content').notNullable(); }; const createRelatedArticlesTable = table => { diff --git a/database/seeds/test-data.js b/database/seeds/test-data.js index 780ba66..6ae1a6e 100644 --- a/database/seeds/test-data.js +++ b/database/seeds/test-data.js @@ -73,13 +73,13 @@ const insertArticleContent = knex => article_id: 1, summary: 'In short', image_url: 'http://placekitten.com/500/500', - markdown_content: 'In long', + html_content: 'In long', }, { article_id: 2, summary: 'In short2', image_url: 'http://placekitten.com/500/500', - markdown_content: 'In long2', + html_content: 'In long2', }, ]); diff --git a/helpers/auth-helper.js b/helpers/auth-helper.js index 92e92c8..e0cdc88 100644 --- a/helpers/auth-helper.js +++ b/helpers/auth-helper.js @@ -30,8 +30,8 @@ const checkPasswordHash = async (plainTextPassword, hashedPassword) => { const calculateDates = issuedAtParam => { const date = new Date(); const issuedAt = issuedAtParam || date.setDate(date.getDate()); - const issuedAtDate = new Date(issuedAt); - const expiryDate = date.setDate(issuedAtDate.getDate() + jwtExpiresInDays); + const issuedAtDate = new Date(issuedAt); // eslint-disable-next-line + const expiryDate = issuedAtDate.setDate(issuedAtDate.getDate() + jwtExpiresInDays); const dates = { iat: issuedAt, exp: expiryDate, @@ -66,7 +66,7 @@ const validateJWT = token => { if (decoded.exp < now) { throw new Error('Token expired'); } - return decoded; + return { ...decoded, now }; }; const authHelper = { diff --git a/package.json b/package.json index f8d3373..07ca84e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodejs-blog", - "version": "0.0.5", + "version": "0.0.8", "description": "A blog API built in NodeJS that can be used as a standalone application, or included in a NodeJS application", "main": "index.js", "author": "Luciano Nooijen (https://bytecode.nl)", @@ -31,9 +31,9 @@ "lint": "yarn run lint:js && yarn run lint:ts", "lint:js": "eslint --ext .js,.jsx .", "lint:ts": "tslint --project .", - "test": "NODE_ENV=test jest -i", + "test": "NODE_ENV=test jest -i --forceExit", "test:watch": "NODE_ENV=test jest -i --watch", - "coverage": "NODE_ENV=test jest -i --coverage", + "coverage": "NODE_ENV=test jest -i --coverage --forceExit", "migrate": "knex migrate:latest", "reinstall": "rm -rf node_modules && yarn", "clean": "rm -rf dist build coverage" diff --git a/src/create-node-blog.js b/src/create-node-blog.js new file mode 100644 index 0000000..7eb4b13 --- /dev/null +++ b/src/create-node-blog.js @@ -0,0 +1,68 @@ +/* eslint-disable object-curly-newline */ + +const getKnexInstance = require('knex'); +const generateKnexfile = require('../database/generate-knexfile'); +const { authHelper } = require('../helpers'); + +const { + Authors, + Auth, + Users, + Categories, + Articles, +} = require('../controllers'); + +const authors = { + list: Authors.listAuthors, + get: Authors.getAuthor, + add: Authors.addAuthor, + modify: Authors.modifyAuthor, + delete: Authors.deleteAuthor, +}; + +const auth = { + generateToken: Auth.generateToken, + authenticate: Auth.authenticateUser, + decode: authHelper.decodeJWT, + validate: Auth.validateToken, +}; + +const users = { + list: Users.listUsers, + get: Users.getUser, + add: Users.addUser, + modify: Users.modifyUser, + delete: Users.deleteUser, +}; + +const categories = { + list: Categories.listCategories, + get: Categories.getCategory, + add: Categories.addCategory, + modify: Categories.modifyCategory, + delete: Categories.deleteCategory, +}; + +const articles = { + list: Articles.listArticles, + get: Articles.getArticle, + add: Articles.addArticle, + // Modify is not yet available + delete: Articles.deleteArticle, +}; + +// eslint-disable-next-line max-len, prettier/prettier +const createNodeBlogInstance = (client, host, database, user, password, debug) => { + const knexConfig = { client, host, database, user, password, debug }; + const knexfile = generateKnexfile(knexConfig); + const knex = getKnexInstance(knexfile); + knex.migrate.latest(); + return knex; +}; + +module.exports = createNodeBlogInstance; +module.exports.authors = authors; +module.exports.auth = auth; +module.exports.users = users; +module.exports.categories = categories; +module.exports.articles = articles; diff --git a/src/nodeblog-class.js b/src/nodeblog-class.js deleted file mode 100644 index 1222b85..0000000 --- a/src/nodeblog-class.js +++ /dev/null @@ -1,20 +0,0 @@ -// TODO: Make this work and stuff - -const getKnexInstance = require('knex'); -const { generateKnexfile } = require('../database/generate-knexfile'); - -class NodeBlog { - constructor(client, host, database, user, password, debug) { - this.client = client; - this.host = host; - this.database = database; - this.user = user; - this.password = password; - this.debug = debug; - this.knexConfig = { client, host, database, user, password, debug }; // eslint-disable-line - this.knex = getKnexInstance(generateKnexfile); - this.knex.migrate.latest(); // TODO: Clean up - } -} - -module.exports = NodeBlog; diff --git a/src/nodejs-blog.js b/src/nodejs-blog.js index b8e9fef..abe5a8f 100644 --- a/src/nodejs-blog.js +++ b/src/nodejs-blog.js @@ -1,3 +1,3 @@ -const getNodeBlogInstance = require('./nodeblog-class'); +const createNodeBlog = require('./create-node-blog'); -module.exports = getNodeBlogInstance; +module.exports = createNodeBlog; diff --git a/swagger.yml b/swagger.yml index d69a306..70e498e 100644 --- a/swagger.yml +++ b/swagger.yml @@ -564,8 +564,10 @@ components: type: 'string' image_url: type: 'string' - markdown_content: + html_content: type: 'string' + reading_time: + type: 'integer' related_articles: type: 'array' items: diff --git a/tests/config/blog.ts b/tests/config/blog.ts new file mode 100644 index 0000000..0011b08 --- /dev/null +++ b/tests/config/blog.ts @@ -0,0 +1,16 @@ +const knexGenerator = require('knex'); +const generateKnexfile = require('../../database/generate-knexfile'); + +const knexConfig = { + client: process.env.DB_CLIENT_TEST, + host: process.env.DB_HOST_TEST, + user: process.env.DB_USER_TEST, + database: process.env.DB_NAME_TEST, + password: process.env.DB_PASS_TEST, + debug: process.env.KNEX_DEBUG === 'true', +}; + +const knexfile = generateKnexfile(knexConfig); +const blog = knexGenerator(knexfile); + +export default blog; diff --git a/tests/controllers/articles.test.ts b/tests/controllers/articles.test.ts new file mode 100644 index 0000000..e513186 --- /dev/null +++ b/tests/controllers/articles.test.ts @@ -0,0 +1,248 @@ +import { useTestDatabase } from '../config/index'; +import blog from '../config/blog'; + +const { + listArticles, + getRelatedArticles, + addRelatedArticlesToArticleObject, + calculateReadingTime, + addReadingTimeToArticles, + getArticle, + generateRelatedArticles, + addToRelatedArticlesTable, + addArticle, + modifyArticle, + deleteArticle, +} = require('../../controllers/articles'); + +useTestDatabase(); + +const newArticle = { + title: 'newtitle', + subtitle: 'newsubtitle', + slug: 'new-title', + image_url: 'http://placekitten.com/400/400', + summary: 'the summary', + html_content: 'the content', + author: 2, + category: 1, + related_articles: [1, 2], +}; + +describe('Articles Controller', () => { + test('listArticles should return articles array', async () => { + expect.assertions(1); + const articles = await listArticles(blog); + expect(articles.length).toBeGreaterThan(0); + }); + + test('listArticles articles should include reading time', async () => { + expect.assertions(1); + const articles = await listArticles(blog); + expect(typeof articles[0].reading_time).toBe('number'); + }); + + test('addReadingTimeToArticles should add reading time', async () => { + expect.assertions(1); + let testString = ''; + for (let i = 0; i < 2750; i += 1) { + testString += 'word '; + } + const testInput = [{ html_content: testString }]; + const expectedOutput = [{ html_content: testString, reading_time: 10 }]; + const receivedArticles = addReadingTimeToArticles(testInput); + expect(receivedArticles).toEqual(expectedOutput); + }); + + test('listArticles should not list hidden articles', async () => { + expect.assertions(2); + const hidden = { hidden: true }; + const newHiddenArticle = { ...newArticle, ...hidden }; + const createdArticle = await addArticle(blog, newHiddenArticle); + const articles = await listArticles(blog); + const articlesWithNewArticleId = await articles.filter(article => { + return article.id === createdArticle.id; + }); + expect(await getArticle(blog, createdArticle.id)).toBeDefined(); + expect(articlesWithNewArticleId.length).toBe(0); + }); + + test('listArticles should not list articles from the future', async () => { + expect.assertions(2); + const date = new Date(); + const futureDays = date.setDate(date.getDate() + 100); + const future = { posted_on: new Date(futureDays) }; + const futureArticle = { ...newArticle, ...future }; + const createdArticle = await addArticle(blog, futureArticle); + const articles = await listArticles(blog); + const articlesWithNewArticleId = await articles.filter(article => { + return article.id === createdArticle.id; + }); + expect(await getArticle(blog, createdArticle.id)).toBeDefined(); + expect(articlesWithNewArticleId.length).toBe(0); + }); + + test('getArticle should return an article with content', async () => { + expect.assertions(14); + const article = await getArticle(blog, 1); + expect(typeof article.id).toBe('number'); + expect(typeof article.title).toBe('string'); + expect(typeof article.subtitle).toBe('string'); + expect(typeof article.slug).toBe('string'); + expect(article.posted_on).toBeInstanceOf(Date); + expect(typeof article.article_image_url).toBe('string'); + expect(typeof article.summary).toBe('string'); + expect(typeof article.html_content).toBe('string'); + expect(typeof article.author_name).toBe('string'); + expect(typeof article.author_role).toBe('string'); + expect(typeof article.author_image_url).toBe('string'); + expect(typeof article.category_name).toBe('string'); + expect(typeof article.category_slug).toBe('string'); + expect(typeof article.reading_time).toBe('number'); + }); + + test('calculateReadingTime should calculate reading time', async () => { + const wordsPerMinute = 275; + let testString = ''; + for (let i = 0; i < 2750; i += 1) { + testString += 'word '; + } + const calculatedReadingTime = calculateReadingTime(testString); + expect(calculatedReadingTime).toBe(10); + }); + + test('calculateReadingTime should round down', async () => { + const wordsPerMinute = 275; + let testString = ''; + for (let i = 0; i < 2760; i += 1) { + testString += 'word '; + } + const calculatedReadingTime = calculateReadingTime(testString); + expect(calculatedReadingTime).toBe(10); + }); + + test('calculateReadingTime should round down', async () => { + const wordsPerMinute = 275; + let testString = ''; + for (let i = 0; i < 2740; i += 1) { + testString += 'word '; + } + const calculatedReadingTime = calculateReadingTime(testString); + expect(calculatedReadingTime).toBe(9); + }); + + test('getRelatedArticles should return an article array', async () => { + expect.assertions(14); + const relatedArticles = await getRelatedArticles(blog, 1); + const article = relatedArticles[0]; + expect(typeof article.id).toBe('number'); + expect(typeof article.title).toBe('string'); + expect(typeof article.subtitle).toBe('string'); + expect(typeof article.slug).toBe('string'); + expect(article.posted_on).toBeInstanceOf(Date); + expect(typeof article.article_image_url).toBe('string'); + expect(typeof article.summary).toBe('string'); + expect(typeof article.author_name).toBe('string'); + expect(typeof article.author_role).toBe('string'); + expect(typeof article.author_image_url).toBe('string'); + expect(typeof article.html_content).toBe('string'); + expect(typeof article.category_name).toBe('string'); + expect(typeof article.category_slug).toBe('string'); + expect(typeof article.reading_time).toBe('number'); + }); + + test('generateRelatedArticles should create valid objects', () => { + const testId = 1; + const testRelatedIds = [2, 3, 4]; + const expectedResponse = [ + { article_id: 1, related_article_id: 2 }, + { article_id: 1, related_article_id: 3 }, + { article_id: 1, related_article_id: 4 }, + ]; + const response = generateRelatedArticles(testId, testRelatedIds); + expect(response).toEqual(expectedResponse); + }); + + test('addToRelatedArticlesTable cant add empty array', async () => { + expect.assertions(1); + const emptyArray = []; + const response = await addToRelatedArticlesTable(blog, 1, emptyArray); + expect(response).toEqual(emptyArray); + }); + + test('addToRelatedArticlesTable works with undefined', async () => { + expect.assertions(1); + const emptyArray = []; + const response = await addToRelatedArticlesTable(blog, 1); + expect(response).toEqual(emptyArray); + }); + + test('addArticle should add an article correctly', async () => { + expect.assertions(15); + const addedArticle = await addArticle(blog, newArticle); + expect(typeof addedArticle).toBe('object'); + expect(typeof addedArticle.id).toBe('number'); + expect(typeof addedArticle.title).toBe('string'); + expect(typeof addedArticle.subtitle).toBe('string'); + expect(typeof addedArticle.slug).toBe('string'); + expect(addedArticle.posted_on).toBeInstanceOf(Date); + expect(typeof addedArticle.article_image_url).toBe('string'); + expect(typeof addedArticle.summary).toBe('string'); + expect(typeof addedArticle.author_name).toBe('string'); + expect(typeof addedArticle.author_role).toBe('string'); + expect(typeof addedArticle.author_image_url).toBe('string'); + expect(typeof addedArticle.html_content).toBe('string'); + expect(typeof addedArticle.category_name).toBe('string'); + expect(typeof addedArticle.category_slug).toBe('string'); + expect(typeof addedArticle.reading_time).toBe('number'); + }); + + // test('addArticle should work with custom posted_on', async () => { TODO: + // }); + + // TODO: Fix + xtest('modifyArticle should modify an article correctly', async () => { + expect.assertions(9); + await modifyArticle(blog, 1, newArticle); + const modifiedArticle = await getArticle(1); + expect(modifiedArticle.title).toBe(newArticle.title); + expect(modifiedArticle.subtitle).toBe(newArticle.subtitle); + expect(modifiedArticle.slug).toBe(newArticle.slug); + expect(modifiedArticle.image_url).toBe(newArticle.image_url); + expect(modifiedArticle.summary).toBe(newArticle.summary); + expect(modifiedArticle.html_content).toBe(newArticle.html_content); + expect(modifiedArticle.author).toBe(newArticle.author); + expect(modifiedArticle.category).toBe(newArticle.category); + expect(modifiedArticle.related_articles) + .toBe(newArticle.related_articles); + }); + + // TODO: Fix + xtest('modifyArticle should work with when partly updating', async () => { + expect.assertions(9); + const originalArticle = await getArticle(blog, 1); + const newArticlePart = { + title: 'updated title', + }; + await modifyArticle(blog, 1, newArticlePart); + const modifiedArticle = await getArticle(newArticlePart); + expect(modifiedArticle.title).toBe(newArticlePart.title); + expect(modifiedArticle.subtitle).toBe(originalArticle.subtitle); + expect(modifiedArticle.slug).toBe(originalArticle.slug); + expect(modifiedArticle.image_url).toBe(originalArticle.image_url); + expect(modifiedArticle.summary).toBe(originalArticle.summary); + expect(modifiedArticle.html_content).toBe(originalArticle.html_content); + expect(modifiedArticle.author).toBe(originalArticle.author); + expect(modifiedArticle.category).toBe(originalArticle.category); + expect(modifiedArticle.related_articles) + .toBe(originalArticle.related_articles); + }); + + test('deleteArticle should delete an article', async () => { + expect.assertions(2); + return deleteArticle(blog, 1) + .then(data => expect(data.id).toBe(1)) + .then(async () => + expect(await getArticle(blog, 1)).toBeUndefined()); + }); +}); diff --git a/tests/controllers/auth.test.ts b/tests/controllers/auth.test.ts index 5d5cd30..45f8dd1 100644 --- a/tests/controllers/auth.test.ts +++ b/tests/controllers/auth.test.ts @@ -1,4 +1,5 @@ import { useTestDatabase } from '../config/index'; +import blog from '../config/blog'; const { getUserByUsername, @@ -32,15 +33,15 @@ const testUser2 = { useTestDatabase(); -beforeEach(async () => await addUser(testUser)); +beforeEach(async () => await addUser(blog, testUser)); describe('Auth Controller', () => { test('getUserByUsername should return user if it exists', async () => { expect.assertions(4); - const addedUser = await addUser(testUser2); + const addedUser = await addUser(blog, testUser2); const expectedId = addedUser.id; const { username, email, password } = testUser2; - const fetchedUser = await getUserByUsername(username); + const fetchedUser = await getUserByUsername(blog, username); expect(fetchedUser.id).toBe(expectedId); expect(fetchedUser.username).toBe(username); expect(fetchedUser.email).toBe(email); @@ -51,15 +52,16 @@ describe('Auth Controller', () => { expect.assertions(2); const { username, password } = testUser; const incorrectPassword = 'incorrect_password'; - expect(await authenticateUser(username, password)).toBe(true); - expect(await authenticateUser(username, incorrectPassword)).toBe(false); + expect(await authenticateUser(blog, username, password)).toBe(true); + expect(await + authenticateUser(blog, username, incorrectPassword)).toBe(false); }); test('generateToken should throw error on invalid login', async () => { expect.assertions(1); const { username } = testUser; const incorrectPassword = 'incorrect_password'; - await expect(generateToken(username, incorrectPassword)) + await expect(generateToken(blog, username, incorrectPassword)) .rejects .toThrowError('Incorrect credentials'); }); @@ -67,7 +69,7 @@ describe('Auth Controller', () => { test('generateTokenPayload should create a user object', async () => { expect.assertions(4); const { username } = testUser; - const tokenPayload = await generateTokenPayload(username); + const tokenPayload = await generateTokenPayload(blog, username); expect(typeof tokenPayload).toBe('object'); expect(typeof tokenPayload.id).toBe('number'); expect(typeof tokenPayload.username).toBe('string'); @@ -82,43 +84,43 @@ describe('Auth Controller', () => { email: 'in@valid.com', }; const invalidToken = authHelper.generateJWT(invalidPayloadData); - const invalidTokenIsValid = await validateToken(invalidToken); + const invalidTokenIsValid = await validateToken(blog, invalidToken); expect(invalidTokenIsValid).toBe(false); }); test('validateToken should fail if token is invalid format', async () => { expect.assertions(1); const invalidToken = 'thisisaninvalidtoken'; - const invalidTokenIsValid = await validateToken(invalidToken); + const invalidTokenIsValid = await validateToken(blog, invalidToken); expect(invalidTokenIsValid).toBe(false); }); test('validateToken should fail if token has expired', async () => { expect.assertions(1); - const addedUser = await addUser(testUser2); + const addedUser = await addUser(blog, testUser2); const { username } = addedUser; - const tokenPayloadData = generateTokenPayload(username); + const tokenPayloadData = generateTokenPayload(blog, username); const date = new Date(); const issuedAt = date.setDate(date.getDate() - jwtExpiresInDays - 1); const expiredJWT = authHelper.generateJWT(tokenPayloadData, issuedAt); - const invalidTokenIsValid = await validateToken(expiredJWT); + const invalidTokenIsValid = await validateToken(blog, expiredJWT); expect(invalidTokenIsValid).toBe(false); }); test('validateToken should pass if token is valid', async () => { expect.assertions(1); const { username } = testUser; - const tokenPayloadData = await generateTokenPayload(username); + const tokenPayloadData = await generateTokenPayload(blog, username); const validToken = authHelper.generateJWT(tokenPayloadData); - const validTokenIsValid = await validateToken(validToken); + const validTokenIsValid = await validateToken(blog, validToken); expect(validTokenIsValid).toBe(true); }); test('generateToken should give valid tokens', async () => { expect.assertions(1); const { username, password } = testUser; - const validToken = await generateToken(username, password); - const validTokenIsValid = await validateToken(validToken); + const validToken = await generateToken(blog, username, password); + const validTokenIsValid = await validateToken(blog, validToken); expect(validTokenIsValid).toBe(true); }); }); diff --git a/tests/controllers/authors.test.ts b/tests/controllers/authors.test.ts index f39aaec..cca6a47 100644 --- a/tests/controllers/authors.test.ts +++ b/tests/controllers/authors.test.ts @@ -1,4 +1,5 @@ import { useTestDatabase } from '../config/index'; +import blog from '../config/blog'; const { listAuthors, @@ -19,13 +20,13 @@ const newAuthor = { describe('Test if Authors CRUD operations are working correctly', () => { test('Listing all Authors should return rows', async () => { expect.assertions(1); - const authors = await listAuthors(); + const authors = await listAuthors(blog); expect(authors.length).toBeGreaterThan(0); }); test('Fetching a single Author should return an Author', async () => { expect.assertions(4); - const author = await getAuthor(1); + const author = await getAuthor(blog, 1); expect(author.id).toBe(1); expect(typeof author.name).toBe('string'); expect(typeof author.image_url).toBe('string'); @@ -34,10 +35,10 @@ describe('Test if Authors CRUD operations are working correctly', () => { test('Adding a new Author should add a single row', async () => { expect.assertions(1); - const authorsBefore = await listAuthors(); + const authorsBefore = await listAuthors(blog); const authorLengthBefore = authorsBefore.length; - return addAuthor(newAuthor).then(async () => { - const authorsAfter = await listAuthors(); + return addAuthor(blog, newAuthor).then(async () => { + const authorsAfter = await listAuthors(blog); const authorLengthAfter = authorsAfter.length; expect(authorLengthAfter).toBe(authorLengthBefore + 1); }); @@ -45,7 +46,7 @@ describe('Test if Authors CRUD operations are working correctly', () => { test('Adding a new Author should return the new Author', async () => { expect.assertions(5); - const addedAuthor = await addAuthor(newAuthor); + const addedAuthor = await addAuthor(blog, newAuthor); expect(addedAuthor.id).toBeDefined(); expect(typeof addedAuthor.id).toBe('number'); expect(addedAuthor.name).toBe(newAuthor.name); @@ -55,12 +56,12 @@ describe('Test if Authors CRUD operations are working correctly', () => { test('Updating an Author should return the modified data', async () => { expect.assertions(9); - const originalAuthor = await getAuthor(1); + const originalAuthor = await getAuthor(blog, 1); expect(originalAuthor.id).toBe(1); expect(originalAuthor.name).not.toBe(newAuthor.name); expect(originalAuthor.image_url).not.toBe(newAuthor.image_url); expect(originalAuthor.role).not.toBe(newAuthor.role); - const modifiedAuthor = await modifyAuthor(1, newAuthor); + const modifiedAuthor = await modifyAuthor(blog, 1, newAuthor); expect(modifiedAuthor.id).toBeDefined(); expect(typeof modifiedAuthor.id).toBe('number'); expect(modifiedAuthor.name).toBe(newAuthor.name); @@ -70,8 +71,9 @@ describe('Test if Authors CRUD operations are working correctly', () => { test('Deleting an Author should return the deleted Author ID', async () => { expect.assertions(2); - return deleteAuthor(1) - .then(data => expect(data.id).toBe(1)) - .then(async () => expect(await getAuthor(1)).toBeUndefined()); + const deletedResponse = await deleteAuthor(blog, 1); + const deletedAuthor = await getAuthor(blog, 1); + expect(deletedResponse.id).toBe(1); + expect(deletedAuthor).toBeUndefined(); }); }); diff --git a/tests/controllers/categories.test.ts b/tests/controllers/categories.test.ts new file mode 100644 index 0000000..163ee21 --- /dev/null +++ b/tests/controllers/categories.test.ts @@ -0,0 +1,72 @@ +import { useTestDatabase } from '../config/index'; +import blog from '../config/blog'; + +const { + listCategories, + getCategory, + addCategory, + modifyCategory, + deleteCategory, +} = require('../../controllers/categories'); + +useTestDatabase(); + +const newCategory = { + name: 'The new category name', + slug: 'the-new-slug', +}; + +describe('Test if Categories CRUD operations are working correctly', () => { + test('Listing all Categories should return rows', async () => { + expect.assertions(1); + const categories = await listCategories(blog); + expect(categories.length).toBeGreaterThan(0); + }); + + test('Fetching a single Category should return an Categorie', async () => { + expect.assertions(3); + const category = await getCategory(blog, 1); + expect(category.id).toBe(1); + expect(typeof category.name).toBe('string'); + expect(typeof category.slug).toBe('string'); + }); + + test('Adding a new Category should add a single row', async () => { + expect.assertions(1); + const categoriesBefore = await listCategories(blog); + const categorieLengthBefore = categoriesBefore.length; + await addCategory(blog, newCategory); + const categoriesAfter = await listCategories(blog); + const categorieLengthAfter = categoriesAfter.length; + expect(categorieLengthAfter).toBe(categorieLengthBefore + 1); + }); + + test('Adding a new Category should return the new Category', async () => { + expect.assertions(3); + const addedCategory = await addCategory(blog, newCategory); + expect(typeof addedCategory.id).toBe('number'); + expect(addedCategory.name).toBe(newCategory.name); + expect(addedCategory.slug).toBe(newCategory.slug); + }); + + test('Updating an Category should return the modified data', async () => { + expect.assertions(7); + const originalCategory = await getCategory(blog, 1); + expect(originalCategory.id).toBe(1); + expect(originalCategory.name).not.toBe(newCategory.name); + expect(originalCategory.slug).not.toBe(newCategory.slug); + const modifiedCategory = await modifyCategory(blog, 1, newCategory); + expect(modifiedCategory.id).toBeDefined(); + expect(typeof modifiedCategory.id).toBe('number'); + expect(modifiedCategory.name).toBe(newCategory.name); + expect(modifiedCategory.slug).toBe(newCategory.slug); + }); + + test('Deleting an Category should return undefined', async () => { + expect.assertions(2); + return deleteCategory(blog, 2) + .then(data => expect(data.id).toBe(2)) + .then(async () => + expect(await getCategory(blog, 2)).toBeUndefined()); + }); +}); diff --git a/tests/controllers/users.test.ts b/tests/controllers/users.test.ts index 9ee1759..4285409 100644 --- a/tests/controllers/users.test.ts +++ b/tests/controllers/users.test.ts @@ -1,4 +1,5 @@ import { useTestDatabase } from '../config/index'; +import blog from '../config/blog'; const { authHelper } = require('../../helpers'); @@ -24,13 +25,13 @@ const newUser = { describe('Test if Users CRUD operations are working correctly', () => { test('Listing all Users should return rows', async () => { expect.assertions(1); - const users = await listUsers(); + const users = await listUsers(blog); expect(users.length).toBeGreaterThan(0); }); test('Fetching a single User should return an User', async () => { expect.assertions(7); - const user = await getUser(1); + const user = await getUser(blog, 1); expect(user.id).toBe(1); expect(typeof user.username).toBe('string'); expect(typeof user.email).toBe('string'); @@ -42,10 +43,10 @@ describe('Test if Users CRUD operations are working correctly', () => { test('Adding a new User should add a single row', async () => { expect.assertions(1); - const usersBefore = await listUsers(); + const usersBefore = await listUsers(blog); const userLengthBefore = usersBefore.length; - return addUser(newUser).then(async () => { - const usersAfter = await listUsers(); + return addUser(blog, newUser).then(async () => { + const usersAfter = await listUsers(blog); const userLengthAfter = usersAfter.length; expect(userLengthAfter).toBe(userLengthBefore + 1); }); @@ -53,7 +54,7 @@ describe('Test if Users CRUD operations are working correctly', () => { test('Adding a new User should return the new User', async () => { expect.assertions(6); - const addedUser = await addUser(newUser); + const addedUser = await addUser(blog, newUser); expect(typeof addedUser.id).toBe('number'); expect(addedUser.username).toBe(newUser.username); expect(addedUser.email).toBe(newUser.email); @@ -66,16 +67,17 @@ describe('Test if Users CRUD operations are working correctly', () => { const { checkPasswordHash } = authHelper; const unhashedPassword = newUser.password; expect.assertions(2); - const addedUser = await addUser(newUser); + const addedUser = await addUser(blog, newUser); const hashed = addedUser.password; - const isValidHash = await checkPasswordHash(unhashedPassword, hashed); + const isValidHash = + await checkPasswordHash(unhashedPassword, hashed); expect(hashed).not.toBe(newUser.password); expect(isValidHash).toBe(true); }); test('Updating an User should return the modified data', async () => { expect.assertions(15); - const originalUser = await getUser(1); + const originalUser = await getUser(blog, 1); expect(originalUser.id).toBe(1); expect(originalUser.username).not.toBe(newUser.username); expect(originalUser.email).not.toBe(newUser.email); @@ -83,7 +85,7 @@ describe('Test if Users CRUD operations are working correctly', () => { expect(originalUser.last_name).not.toBe(newUser.last_name); expect(originalUser.password).not.toBe(newUser.password); expect(originalUser.author_id).not.toBe(newUser.author_id); - const modifiedUser = await modifyUser(1, newUser); + const modifiedUser = await modifyUser(blog, 1, newUser); expect(modifiedUser.id).toBeDefined(); expect(typeof modifiedUser.id).toBe('number'); expect(modifiedUser.username).toBe(newUser.username); @@ -96,8 +98,8 @@ describe('Test if Users CRUD operations are working correctly', () => { test('Deleting an User should return undefined', async () => { expect.assertions(2); - return deleteUser(2) + return deleteUser(blog, 2) .then(data => expect(data.id).toBe(2)) - .then(async () => expect(await getUser(2)).toBeUndefined()); + .then(async () => expect(await getUser(blog, 2)).toBeUndefined()); }); }); diff --git a/tests/helpers/auth-helper.test.ts b/tests/helpers/auth-helper.test.ts index daf876b..3606a88 100644 --- a/tests/helpers/auth-helper.test.ts +++ b/tests/helpers/auth-helper.test.ts @@ -46,10 +46,8 @@ describe('Authentication helper', () => { }); test('decodeJWT should return the correct payload', () => { - const payload = authHelper.generatePayload(payloadData); const authToken = authHelper.generateJWT(payloadData); const decodedPayload = authHelper.decodeJWT(authToken); - expect(decodedPayload).toEqual(payload); expect(decodedPayload.data).toEqual(payloadData); }); @@ -93,8 +91,8 @@ describe('Authentication helper', () => { }); test('validateJWT should throw error if JWT is expired', () => { - const date = new Date(baseTime); - const issuedAt = date.setDate(date.getDate() - jwtExpiresInDays - 1); + const date = new Date(); + const issuedAt = date.setDate(date.getDate() - jwtExpiresInDays - 9999); const expiredJWT = authHelper.generateJWT(payloadData, issuedAt); expect(() => authHelper.validateJWT(expiredJWT)) .toThrowError('Token expired'); diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..b4d6052 --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,65 @@ +import { useTestDatabase } from './config/index'; +import blog from './config/blog'; + +const nodeBlog = require('../'); +const { authors, auth, users, categories, articles } = require('../'); + +const client = process.env.DB_CLIENT_TEST; +const host = process.env.DB_HOST_TEST; +const user = process.env.DB_USER_TEST; +const database = process.env.DB_NAME_TEST; +const password = process.env.DB_PASS_TEST; +const debug = process.env.KNEX_DEBUG === 'true'; + +const generatedBlog = nodeBlog(client, host, user, database, password, debug); + +useTestDatabase(); + +describe('NodeBlog NPM module', () => { + test('NodeBlog to create a knex instance', () => { + expect(typeof generatedBlog).toBe('function'); + }); + test('Blog authors should work', async () => { + expect.assertions(2); + const list = await authors.list(blog); + const getItem = await authors.get(blog, 1); + expect(typeof list).toBe('object'); + expect(typeof getItem).toBe('object'); + }); + test('Blog users should work', async () => { + expect.assertions(2); + const list = await users.list(blog); + const getItem = await users.get(blog, 1); + expect(typeof list).toBe('object'); + expect(typeof getItem).toBe('object'); + }); + test('Blog categories should work', async () => { + expect.assertions(2); + const list = await categories.list(blog); + const getItem = await categories.get(blog, 1); + expect(typeof list).toBe('object'); + expect(typeof getItem).toBe('object'); + }); + test('Blog articles should work', async () => { + expect.assertions(2); + const list = await articles.list(blog); + const getItem = await articles.get(blog, 1); + expect(typeof list).toBe('object'); + expect(typeof getItem).toBe('object'); + }); + test('Add user should be working', async () => { + expect.assertions(1); + const newUser = { + username: 'anewuser', + email: 'anewuser@domain.com', + first_name: 'Jane', + last_name: 'Doe', + password: 'theplainpasswordgoeshere', + author_id: 2, + }; + await users.add(blog, newUser); + const isAuth = + await auth.authenticate(blog, newUser.username, newUser.password); + expect(isAuth).toBe(true); + }); +});