Skip to content

Commit

Permalink
Merge pull request #320 from saplingjs/test/storage
Browse files Browse the repository at this point in the history
Tests for Storage, Request and User
  • Loading branch information
groenroos authored Oct 24, 2021
2 parents 8f52bd7 + 6d88dfc commit c49d115
Show file tree
Hide file tree
Showing 23 changed files with 1,408 additions and 302 deletions.
15 changes: 4 additions & 11 deletions core/loadModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Storage from '../lib/Storage.js';
*/
export default async function loadModel(next) {
const modelPath = path.join(this.dir, this.config.modelsDir);
const structure = {};
const schema = {};
let files = {};

/* Load all models in the model directory */
Expand All @@ -41,7 +41,7 @@ export default async function loadModel(next) {

const model = fs.readFileSync(path.join(modelPath, file));

/* Read the model JSON into the structure */
/* Read the model JSON into the schema */
try {
/* Attempt to parse the JSON */
const parsedModel = JSON.parse(model.toString());
Expand All @@ -63,21 +63,14 @@ export default async function loadModel(next) {
}

/* Save */
structure[table] = parsedModel;
schema[table] = parsedModel;
} catch {
throw new SaplingError(`Error parsing model \`${table}\``);
}
}

this.structure = structure;

/* Create a storage instance based on the models */
this.storage = new Storage(this, {
name: this.name,
schema: this.structure,
config: this.config,
dir: this.dir,
});
this.storage = new Storage(this, schema);

if (next) {
next();
Expand Down
11 changes: 5 additions & 6 deletions hooks/sapling/model/retrieve.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,28 @@
/* Dependencies */
import Response from '@sapling/sapling/lib/Response.js';
import SaplingError from '@sapling/sapling/lib/SaplingError.js';
import Utils from '@sapling/sapling/lib/Utils.js';


/* Hook /api/model/:model */
export default async function retrieve(app, request, response) {
if (request.params.model) {
/* Fetch the given model */
const schema = new Utils().deepClone(app.storage.schema[request.params.model] || []);
const rules = app.storage.getRules(request.params.model);

/* If no model, respond with an error */
if (schema.length === 0) {
if (Object.keys(rules).length === 0) {
return new Response(app, request, response, new SaplingError('No such model'));
}

/* Remove any internal/private model values (begin with _) */
for (const k in schema) {
for (const k in rules) {
if (k.startsWith('_')) {
delete schema[k];
delete rules[k];
}
}

/* Send it out */
return new Response(app, request, response, null, schema);
return new Response(app, request, response, null, rules);
}

return new Response(app, request, response, new SaplingError('No model specified'));
Expand Down
5 changes: 4 additions & 1 deletion hooks/sapling/user/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import SaplingError from '@sapling/sapling/lib/SaplingError.js';

/* Hook /api/user/login */
export default async function login(app, request, response) {
/* Fetch the user model */
const rules = app.storage.getRules('users');

/* Find all identifiable fields */
const identifiables = Object.keys(app.storage.schema.users).filter(field => app.storage.schema.users[field].identifiable);
const identifiables = Object.keys(rules).filter(field => rules[field].identifiable);

/* Figure out which request value is used */
let identValue = false;
Expand Down
145 changes: 75 additions & 70 deletions lib/Request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import _ from 'underscore';

import { console } from './Cluster.js';
import Response from './Response.js';
import SaplingError from './SaplingError.js';
import Validation from './Validation.js';


Expand Down Expand Up @@ -61,7 +59,7 @@ export default class Request {
getCreatorConstraint(request, role) {
const conditions = {};

if (request.permission && request.permission.role.includes('owner') && role !== 'admin') {
if (request.session && request.session.user && request.permission && request.permission.role.includes('owner') && role !== 'admin') {
conditions._creator = request.session.user._id;
}

Expand All @@ -82,7 +80,7 @@ export default class Request {
/* Request method */
const method = request.method && request.method.toUpperCase();

/* Trim uneeded parts of the request */
/* Trim unneeded parts of the request */
if (parts[0] === '') {
parts.splice(0, 1);
}
Expand Down Expand Up @@ -122,6 +120,71 @@ export default class Request {
}
}

/* Format incoming data */
if (request.body) {
/* Go through every key in incoming data */
for (const key in request.body) {
if (Object.prototype.hasOwnProperty.call(request.body, key)) {
/* Get the corresponding ruleset */
const rule = this.app.storage.getRule(key, collection);

/* Trim incoming data unless otherwise specified in model */
if (typeof request.body[key] === 'string' && (!rule || !('trim' in rule) || rule.trim !== false)) {
request.body[key] = String(request.body[key]).trim();
}

/* If the data is a number, convert from string */
if (rule && rule.type === 'number') {
request.body[key] = Number.parseFloat(request.body[key], 10);
}

/* Ignore CSRF tokens */
if (key === '_csrf') {
delete request.body[key];
}

/* In strict mode, don't allow unknown fields */
if (!rule && this.app.config.strict) {
console.warn('UNKNOWN FIELD', key);
delete request.body[key];
}

/* If this field has no defined access level, we can skip the rest of the checks */
if (!rule || !rule.access) {
continue;
}

/* Get the write access level */
const access = rule.access.w || rule.access;

/* If the field is owner-only, defer to individual op methods to check against it */
if (access === 'owner') {
continue;
}

/* Get the role from session, if any */
const role = this.app.user.getRole({ session: request.session });

/* If we do not have access, raise hell */
if (this.app.user.isRoleAllowed(role, access) === false) {
console.warn(`NO ACCESS TO FIELD '${key}'`);
console.warn(`Current permission level: ${role}`);
console.warn(`Required permission level: ${access}`);
delete request.body[key];
}
}
}

/* Go through every rule */
const rules = this.app.storage.getRules(collection);
for (const key in rules) {
/* If inserting, and a field with a default value is missing, apply default */
if (parts.length <= 2 && !(key in request.body) && 'default' in rules[key]) {
request.body[key] = rules[key].default;
}
}
}

/* Modify the request object */
return _.extend(request, {
collection,
Expand All @@ -140,21 +203,18 @@ export default class Request {
* @param {object} request Request object from Express
* @param {object} response Response object from Express
*/
validateData(request, response) {
const { collection, body, session, type } = request;
validateData(request) {
const { collection, body, type } = request;

/* Get the collection definition */
const rules = this.app.storage.schema[collection] || {};
const rules = this.app.storage.getRules(collection);

let errors = [];
const data = body || {};

/* Get the role from session, if any */
const role = this.app.user.getRole({ session });

/* Model must be defined before pushing data */
if (!rules && this.app.config.strict) {
new Response(this.app, request, response, new SaplingError({
if (Object.keys(rules).length === 0 && this.app.config.strict) {
return [{
status: '500',
code: '1010',
title: 'Non-existent',
Expand All @@ -163,71 +223,25 @@ export default class Request {
type: 'data',
error: 'nonexistent',
},
}));
return false;
}];
}

/* Go through every key in incoming data */
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
/* Ignore CSRF tokens */
if (key === '_csrf') {
delete data[key];
}

/* Get the corresponding ruleset */
const rule = rules[key];

/* Trim incoming data unless otherwise specified in model */
if (typeof data[key] === 'string' && (!rule || !('trim' in rule) || rule.trim !== false)) {
data[key] = String(data[key]).trim();
}
const rule = this.app.storage.getRule(key, collection);

/* If the field isn't defined */
/* If the field isn't defined, skip */
if (!rule) {
/* In strict mode, don't allow unknown fields */
if (this.app.config.strict) {
console.warn('UNKNOWN FIELD', key);
delete data[key];
}

/* Otherwise skip this field */
continue;
}

const dataType = (rule.type || rule).toLowerCase();

/* If the data is a number, convert from string */
if (dataType === 'number') {
data[key] = Number.parseFloat(data[key], 10);
}

/* Test in the validation library */
const error = new Validation().validate(data[key], key, rule);
if (error.length > 0) {
errors = error;
}

/* If this field has no defined access level, we can skip the rest of the checks */
if (!rule.access) {
continue;
}

/* Get the write access level */
const access = rule.access.w || rule.access;

/* If the field is owner-only, defer to individual op methods to check against it */
if (access === 'owner') {
continue;
}

/* If we do not have access, raise hell */
if (this.app.user.isRoleAllowed(role, access) === false) {
console.warn(`NO ACCESS TO FIELD '${key}'`);
console.warn('Current permission level:', role);
console.warn('Required permission level:', access);
delete data[key];
}
}
}

Expand All @@ -240,10 +254,6 @@ export default class Request {
continue;
}

if (typeof rules[key] !== 'object') {
continue;
}

/* We now know the given field does not have a corresponding value
in the incoming data */

Expand All @@ -260,11 +270,6 @@ export default class Request {
},
});
}

/* Set the data to the default value, if provided */
if ('default' in rules[key]) {
data[key] = rules[key].default;
}
}
}

Expand Down
Loading

0 comments on commit c49d115

Please sign in to comment.