From 9e048901a52f6130220742366aee682c9fff924c Mon Sep 17 00:00:00 2001 From: groenroos Date: Sun, 24 Oct 2021 12:35:23 +0100 Subject: [PATCH] #130: Write tests for Storage --- lib/Storage.js | 60 ++--- test/_utils/getFileObject.js | 25 ++ test/lib/Storage.test.js | 433 ++++++++++++++++++++++++++++++++++- test/lib/Uploads.test.js | 21 +- 4 files changed, 489 insertions(+), 50 deletions(-) create mode 100644 test/_utils/getFileObject.js diff --git a/lib/Storage.js b/lib/Storage.js index f863a53..ea00654 100755 --- a/lib/Storage.js +++ b/lib/Storage.js @@ -82,9 +82,6 @@ export default class Storage { if (this.app.uploads) { this.schema.uploads = uploadStructure; } - - /* Create dbs */ - this.createDatabase(); } @@ -112,6 +109,8 @@ export default class Storage { } } } + + await this.createDatabase(); } return this.db; @@ -122,36 +121,38 @@ export default class Storage { * Connect to the database and create each collection defined in the scheme */ async createDatabase() { - await this.importDriver(); - - const dbConfig = this.app.config.db; - dbConfig.name = this.app.name || 'app'; - dbConfig.dataLimit = this.app.config.dataLimit; + if (this.db === null) { + await this.importDriver(); + } else { + const dbConfig = this.app.config.db; + dbConfig.name = this.app.name || 'app'; + dbConfig.dataLimit = this.app.config.dataLimit; - await this.db.connect(dbConfig); + await this.db.connect(dbConfig); - /* Create each collection in the schema in the database */ - for (const collection in this.schema) { - if (Object.prototype.hasOwnProperty.call(this.schema, collection)) { - const fields = this.schema[collection]; + /* Create each collection in the schema in the database */ + for (const collection in this.schema) { + if (Object.prototype.hasOwnProperty.call(this.schema, collection)) { + const fields = this.schema[collection]; - try { - await this.db.createCollection(collection, fields); - } catch (error) { - console.warn(error); - } + try { + await this.db.createCollection(collection, fields); + } catch (error) { + console.warn(error); + } - /* Go through all the fields in the model */ - for (const key in fields) { - /* Create indices for any fields marked unique or identifiable */ - if (fields[key].unique || fields[key].identifiable) { - await this.db.createIndex(collection, { [key]: 'unique' }); + /* Go through all the fields in the model */ + for (const key in fields) { + /* Create indices for any fields marked unique or identifiable */ + if (fields[key].unique || fields[key].identifiable) { + await this.db.createIndex(collection, { [key]: 'unique' }); + } } } } - } - console.log('CREATED DBS'); + console.log('CREATED DBS'); + } } @@ -195,7 +196,7 @@ export default class Storage { const rules = this.getRules(collection); /* If it doesn't exist, return null in strict mode */ - if (!('field' in rules) && this.app.config.strict) { + if (!(field in rules) && this.app.config.strict) { return null; } @@ -351,10 +352,9 @@ export default class Storage { /* Specially format certain fields */ for (const field of Object.keys(rules)) { const rule = rules[field]; - const type = (rule.type || rule).toLowerCase(); /* Format reference fields */ - if (type === 'reference' || type === 'id') { + if (rule.type === 'reference' || rule.type === 'id') { if (conditions.references) { conditions.references.push(field); } else { @@ -364,12 +364,12 @@ export default class Storage { if (data[field]) { /* Format date fields */ - if (type === 'date') { + if (rule.type === 'date') { data[field] = Number(moment(data[field]).format('x')); } /* Format boolean fields */ - if (type === 'boolean') { + if (rule.type === 'boolean') { data[field] = Boolean(data[field]); } } diff --git a/test/_utils/getFileObject.js b/test/_utils/getFileObject.js new file mode 100644 index 0000000..58f5405 --- /dev/null +++ b/test/_utils/getFileObject.js @@ -0,0 +1,25 @@ +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import mimeTypes from 'mime-types'; + + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + + +export default (filename, cb) => { + const filepath = path.join(__dirname, '../_data/files', filename); + const file = fs.readFileSync(filepath); + const stats = fs.statSync(filepath); + const mime = mimeTypes.lookup(filepath); + + return { + name: filename, + data: file, + size: stats.size, + tempFilePath: filepath, + truncated: false, + mimetype: mime, + mv: cb ? cb : () => true + }; +}; diff --git a/test/lib/Storage.test.js b/test/lib/Storage.test.js index ca8c08c..7b5b4b9 100644 --- a/test/lib/Storage.test.js +++ b/test/lib/Storage.test.js @@ -1,5 +1,436 @@ import test from 'ava'; +import Request from '../../lib/Request.js'; +import Response from '../../lib/Response.js'; +import SaplingError from '../../lib/SaplingError.js'; +import Uploads from '../../lib/Uploads.js'; +import User from '../../lib/User.js'; + +import getFileObject from '../_utils/getFileObject.js'; + import Storage from '../../lib/Storage.js'; -test.todo('tests Storage'); + +test.beforeEach(async t => { + t.context.app = (await import('../_utils/app.js')).default(); + t.context.app.user = new User(t.context.app); + t.context.app.request = new Request(t.context.app); + + t.context.request = (await import('../_utils/request.js')).default(); + t.context.app.name = 'test'; + + t.context.schema = { + posts: { + title: 'string', + body: { + type: 'string', + required: true + }, + tags: ['bad', 'data'], + posted: { + type: 'Date' + }, + published: { + type: 'boolean' + } + }, + tags: { + name: 'string', + post: 'reference' + } + }; +}); + + +/* importDriver */ + +test.serial('loads the default driver', async t => { + await t.notThrowsAsync(async () => { + t.context.app.storage = new Storage(t.context.app); + return await t.context.app.storage.importDriver(); + }); +}); + +test.serial('loads a driver case insensitively', async t => { + t.context.app.config.db.driver = 'meMoRY'; + + await t.notThrowsAsync(async () => { + t.context.app.storage = new Storage(t.context.app); + return await t.context.app.storage.importDriver(); + }); +}); + +test.serial('loads a custom driver', async t => { + t.context.app.config.db.driver = 'db-driver-custom'; + + await t.throwsAsync(async () => { + t.context.app.storage = new Storage(t.context.app); + return await t.context.app.storage.importDriver(); + }, { + instanceOf: SaplingError, + message: 'Cannot find any DB driver for \'db-driver-custom\'' + }); +}); + + +/* createDatabase */ + +test.serial('imports driver if not yet imported', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + t.context.app.storage.importDriver = t.pass; + + await t.context.app.storage.createDatabase(); +}); + +test.serial('does not import driver if already imported', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + t.context.app.storage.importDriver = t.fail; + + await t.context.app.storage.createDatabase(); + t.pass(); +}); + + + + +/* getRules */ + +test.serial('returns and normalises the given ruleset', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + t.deepEqual( + t.context.app.storage.getRules('posts'), + { + title: { + type: 'string' + }, + body: { + type: 'string', + required: true + }, + tags: { + type: 'string' + }, + posted: { + type: 'date' + }, + published: { + type: 'boolean' + } + } + ); +}); + +test.serial('returns an empty object for nonexistent ruleset', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + t.deepEqual( + t.context.app.storage.getRules('updates'), + {} + ); +}); + + +/* getRule */ + +test.serial('returns and normalises the given rule', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + t.deepEqual( + t.context.app.storage.getRule('body', 'posts'), + { + type: 'string', + required: true + } + ); +}); + +test.serial('returns a default object for nonexistent rule', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + t.deepEqual( + t.context.app.storage.getRule('location', 'posts'), + { + type: 'string' + } + ); +}); + +test.serial('returns null for nonexistent rule in strict mode', async t => { + t.context.app.config.strict = true; + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + t.is( + t.context.app.storage.getRule('location', 'posts'), + null + ); +}); + + +/* get */ + +test.serial('gets multiple records', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + await t.context.app.storage.db.write('posts', { title: 'Hello' }); + await t.context.app.storage.db.write('posts', { title: 'Hi' }); + + const response = await t.context.app.storage.get({ + url: '/data/posts', + permission: { role: 'member' }, + session: { role: 'member' } + }); + + t.is(response.length, 2); + t.is(response[0].title, 'Hello'); + t.true('_id' in response[0]); + t.is(response[1].title, 'Hi'); + t.true('_id' in response[1]); +}); + +test.serial('gets a single record', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + await t.context.app.storage.db.write('posts', { title: 'Hello' }); + await t.context.app.storage.db.write('posts', { title: 'Hi' }); + + const response = await t.context.app.storage.get({ + url: '/data/posts/title/Hi', + permission: { role: [ 'member' ] }, + session: { user: { role: 'member' } } + }); + + t.is(response.length, 1); + t.is(response[0].title, 'Hi'); + t.true('_id' in response[0]); +}); + + +/* post */ + +test.serial('posts a record', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Hello', + body: 'This is a post' + } + }); + + t.is(response.length, 1); + t.is(response[0].title, 'Hello'); + t.is(response[0].body, 'This is a post'); + t.true('_id' in response[0]); + t.true('_created' in response[0]); +}); + +test.serial('attaches creator details', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Howdy', + body: 'This is a post' + }, + permission: { role: [ 'member' ] }, + session: { + user: { + role: 'admin', + _id: '123', + email: 'foo@example.com' + } + } + }); + + t.is(response.length, 1); + t.is(response[0].title, 'Howdy'); + t.is(response[0]._creator, '123'); + t.is(response[0]._creatorEmail, 'foo@example.com'); + t.true('_id' in response[0]); +}); + +test.serial('modifies a record', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + await t.context.app.storage.db.write('posts', { title: 'Hello' }); + await t.context.app.storage.db.write('posts', { title: 'Hi' }); + + const response = await t.context.app.storage.post({ + url: '/data/posts/title/Hi', + body: { + title: 'Howdy' + } + }); + + t.is(response.length, 1); + t.is(response[0].title, 'Howdy'); + t.true('_id' in response[0]); +}); + +test.serial('attaches updator details', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Howdy', + body: 'This is a post' + }, + permission: { role: [ 'member' ] }, + session: { + user: { + role: 'admin', + _id: '123', + email: 'foo@example.com' + } + } + }); + + const response = await t.context.app.storage.post({ + url: '/data/posts/title/Howdy', + body: { + title: 'Hello' + }, + permission: { role: [ 'member' ] }, + session: { + user: { + role: 'admin', + _id: '345', + email: 'bar@example.com' + } + } + }); + + t.is(response.length, 1); + t.is(response[0].title, 'Hello'); + t.is(response[0]._creator, '123'); + t.is(response[0]._creatorEmail, 'foo@example.com'); + t.is(response[0]._lastUpdator, '345'); + t.is(response[0]._lastUpdatorEmail, 'bar@example.com'); + t.true('_id' in response[0]); +}); + +test.serial('formats specific fields correctly', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Hello', + body: 'This is a post', + posted: '1996-02-10T02:00:00+02:00', + published: 'true' + } + }); + + t.is(response.length, 1); + t.is(response[0].posted, 823910400000); + t.is(typeof response[0].posted, 'number'); + t.is(response[0].published, true); + t.is(typeof response[0].published, 'boolean'); +}); + +test.serial('handles request with files if file uploads are configured', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + t.context.app.config.upload = { + type: 'local', + destination: 'uploads' + }; + t.context.app.uploads = new Uploads(t.context.app); + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Howdy', + body: 'This is a post' + }, + files: { + image: getFileObject('image.png') + } + }); + + t.false(response.error instanceof SaplingError); +}); + +test.serial('responds with an error to request with files if file uploads are not configured', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Howdy', + body: 'This is a post' + }, + files: { + image: getFileObject('image.png') + } + }); + + t.true(response instanceof Response); + t.true(response.error instanceof SaplingError); + t.is(response.error.message, 'File uploads are not allowed'); +}); + +test.serial('responds with an error if write fails', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + t.context.app.storage.db.write = () => { + throw 'DB error'; + }; + + const response = await t.context.app.storage.post({ + url: '/data/posts', + body: { + title: 'Howdy' + } + }); + + t.true(response instanceof Response); + t.true(response.error instanceof SaplingError); +}); + +test.serial('responds with an error if modify fails', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + t.context.app.storage.db.modify = () => { + throw 'DB error'; + }; + + const response = await t.context.app.storage.post({ + url: '/data/posts/title/Hi', + body: { + title: 'Howdy' + } + }); + + t.true(response instanceof Response); + t.true(response.error instanceof SaplingError); +}); + + +/* delete */ + +test.serial('deletes a record', async t => { + t.context.app.storage = new Storage(t.context.app, t.context.schema); + await t.context.app.storage.importDriver(); + + await t.context.app.storage.db.write('posts', { title: 'Hello' }); + await t.context.app.storage.db.write('posts', { title: 'Hi' }); + + const response = await t.context.app.storage.delete({ + url: '/data/posts/title/Hi' + }); + + t.deepEqual(response, [ { success: true } ]); +}); diff --git a/test/lib/Uploads.test.js b/test/lib/Uploads.test.js index dfc70bf..e45ff73 100644 --- a/test/lib/Uploads.test.js +++ b/test/lib/Uploads.test.js @@ -2,36 +2,19 @@ import test from 'ava'; import path from 'path'; import fs from 'fs'; import _ from 'underscore'; -import mimeTypes from 'mime-types'; import { fileURLToPath } from 'url'; import Response from '../../lib/Response.js'; import SaplingError from '../../lib/SaplingError.js'; +import getFileObject from '../_utils/getFileObject.js'; + import Uploads from '../../lib/Uploads.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const getFileObject = (filename, cb) => { - const filepath = path.join(__dirname, '../_data/files', filename); - const file = fs.readFileSync(filepath); - const stats = fs.statSync(filepath); - const mime = mimeTypes.lookup(filepath); - - return { - name: filename, - data: file, - size: stats.size, - tempFilePath: filepath, - truncated: false, - mimetype: mime, - mv: cb ? cb : () => true - }; -}; - - test.beforeEach(async t => { t.context.app = _.defaults({ dir: __dirname,