diff --git a/modules/image.js b/modules/image.js new file mode 100644 index 0000000..f67e8c3 --- /dev/null +++ b/modules/image.js @@ -0,0 +1,119 @@ +'use strict' + +const sqlite = require('sqlite-async') + +/** + * Class representing images in the database. + */ +class Image { + + /**Constructs database with the appropriate table for article images. + * @constructor + * @param {string} [dbName=':memory:'] - database for the website + */ + constructor(dbName = ':memory:') { + return (async() => { + this.db = await sqlite.open(dbName) + const sql = `CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + image_path TEXT, + image_description TEXT);` + await this.db.run(sql) + return this + })() + } + + /** + * Adds filename of selected file to the database. + * @param {string} filename + * @param {string} [description] + * @returns {Promise} confirmation for insertion of filename in database + */ + async add(filename, description) { + try{ + if(filename.length === 0) throw new Error('filename is missing') + let sql = `SELECT COUNT(id) as records from images WHERE image_path = '${filename}';` + const data = await this.db.get(sql) + if(data.records !== 0) throw new Error('filename is already in use') + if(description === undefined) { + sql = `INSERT INTO images (image_path) values ('${filename}');` + await this.db.run(sql) + return true + } else { + sql = `INSERT INTO images (image_path, image_description) values ('${filename}', '${description}');` + await this.db.run(sql) + return true + } + } catch(err) { + throw err + } + } + + /** + * Retrieves details of an image + * @typedef {imageData} + * @property {string} - image_path + * @property {string} - image_description + * @param {Integer} key - id of the image + * @returns {Promise} + */ + async get(key) { + try{ + if(key === undefined) throw new Error('key is not defined') + let sql = `SELECT COUNT(id) as records FROM images WHERE id = ${key};` + let data = await this.db.get(sql) + if(data.records === 0) throw new Error('file does not exist') + sql = `SELECT image_path, image_description + FROM images WHERE id = ${key};` + data = await this.db.get(sql) + return data + } catch(err) { + throw err + } + } + + async getAll() { + const sql = `SELECT image_path as path, + image_description as description + FROM images;` + const data = await this.db.all(sql) + return data + } + + async rename(targetFile, newName) { + try{ + if(targetFile.length === 0) throw new Error('file not selected') + if(newName.length === 0) throw new Error('new name not stated') + let sql = `SELECT COUNT(id) as records FROM images WHERE image_path = '${targetFile}';` + const data = await this.db.get(sql) + if(data.records === 0) throw new Error('file is not found') + sql = `UPDATE images SET image_path = '${newName}' WHERE image_path = '${targetFile}';` + await this.db.run(sql) + return true + } catch(err) { + throw err + } + } + + async delete(key) { + try{ + if(key === undefined) throw new Error('key is not defined') + let sql = `SELECT COUNT(id) as records FROM images WHERE id = ${key};` + const data = await this.db.get(sql) + if(data.records === 0) throw new Error('file is not found') + sql = `DELETE FROM images WHERE id = ${key}` + await this.db.run(sql) + return true + } catch(err) { + throw err + } + } + + async count() { + const sql = 'SELECT COUNT(id) as records FROM images' + const data = await this.db.get(sql) + return data.records + } +} + +module.exports = Image diff --git a/modules/imagefs.js b/modules/imagefs.js new file mode 100644 index 0000000..116af39 --- /dev/null +++ b/modules/imagefs.js @@ -0,0 +1,49 @@ +'use strict' + +const fs = require('fs') +const util = require('util') +const mime = require('mime-types') + +const copy = util.promisify(fs.copyFile) +const unlink = util.promisify(fs.unlink) +const exists = util.promisify(fs.exists) + +const uploadPicture = async(path, mimeType, fileName, newPath) => { + try{ + if(path.length === 0) throw new Error('path is blank') + if(mimeType.length === 0) throw new Error('mimeType is blank') + const extension = mime.extension(mimeType) + const acceptedExtension = ['jpg', 'png', 'webp', 'tiff', 'gif', 'svg'] + if(acceptedExtension.includes(extension) === false) throw new Error('file type is not suitable for upload') + const newFile = `${newPath}${fileName}.${extension}` + await copy(path, newFile) + return true + } catch(err) { + throw err + } +} + +const deletePicture = async(targetFile) => { + try{ + if(await exists(`${targetFile}`) === false) throw new Error('image file does not exist') + await unlink(`${targetFile}`) + return true + } catch(err) { + throw err + } +} + +const renamePicture = async(targetFile, newName) => { + try{ + if(await exists(`${targetFile}`) === false) throw new Error('file does not exist') + const name = String(newName) + await copy(`${targetFile}`, `${name}`) + await unlink(`${targetFile}`) + return true + } catch(err) { + throw err + } +} + + +module.exports = {uploadPicture, deletePicture, renamePicture} diff --git a/unit tests/image.spec.js b/unit tests/image.spec.js new file mode 100644 index 0000000..de2c694 --- /dev/null +++ b/unit tests/image.spec.js @@ -0,0 +1,172 @@ +'use strict' + +const Image = require('../modules/image') + +describe('add()', () => { + test('return error if filename is empty', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.add('')) + .rejects.toEqual(Error('filename is missing')) + done() + }) + + test('return error if filename exists in database', async done => { + expect.assertions(1) + const image = await new Image() + await image.add('public/avatars/adrian.png') + await expect(image.add('public/avatars/adrian.png')) + .rejects.toEqual(Error('filename is already in use')) + done() + }) + test('add a single item', async done => { + expect.assertions(1) + const image = await new Image() + await image.add('public/avatars/adrian.png') + const count = await image.count() + expect(count).toEqual(1) + done() + }) + + test('add both filename and description to the database', async done => { + expect.assertions(2) + const image = await new Image() + const described = await image.add('public/avatars/adrian.png', 'Images of Adrian') + const count = await image.count() + expect(count).toEqual(1) + expect(described).toEqual(true) + done() + }) +}) + +describe('get()', () => { + test('return error if parameters are not stated', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.get()) + .rejects.toEqual(Error('key is not defined')) + done() + }) + + test('return error if file does not exist', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.get(1)) + .rejects.toEqual(Error('file does not exist')) + done() + }) + + test('return the filename and description based on input', async done => { + expect.assertions(2) + const image = await new Image() + await image.add('public/avatars/adrian.png', 'Image of Adrian') + const result = await image.get(1) + expect(result.image_path).toEqual('public/avatars/adrian.png') + expect(result.image_description).toEqual('Image of Adrian') + done() + }) +}) +describe('getAll()', () => { + + test('return empty array if there are no data stored', async done => { + expect.assertions(1) + const image = await new Image() + const results = await image.getAll() + expect(results).toEqual([]) + done() + }) + + test('return all image filenames and descriptions', async done => { + expect.assertions(2) + const image = await new Image() + await image.add('public/avatars/adrian.png', 'Image of Adrian') + const results = await image.getAll() + expect(results[0].path).toEqual('public/avatars/adrian.png') + expect(results[0].description).toEqual('Image of Adrian') + done() + }) +}) + +describe('rename()', () => { + + test('return error if filename is empty', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.rename('', 'public/avatars/adrian.png')) + .rejects.toEqual(Error('file not selected')) + done() + }) + + test('return error if new name is empty', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.rename('public/avatars/adrian.png', '')) + .rejects.toEqual(Error('new name not stated')) + done() + }) + + test('return true when image_path is renamed', async done => { + expect.assertions(1) + const image = await new Image() + await image.add('public/avatars/adrian.png') + const renamed = await image.rename('public/avatars/adrian.png', 'public/avatars/adrian1.png') + expect(renamed).toEqual(true) + done() + }) + + test('return error when image_path is not found', async done => { + expect.assertions(1) + const image = await new Image() + await expect( image.rename('public/avatars/adrian.png', 'public/avatars/adrian1.png')) + .rejects.toEqual(Error('file is not found')) + done() + }) +}) + +describe('delete()', () => { + + test('return error when file id is not defined', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.delete()) + .rejects.toEqual(Error('key is not defined')) + done() + }) + + test('return error when file id does not exist', async done => { + expect.assertions(1) + const image = await new Image() + await expect(image.delete(1)) + .rejects.toEqual(Error('file is not found')) + done() + }) + + test('delete a single item from database', async done => { + expect.assertions(1) + const image = await new Image() + await image.add('public/avatars/adrian.png') + const deleted = await image.delete(1) + expect(deleted).toEqual(true) + done() + }) +}) + +describe('count()', () => { + test('return the amount of images in database', async done => { + expect.assertions(1) + const image = await new Image() + await image.add('public/avatars/adrian.png') + await image.add('public/avatars/adrian1.png') + const amount = await image.count() + expect(amount).toEqual(2) + done() + }) + + test('return zero if no images are stored', async done => { + expect.assertions(1) + const image = await new Image() + const amount = await image.count() + expect(amount).toEqual(0) + done() + }) +}) diff --git a/unit tests/imagefs.spec.js b/unit tests/imagefs.spec.js new file mode 100644 index 0000000..f0cdd53 --- /dev/null +++ b/unit tests/imagefs.spec.js @@ -0,0 +1,96 @@ +'use strict' + +const imagefs = require('../modules/imagefs') +const mock = require('mock-fs') +const fs = require('fs') + +const imageFile = fs.readFileSync('unit\ tests/avatar.png') +const soundFile = fs.readFileSync('unit\ tests/sample.mp3') + +beforeEach(() => { + mock({ + 'files/': { + 'fake_image_file': imageFile, + 'fake_audio_file': soundFile + }, + 'public/': { + 'avatars': {} + } + }) +}) + +afterEach(() => { + mock.restore() +}) + +describe('uploadPicture()', () => { + + test('return error if path is blank', async done => { + expect.assertions(1) + await expect(imagefs.uploadPicture('', 'image/png', 'adrian', 'public/avatars/')) + .rejects.toEqual(Error('path is blank')) + done() + }) + + test('return error if mimetype is blank', async done => { + expect.assertions(1) + await expect(imagefs.uploadPicture('files/fake_image_file', '', 'adrian', 'public/avatars/')) + .rejects.toEqual(Error('mimeType is blank')) + mock.restore() + done() + }) + + test('image should be uploaded in specified directory with intended filename', async done => { + expect.assertions(1) + const uploaded = await imagefs.uploadPicture('files/fake_image_file', 'image/png', 'adrian', 'public/avatars/') + expect(uploaded).toEqual(true) + mock.restore() + done() + }) + + test('return error when unsuitable files are uploaded', async done => { + expect.assertions(1) + await expect( imagefs.uploadPicture('files/fake_mp3_file', 'audio/mp3', 'adrian', 'public/avatars/') ) + .rejects.toEqual( Error('file type is not suitable for upload')) + mock.restore() + done() + }) +}) + +describe('deletePicture()', () => { + test('return an error if image does not exist', async done => { + expect.assertions(1) + await expect( imagefs.deletePicture('public/avatars/adrian.png')) + .rejects.toEqual( Error('image file does not exist')) + mock.restore() + done() + }) + + test('delete selected image', async done => { + expect.assertions(1) + await imagefs.uploadPicture('files/fake_image_file', 'image/png', 'adrian', 'public/avatars/') + const deleted = await imagefs.deletePicture('public/avatars/adrian.png') + expect(deleted).toEqual(true) + mock.restore() + done() + }) +}) + +describe('renamePicture()', () => { + test('return error when selected image path does not exist', async done => { + expect.assertions(1) + await expect( imagefs.renamePicture('public/avatars/adrian.png', 'public/avatars/adrian2.png')) + .rejects.toEqual(Error('file does not exist')) + mock.restore() + done() + }) + + test('rename selected image path', async done => { + expect.assertions(1) + await imagefs.uploadPicture('files/fake_image_file', 'image/png', 'adrian', 'public/avatars/') + const renamed = await imagefs.renamePicture('public/avatars/adrian.png', 'public/avatars/adrian2.png') + expect(renamed).toEqual(true) + mock.restore() + done() + }) +})