From ad28ea98423cb358232be6f0db089625587018d6 Mon Sep 17 00:00:00 2001 From: jeea2 Date: Wed, 27 Nov 2019 01:14:09 +0000 Subject: [PATCH] Add more functionalities to user.js Added functions for getting, inserting and deleting user data. Expanded user table to accommodate for newly-added data columns(email, location, date registered). Future considerations: Add a table for rating artiles which would be linked to the user table. Add functions to retrieve specifically email addresses from users to enable sending emails to users. --- modules/user.js | 169 +++++++++++++++++++++++++---- unit tests/user.spec.js | 235 ++++++++++++++++++++++++++++++---------- 2 files changed, 320 insertions(+), 84 deletions(-) diff --git a/modules/user.js b/modules/user.js index aff030c..fb55488 100644 --- a/modules/user.js +++ b/modules/user.js @@ -2,32 +2,52 @@ 'use strict' const bcrypt = require('bcrypt-promise') -const fs = require('fs-extra') -const mime = require('mime-types') const sqlite = require('sqlite-async') const saltRounds = 10 -module.exports = class User { +/** + * Class representing the details of the user. + * */ +class User { + /** + * Constructor for User table + * @constructor + * @param {} [dbName=':memory:'] database for the website + */ constructor(dbName = ':memory:') { return (async() => { this.db = await sqlite.open(dbName) - // we need this table to store the user accounts - const sql = 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, pass TEXT);' + const sql = `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user TEXT, + password TEXT, + email TEXT, + location TEXT, + profileImg TEXT, + profileTxt TEXT, + registeredDate TEXT);` await this.db.run(sql) return this })() } - async register(user, pass) { + /** + * Register user with suitable username and password. + * @param {string} user - username + * @param {string} password + * @return {Promise} registration confirmation + */ + async register(user, password) { try { if(user.length === 0) throw new Error('missing username') - if(pass.length === 0) throw new Error('missing password') - let sql = `SELECT COUNT(id) as records FROM users WHERE user="${user}";` + if(password.length === 0) throw new Error('missing password') + let sql = `SELECT COUNT(id) as records FROM users WHERE user='${user}';` const data = await this.db.get(sql) if(data.records !== 0) throw new Error(`username "${user}" already in use`) - pass = await bcrypt.hash(pass, saltRounds) - sql = `INSERT INTO users(user, pass) VALUES("${user}", "${pass}")` + password = await bcrypt.hash(password, saltRounds) + const currentDate = Date.now().toString() + sql = `INSERT INTO users(user, password, registeredDate) VALUES('${user}', '${password}', ${currentDate})` await this.db.run(sql) return true } catch(err) { @@ -35,30 +55,131 @@ module.exports = class User { } } - async uploadPicture(path, mimeType, bodyUser) { + /** + * Login user with existing username and password. + * @param {string} username + * @param {string} password + * @return {Promise} login confirmation + */ + async login(username, password) { + try { + let sql = `SELECT count(id) AS count FROM users WHERE user='${username}';` + const records = await this.db.get(sql) + if(!records.count) throw new Error(`username "${username}" not found`) + sql = `SELECT password FROM users WHERE user = '${username}';` + const record = await this.db.get(sql) + const valid = await bcrypt.compare(password, record.password) + if(valid === false) throw new Error(`invalid password for user "${username}"`) + return true + } catch(err) { + throw err + } + } + + /** + * Check email syntax. + * @param {string} email + * @returns {Promise} confirmation for proper email syntax + */ + async checkEmail(email) { try{ - const extension = mime.extension(mimeType) - const acceptedExtension = ['jpg', 'png', 'webp', 'tif', 'gif', 'svg'] - if(acceptedExtension.includes(extension) === false) throw new Error('file type is not suitable for upload') - await fs.copy(path, `public/avatars/${bodyUser}.${extension}`) + if(email.length === 0) throw new Error('missing email') + if(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) === false) throw new Error('email is invalid') + else return true } catch(err) { throw err } } - async login(username, password) { - try { - let sql = `SELECT count(id) AS count FROM users WHERE user="${username}";` - const records = await this.db.get(sql) - if(!records.count) throw new Error(`username "${username}" not found`) - sql = `SELECT pass FROM users WHERE user = "${username}";` - const record = await this.db.get(sql) - const valid = await bcrypt.compare(password, record.pass) - if(valid === false) throw new Error(`invalid password for account "${username}"`) + /** + * Add user email. + * @param {string} user + * @param {string} email + * @returns {Promise} confirmation for adding user email + */ + async addEmail(user, email) { + try{ + if(user.length === 0) throw new Error('missing username') + await this.checkEmail(email) + let sql = `SELECT COUNT(id) as records FROM users WHERE user='${user}';` + const data = await this.db.get(sql) + if(data.records === 0) throw new Error('username does not exist') + sql = `UPDATE users SET email = ('${email}') WHERE user = '${user}';` + await this.db.run(sql) return true } catch(err) { throw err } } + /** + * Add user location. + * @param {string} user + * @param {string} location + * @returns {Promise} confirmation for adding user location + */ + async addLocation(user, location) { + try{ + if(user.length === 0) throw new Error('missing username') + if(location.length === 0) throw new Error('missing location') + let sql = `SELECT COUNT(id) as records FROM users WHERE user='${user}';` + const data = await this.db.get(sql) + if(data.records === 0) throw new Error('username does not exist') + sql = `UPDATE users SET location = '${location}' WHERE user = '${user}';` + await this.db.run(sql) + return true + } catch(err) { + throw err + } + } + + /** + * Get all data of a selected user + * @param {string} username + * @returns {Promise} + */ + async getUser(username) { + try{ + if(username.length === 0) throw new Error('username is missing') + let sql = `SELECT COUNT(id) as records FROM users WHERE user = '${username}'` + let data = await this.db.get(sql) + if(data.records === 0) throw new Error('username does not exist') + sql = `SELECT * FROM users WHERE user = '${username}'` + data = await this.db.get(sql) + return data + } catch(err) { + throw err + } + } + + /** + * Get all data of all users + * @returns {Promise} array of data objects of all existing user + */ + async getAll() { + const sql = 'SELECT * FROM users;' + const data = await this.db.all(sql) + return data + } + + /** + * Delete a user. + * @param {integer} key + * @returns {Promise} confirmation for deleting user + */ + async delete(key) { + try{ + if(key === undefined) throw new Error('missing id') + let sql = `SELECT COUNT(id) as records FROM users WHERE id=${key};` + const data = await this.db.get(sql) + if(data.records === 0) throw new Error(`user with id ${key} does not exist`) + sql = `DELETE FROM users WHERE id = ${key};` + await this.db.run(sql) + return true + } catch(err) { + throw err + } + } } + +module.exports = User diff --git a/unit tests/user.spec.js b/unit tests/user.spec.js index be4b95e..c6efc7c 100644 --- a/unit tests/user.spec.js +++ b/unit tests/user.spec.js @@ -1,118 +1,233 @@ - 'use strict' -const mock = require('mock-fs') -const fs = require('fs-extra') -const Accounts = require('../modules/user.js') +const User = require('../modules/user.js') describe('register()', () => { - test('register a valid account', async done => { + test('register a valid user', async done => { expect.assertions(1) - const account = await new Accounts() - const register = await account.register('doej', 'password') + const user = await new User() + const register = await user.register('doej', 'password') expect(register).toBe(true) done() }) test('register a duplicate username', async done => { expect.assertions(1) - const account = await new Accounts() - await account.register('doej', 'password') - await expect( account.register('doej', 'password') ) + const user = await new User() + await user.register('doej', 'password') + await expect( user.register('doej', 'password') ) .rejects.toEqual( Error('username "doej" already in use') ) done() }) test('error if blank username', async done => { expect.assertions(1) - const account = await new Accounts() - await expect( account.register('', 'password') ) + const user = await new User() + await expect( user.register('', 'password') ) .rejects.toEqual( Error('missing username') ) done() }) test('error if blank password', async done => { expect.assertions(1) - const account = await new Accounts() - await expect( account.register('doej', '') ) + const user = await new User() + await expect( user.register('doej', '') ) .rejects.toEqual( Error('missing password') ) done() }) }) -describe('uploadPicture()', () => { - beforeEach(() => { - const soundFile = fs.readFileSync('unit\ tests/sample.mp3') - const image = fs.readFileSync('unit\ tests/avatar.png') +describe('login()', () => { - mock({ - 'files/': { - 'fake_mp3_file': soundFile, - 'fake_text_file': '', - 'fake_image_file': image - }, 'public/': { - 'avatars/': {} - } - }) + test('log in with valid credentials', async done => { + expect.assertions(1) + const user = await new User() + await user.register('doej', 'password') + const valid = await user.login('doej', 'password') + expect(valid).toBe(true) + done() }) - afterEach(() => { - mock.restore() + test('invalid username', async done => { + expect.assertions(1) + const user = await new User() + await user.register('doej', 'password') + await expect( user.login('roej', 'password') ) + .rejects.toEqual( Error('username "roej" not found') ) + done() }) - test('image should be uploaded as "public/username.image_extensions"', async done => { + test('invalid password', async done => { expect.assertions(1) - const account = await new Accounts() - const path = 'files/fake_image_file' - const type = 'image/png' - await account.uploadPicture(path, type, 'adrian') - expect(fs.pathExistsSync('public/avatars/adrian.png')).toEqual(true) - mock.restore() + const user = await new User() + await user.register('doej', 'password') + await expect( user.login('doej', 'bad') ) + .rejects.toEqual( Error('invalid password for user "doej"') ) + done() + }) +}) + +describe('checkEmail()', () => { + test('return error if blank email', async done => { + expect.assertions(1) + const user = await new User() + await expect( user.checkEmail('')) + .rejects.toEqual(Error('missing email')) done() }) - test('return error when unsuitable files are uploaded', async done => { + test('return error if invalid email syntax', async done => { expect.assertions(1) - const account = await new Accounts() - const path = 'files/fake_mp3_file' - const type = 'audio/mp3' - await expect( account.uploadPicture(path, type, 'adrian') ) - .rejects.toEqual( Error('file type is not suitable for upload')) - mock.restore() + const user = await new User() + await expect(user.checkEmail('befuncle')) + .rejects.toEqual(Error('email is invalid')) + done() + }) + + test('return true if email is valid', async done => { + expect.assertions(1) + const user = await new User() + const valid = await user.checkEmail('befuncle@befuncle.com') + expect(valid).toEqual(true) done() }) - // this would have to be done by mocking the file system - // perhaps using mock-fs? }) -describe('login()', () => { - test('log in with valid credentials', async done => { +describe('addEmail()', () => { + test('error if blank username', async done => { + expect.assertions(1) + const user = await new User() + await expect( user.addEmail('', 'befuncle@befuncle.com') ) + .rejects.toEqual( Error('missing username') ) + done() + }) + + test('return error if username does not exist', async done => { + expect.assertions(1) + const user = await new User() + await expect( user.addEmail('befuncle', 'befuncle@befuncle.com')) + .rejects.toEqual( Error('username does not exist')) + done() + }) + + test('return true when email is added to database', async done => { expect.assertions(1) - const account = await new Accounts() - await account.register('doej', 'password') - const valid = await account.login('doej', 'password') + const user = await new User() + await user.register('befuncle', 'password') + const valid = await user.addEmail('befuncle', 'befuncle@befuncle.com') expect(valid).toBe(true) done() }) +}) - test('invalid username', async done => { +describe('addLocation()', () => { + test('error if blank username', async done => { expect.assertions(1) - const account = await new Accounts() - await account.register('doej', 'password') - await expect( account.login('roej', 'password') ) - .rejects.toEqual( Error('username "roej" not found') ) + const user = await new User() + await expect( user.addLocation('', 'United Kingdom') ) + .rejects.toEqual( Error('missing username') ) done() }) - test('invalid password', async done => { + test('return error if blank location', async done => { expect.assertions(1) - const account = await new Accounts() - await account.register('doej', 'password') - await expect( account.login('doej', 'bad') ) - .rejects.toEqual( Error('invalid password for account "doej"') ) + const user = await new User() + await user.register('befuncle', 'password') + await expect( user.addLocation('befuncle', '')) + .rejects.toEqual(Error('missing location')) done() }) + test('return error if username does not exist', async done => { + expect.assertions(1) + const user = await new User() + await expect( user.addLocation('befuncle', 'United Kingdom')) + .rejects.toEqual( Error('username does not exist')) + done() + }) + + test('return true if location is added to database', async done => { + expect.assertions(1) + const user = await new User() + await user.register('befuncle', 'password') + const valid = await user.addLocation('befuncle', 'United Kingdom') + expect(valid).toBe(true) + done() + }) +}) + +describe('getUser()', () => { + test('return error when username is blank', async done => { + expect.assertions(1) + const user = await new User() + await expect(user.getUser('')) + .rejects.toEqual(Error('username is missing')) + done() + }) + + test('return error if username does not exist', async done => { + expect.assertions(1) + const user = await new User() + await expect(user.getUser('adrian')) + .rejects.toEqual(Error('username does not exist')) + done() + }) + + test('return true if user data is found', async done => { + expect.assertions(1) + const user = await new User() + await user.register('adrian', '123') + const data = await user.getUser('adrian') + expect(data.user).toEqual('adrian') + done() + }) +}) + +describe('getAll()', () => { + test('return empty array when there is no user', async done => { + expect.assertions(1) + const user = await new User() + const data = await user.getAll() + expect(data).toEqual([]) + done() + }) + + test('return all details of all users', async done => { + expect.assertions(1) + const user = await new User() + await user.register('adrian', '123') + await user.register('ad', '122') + const data = await user.getAll() + expect(data[0].user).toEqual('adrian') + done() + }) +}) + +describe('delete()', () => { + test('return error when key is not defined', async done => { + expect.assertions(1) + const user = await new User() + await expect(user.delete()) + .rejects.toEqual(Error('missing id')) + done() + }) + + test('return error when key is not found', async done => { + expect.assertions(1) + const user = await new User() + await expect(user.delete(1)) + .rejects.toEqual(Error('user with id 1 does not exist')) + done() + }) + + test('return true when all data of selected user is deleted', async done => { + expect.assertions(1) + const user = await new User() + await user.register('befuncle', 'password') + const deleted = await user.delete(1) + expect(deleted).toEqual(true) + done() + }) })