Skip to content

Commit

Permalink
Merge b45ad99 into 637781d
Browse files Browse the repository at this point in the history
  • Loading branch information
micah-akpan committed Apr 16, 2019
2 parents 637781d + b45ad99 commit 2b0e4a5
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 107 deletions.
214 changes: 137 additions & 77 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"passport-twitter": "^1.0.4",
"pg": "^7.9.0",
"pg-hstore": "^2.3.2",
"reading-time-estimator": "^1.0.3",
"sequelize": "^5.2.0",
"sequelize-cli": "^5.4.0",
"slugify": "^1.3.4",
Expand Down
1 change: 1 addition & 0 deletions src/controllers/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { slug } from '../utils/article';
import { Article } from '../models';
import { responseHandler } from '../utils';


/**
* @name addArticle
* @description This is the method for inserting articles
Expand Down
4 changes: 4 additions & 0 deletions src/migrations/20190331141021-create-article.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export default {
type: Sequelize.INTEGER,
defaultValue: 0,
},
totalReadTime: {
type: Sequelize.INTEGER,
defaultValue: 1,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
Expand Down
20 changes: 20 additions & 0 deletions src/models/article.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { computeArticleReadingTime } from '../utils/article';

/**
* @name init
* @param {sequelize} sequelize
Expand Down Expand Up @@ -59,9 +61,27 @@ export default (sequelize, DataTypes) => {
type: DataTypes.INTEGER,
defaultValue: 0,
},
totalReadTime: {
type: DataTypes.INTEGER,
defaultValue: 1,
},
},
{
tableName: 'articles',
hooks: {
beforeCreate(article) {
const totalReadTime = computeArticleReadingTime(article.get('body'));
article.set('totalReadTime', totalReadTime);
},
beforeUpdate(article) {
const totalReadTime = computeArticleReadingTime(article.get('body'));
article.set('totalReadTime', totalReadTime);
},
beforeSave(article) {
const totalReadTime = computeArticleReadingTime(article.get('body'));
article.set('totalReadTime', totalReadTime);
}
}
}
);
Article.associate = (models) => {
Expand Down
13 changes: 13 additions & 0 deletions src/utils/article.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Validator from 'validatorjs';
import slugify from 'slugify';
import { readingTime } from 'reading-time-estimator';

/**
* @description This is the method for validating articles before inserting
Expand Down Expand Up @@ -34,3 +35,15 @@ export const validateArticle = async (payload) => {
* @returns {string} Returns string
*/
export const slug = payload => `${slugify(payload, '-')}-${new Date().getTime()}`;

/**
* @func computeArticleReadingTime
* @param {string} words the words for estimating read time
* @param {*} opts an hash of wordsPerMinute and locale options
* @returns {number} Returns the total time (rounded to the nearest greater whole number)
* it takes to read the article
*/
export const computeArticleReadingTime = (words, { wordsPerMinute = 250, locale = 'en' } = {}) => {
const totalTime = readingTime(words, { wordsPerMinute, locale });
return Math.ceil(totalTime.minutes);
};
15 changes: 15 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,18 @@ export const checkIDParamType = (param) => {
if (!isUUID(param, '4')) { return false; }
return true;
};
/**
* @function generateDummyWords
* @param {string} word
* @param {number} number
* @returns {string} Returns `word` duplicated `number` times
*/
export const generateDummyWords = (word, number = 10) => {
let newParagraph = '';
let count = 1;
while (count < number) {
newParagraph += ` ${word}`;
count += 1;
}
return newParagraph;
};
1 change: 1 addition & 0 deletions tests/integration/article.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('POST /api/v1/articles', () => {
expect(body.data).to.have.property('slug');
expect(body.data.title).to.be.equal(ARTICLE.title);
expect(body.data.body).to.be.equal(ARTICLE.body);
expect(body.data.totalReadTime).to.equal(2);
done();
});
});
Expand Down
4 changes: 3 additions & 1 deletion tests/mock/article.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { generateDummyWords } from '../../src/utils';

export const ARTICLE = {
title: 'Andela is cool 7888',
userId: '979eaa2e-5b8f-4103-8192-4639afae2ba8',
description: 'Lorem ipsum dolor sit amet, sit ut dolor alterum, sed malis referrentur cu. Aperiam fabulas eos ea. Sea mazim senserit tincidunt te.',
body: 'Lorem ipsum dolor sit amet, sit ut dolor alterum, sed malis referrentur cu. Aperiam fabulas eos ea. Sea mazim senserit tincidunt te. Mei volutpat delicatissimi ut, id mollis alienum argumentum has, semper efficiendi sed ea. Ius decore consul forensibus ne, enim verear corpora sit ut. Usu eu possit equidem menandri, quo et noster officiis iracundia.',
body: generateDummyWords('lorem', 500),
imageUrl: 'https://picsum.photos/200/300',
tags: ['hello', 'async', 'await']
};
Expand Down
92 changes: 64 additions & 28 deletions tests/unit/article.test.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,73 @@
import 'chai/register-should';
import { expect } from 'chai';
import { validateArticle } from '../../src/utils/article';
import { ARTICLE } from '../mock/article';
import { generateDummyWords } from '../../src/utils';
import { validateArticle, computeArticleReadingTime } from '../../src/utils/article';
import {ARTICLE} from '../mock/article';

describe('validateArticle()', () => {
it('should return true if the validation passes', async () => {
const validate = await validateArticle(ARTICLE);
expect(validate.passes()).to.be.equal(true);
});

it('should return false if the validation fails due to missing field', async () => {
delete ARTICLE.title;
const validate = await validateArticle(ARTICLE);
expect(validate.fails()).to.be.equal(true);
expect(validate.passes()).to.be.equal(false);
describe('Article Utils test', () => {
describe('validateArticle()', () => {
it('should return true if the validation passes', () => {
const validate = validateArticle(ARTICLE);
validate.then((res) => {
expect(res.passes()).to.be.equal(true);
});
});

it('should return false if the validation fails due to missing field', () => {
delete ARTICLE.title;
const validate = validateArticle(ARTICLE);
validate.then((res) => {
expect(res.fails()).to.be.equal(true);
expect(res.passes()).to.be.equal(false);

const error = res.errors.all();
expect(error).should.be.an('object');
expect(error).to.have.property('title');
expect(error.title).to.be.an('array');
});
});

it('should return false if the validation fails due to an invalid field', () => {
ARTICLE.title = 1111111111;
ARTICLE.tags = 1111111111;
const validate = validateArticle(ARTICLE);
validate.then((res) => {
expect(res.fails()).to.be.equal(true);
expect(res.passes()).to.be.equal(false);

const error = validate.errors.all();
expect(error).should.be.an('object');
expect(error).to.have.property('title');
expect(error.title).to.be.an('array');
const error = res.errors.all();
expect(error).should.be.an('object');
expect(error).to.have.property('title');
expect(error.title).to.be.an('array');
expect(error.tags).to.be.an('array');
});
});
});

it('should return false if the validation fails due to an invalid field', async () => {
ARTICLE.title = 1111111111;
ARTICLE.tags = 1111111111;
const validate = await validateArticle(ARTICLE);
expect(validate.fails()).to.be.equal(true);
expect(validate.passes()).to.be.equal(false);

const error = validate.errors.all();
expect(error).should.be.an('object');
expect(error).to.have.property('title');
expect(error.title).to.be.an('array');
expect(error.tags).to.be.an('array');
describe('computeArticleReadingTime()', () => {
describe('handle valid input', () => {
it('should return an estimated reading time', () => {
computeArticleReadingTime('code till you drop!').should.equal(1);
});

it('should return an estimated reading time', () => {
computeArticleReadingTime(generateDummyWords('nodejs', 1000)).should.equal(4);
});

it('should return an estimated reading time', () => {
computeArticleReadingTime(generateDummyWords('code', 500)).should.equal(2);
});
});

describe('handle invalid input', () => {
it('should throw an error if words is not a string', () => {
(() => computeArticleReadingTime(1)).should.throw(TypeError);
});

it('should throw an error if wordsPerMinute is less than 1', () => {
(() => computeArticleReadingTime(generateDummyWords('grit'), { wordsPerMinute: 0 })).should.throw();
});
});
});
});
14 changes: 13 additions & 1 deletion tests/unit/util.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { parseErrorResponse, errorResponseFormat } from '../../src/utils';
import {
parseErrorResponse, errorResponseFormat, generateDummyWords
} from '../../src/utils';

describe('Util test', () => {
describe('parseErrorResponse()', () => {
Expand All @@ -25,4 +27,14 @@ describe('Util test', () => {
expect(response.message).to.equal('server is down at the moment');
});
});

describe('generateDummyWords()', () => {
it('should generate same word multiple times', () => {
generateDummyWords('word', 20).split(' ').length.should.equal(20);
});

it('should generate same word multiple times', () => {
generateDummyWords('word').split(' ').length.should.equal(10);
});
});
});

0 comments on commit 2b0e4a5

Please sign in to comment.