mirror of
https://github.com/Febbweiss/CloudBudget.git
synced 2026-03-04 14:25:39 +00:00
Feature: First version of the REST API done
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
logs/*
|
||||
data/*
|
||||
mongod*
|
||||
public/libs/*
|
||||
3
app.js
Normal file
3
app.js
Normal file
@@ -0,0 +1,3 @@
|
||||
var server = require('./server.js');
|
||||
|
||||
server.listen();
|
||||
227
app/controllers/accounts.js
Normal file
227
app/controllers/accounts.js
Normal file
@@ -0,0 +1,227 @@
|
||||
var mongoose = require('mongoose'),
|
||||
ObjectId = mongoose.Schema.Types.ObjectId,
|
||||
Account = mongoose.model('Account'),
|
||||
Entry = mongoose.model('Entry'),
|
||||
Handler = require('../helpers/handler'),
|
||||
categories = require('../models/categories');
|
||||
|
||||
var load_categories = function(language, callback) {
|
||||
var catz = {};
|
||||
categories.forEach(function(category) {
|
||||
var selected_category = category[language],
|
||||
key = category.en.categorygroup.toLowerCase().replace(/ /g, '_'),
|
||||
cat = catz[key] || {key: key, label: selected_category.categorygroup, sub_categories: []};
|
||||
|
||||
if( !selected_category.category ) {
|
||||
cat.sub_categories.push({label: selected_category.subcategory, key: selected_category.subcategory.toLowerCase().replace(/ /g, '_')});
|
||||
}
|
||||
|
||||
catz[key] = cat;
|
||||
});
|
||||
|
||||
callback(null, catz);
|
||||
};
|
||||
|
||||
var check_account = function(request, response, callback) {
|
||||
Account.findById(request.params.account_id, function(errors, account) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 404, response);
|
||||
}
|
||||
|
||||
if( !account ) {
|
||||
return response.status(404).json({message: 'Unknown account'});
|
||||
}
|
||||
|
||||
if( !account.user_id.equals(request.user.id) ) {
|
||||
return response.status(401).end();
|
||||
}
|
||||
|
||||
return callback(null, account);
|
||||
});
|
||||
};
|
||||
|
||||
var delete_account = function(account, callback) {
|
||||
Entry.find({account_id: account.id}).remove(function(errors) {
|
||||
if( errors ) {
|
||||
if( callback ) {
|
||||
return callback(errors);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
account.remove(function(errors) {
|
||||
if( errors ) {
|
||||
if( callback ) {
|
||||
return callback(errors);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if( callback ) {
|
||||
return callback();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create : function(request, response) {
|
||||
var user = request.user,
|
||||
account = new Account({
|
||||
name: request.body.name,
|
||||
reference: request.body.reference,
|
||||
user_id: user.id
|
||||
});
|
||||
|
||||
load_categories(user.language, function(error, result) {
|
||||
for( var key in result ) {
|
||||
account.categories.push( result[key] );
|
||||
}
|
||||
|
||||
account.save(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 400, response);
|
||||
}
|
||||
|
||||
return response.status(201).json(account);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
modify : function(request, response) {
|
||||
return check_account(request, response, function(error, account) {
|
||||
account.name = request.body.name;
|
||||
account.reference = request.body.reference;
|
||||
|
||||
account.save(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 400, response);
|
||||
}
|
||||
|
||||
return response.json(account);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
delete : function(request, response) {
|
||||
return check_account(request, response, function(error, account) {
|
||||
return delete_account(account, function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 500, response);
|
||||
}
|
||||
|
||||
return response.status(204).end();
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
delete_account : delete_account,
|
||||
|
||||
get : function(request, response) {
|
||||
return check_account(request, response, function(error, account) {
|
||||
return response.json(account);
|
||||
});
|
||||
},
|
||||
|
||||
add_entry : function(request, response) {
|
||||
return check_account(request, response, function(error, account) {
|
||||
var data = request.body,
|
||||
entry = new Entry({
|
||||
account_id: account.id,
|
||||
category: data.category ? new ObjectId(data.category) : undefined,
|
||||
sub_category: data.sub_category ? new ObjectId(data.sub_category) : undefined,
|
||||
label: data.label,
|
||||
amount: data.amount,
|
||||
date: new Date(data.date),
|
||||
type: data.amount >= 0 ? 'DEPOSIT' : 'BILL'
|
||||
});
|
||||
|
||||
entry.save(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 400, response);
|
||||
}
|
||||
|
||||
response.status(201).json(entry);
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
modify_entry : function(request, response) {
|
||||
return check_account(request, response, function(error, account) {
|
||||
Entry.findById(request.params.entry_id, function(errors, entry) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 404, response);
|
||||
}
|
||||
|
||||
if( !entry ) {
|
||||
return response.status(404).end();
|
||||
}
|
||||
|
||||
if( !entry.account_id.equals( account.id ) ) {
|
||||
return response.status(401).end();
|
||||
}
|
||||
|
||||
var data = request.body;
|
||||
|
||||
entry.category = data.category ? new ObjectId(data.category) : undefined;
|
||||
entry.sub_category = data.sub_category ? new ObjectId(data.sub_category) : undefined;
|
||||
entry.label = data.label;
|
||||
entry.amount = data.amount;
|
||||
entry.date = new Date(data.date);
|
||||
entry.type = data.amount >= 0 ? 'DEPOSIT' : 'BILL';
|
||||
|
||||
entry.save(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 400, response );
|
||||
}
|
||||
|
||||
return response.json(entry);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
delete_entry : function(request, response) {
|
||||
return check_account(request, response, function(errors, account) {
|
||||
Entry.findById(request.params.entry_id, function(errors, entry) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 404, response);
|
||||
}
|
||||
|
||||
if( !entry ) {
|
||||
return response.status(404).end();
|
||||
}
|
||||
|
||||
if( !entry.account_id.equals( account.id ) ) {
|
||||
return response.status(401).end();
|
||||
}
|
||||
|
||||
entry.remove(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 500, response);
|
||||
}
|
||||
|
||||
return response.status(204).end();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
list_entries : function(request, response) {
|
||||
return check_account(request, response, function(errors, account) {
|
||||
Entry.find({
|
||||
account_id: account.id
|
||||
})
|
||||
.sort('-date')
|
||||
.exec(function(errors, entries) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 500, response);
|
||||
}
|
||||
|
||||
return response.json(entries);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
79
app/controllers/users.js
Normal file
79
app/controllers/users.js
Normal file
@@ -0,0 +1,79 @@
|
||||
var mongoose = require('mongoose'),
|
||||
User = mongoose.model('User'),
|
||||
jwt = require('jsonwebtoken'),
|
||||
security = require('../../config/security'),
|
||||
Handler = require('../helpers/handler'),
|
||||
EventEmitter = require('../events/listeners');
|
||||
|
||||
module.exports = {
|
||||
login : function(request, response) {
|
||||
var user = request.user;
|
||||
if( !user ) {
|
||||
return response.status(401).json({message: 'Authentication failed'});
|
||||
}
|
||||
|
||||
return response.json(
|
||||
{
|
||||
username: user.username,
|
||||
token: jwt.sign(
|
||||
{
|
||||
user_id: user.id
|
||||
}, security.jwt.secretOrKey)
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
logout : function(request, response) {
|
||||
return response.status(200).end();
|
||||
},
|
||||
|
||||
subscribe : function(request, response) {
|
||||
var registered = new User({username: request.body.username, password: request.body.password});
|
||||
registered.validate(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 400, response);
|
||||
}
|
||||
|
||||
User.findOne({username: request.body.username}, function(error, user) {
|
||||
if( error ) {
|
||||
return response.send(error);
|
||||
}
|
||||
if( !user ) {
|
||||
registered.save(function(errors) {
|
||||
if( errors ) {
|
||||
return Handler.errorHandler(errors, 500, response);
|
||||
}
|
||||
|
||||
return response.status(201).json({
|
||||
username: registered.username,
|
||||
token: jwt.sign(
|
||||
{
|
||||
user_id: registered.id
|
||||
}, security.jwt.secretOrKey)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return response.status(409).json({message: 'Account already exists'});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unsubscribe : function(request, response) {
|
||||
var user = request.user;
|
||||
|
||||
if( !user ) {
|
||||
return response.status(401).json({message: 'Authentication failed'});
|
||||
}
|
||||
|
||||
User.remove({username: user.username}, function(error) {
|
||||
if( error ) {
|
||||
return response.status(500).send(error);
|
||||
}
|
||||
|
||||
EventEmitter.eventEmitter.emit(EventEmitter.events.ACCOUNTS_DELETE_BY_USER_ID_EVT, user.id);
|
||||
|
||||
return response.status(204).end();
|
||||
});
|
||||
}
|
||||
}
|
||||
35
app/events/listeners.js
Normal file
35
app/events/listeners.js
Normal file
@@ -0,0 +1,35 @@
|
||||
var mongoose = require('mongoose'),
|
||||
Account = mongoose.model('Account'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
AccountController = require('../controllers/accounts');
|
||||
|
||||
|
||||
var eventEmitter = new EventEmitter(),
|
||||
ACCOUNTS_DELETE_BY_USER_ID_EVT = 'accounts.delete.by.user.id',
|
||||
ENTRIES_DELETE_BY_ACCOUNT_EVT = 'entries.delete.by.account';
|
||||
|
||||
eventEmitter.on(ACCOUNTS_DELETE_BY_USER_ID_EVT, function(user_id) {
|
||||
Account.find({user_id: user_id}, function(errors, accounts) {
|
||||
if( errors ) {
|
||||
console.error('An error occurs during accounts deletion for user ' + user_id, errors);
|
||||
return;
|
||||
}
|
||||
|
||||
if( !accounts ) {
|
||||
console.log('No accounts');
|
||||
return;
|
||||
}
|
||||
for( var index in accounts ) {
|
||||
eventEmitter.emit(ENTRIES_DELETE_BY_ACCOUNT_EVT, accounts[index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
eventEmitter.on(ENTRIES_DELETE_BY_ACCOUNT_EVT, AccountController.delete_account);
|
||||
|
||||
module.exports = {
|
||||
events : {
|
||||
ACCOUNTS_DELETE_BY_USER_ID_EVT: ACCOUNTS_DELETE_BY_USER_ID_EVT,
|
||||
ENTRIES_DELETE_BY_ACCOUNT_EVT: ENTRIES_DELETE_BY_ACCOUNT_EVT
|
||||
},
|
||||
eventEmitter: eventEmitter
|
||||
}
|
||||
18
app/helpers/handler.js
Normal file
18
app/helpers/handler.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
errorHandler : function(errors, status, response) {
|
||||
var message = []
|
||||
if( errors.errors) {
|
||||
Object.keys(errors.errors).forEach(function (field) {
|
||||
var error = errors.errors[field];
|
||||
message.push({
|
||||
field: error.path,
|
||||
rule: error.kind,
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
return response.status(status).json(message);
|
||||
} else {
|
||||
return response.status(status).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app/models/account.js
Normal file
25
app/models/account.js
Normal file
@@ -0,0 +1,25 @@
|
||||
var mongoose = require('mongoose'),
|
||||
Schema = mongoose.Schema,
|
||||
ObjectId = Schema.Types.ObjectId;
|
||||
|
||||
var CategorySchema = new Schema({
|
||||
label: {type: String, required:true},
|
||||
key: {type: String, required: true, index: {unique: false} },
|
||||
sub_categories: [{
|
||||
label: {type: String, required:true},
|
||||
key: {type: String, required: true, index: {unique: false} },
|
||||
}]
|
||||
});
|
||||
|
||||
var AccountSchema = new Schema({
|
||||
name: {type: String, required: true},
|
||||
reference: {type: String, required: false},
|
||||
categories: {type: [CategorySchema], required: true},
|
||||
user_id: {type: ObjectId, ref: 'User', required: true},
|
||||
created_at: {type: Date, default: Date.now}
|
||||
});
|
||||
|
||||
var Account = mongoose.model('Account', AccountSchema);
|
||||
var Category = mongoose.model('Category', CategorySchema);
|
||||
|
||||
module.exports = Account;
|
||||
1514
app/models/categories.js
Normal file
1514
app/models/categories.js
Normal file
File diff suppressed because it is too large
Load Diff
20
app/models/entry.js
Normal file
20
app/models/entry.js
Normal file
@@ -0,0 +1,20 @@
|
||||
var mongoose = require('mongoose'),
|
||||
Schema = mongoose.Schema,
|
||||
ObjectId = Schema.Types.ObjectId;
|
||||
|
||||
var DEBIT = 'D',
|
||||
BILL = 'B';
|
||||
|
||||
var EntrySchema = new Schema({
|
||||
account_id: {type: ObjectId, ref: 'Category', required: true},
|
||||
category: {type: ObjectId, ref: 'Account', required: false},
|
||||
sub_category: {type: ObjectId, required: false},
|
||||
label: {type: String, required:false},
|
||||
type: {type: String, required: true},
|
||||
amount: {type: Number, required: true},
|
||||
date: {type: Date, required: true},
|
||||
created_at: {type: Date, default: Date.now}
|
||||
});
|
||||
|
||||
var Entry = mongoose.model('Entry', EntrySchema);
|
||||
module.exports = Entry;
|
||||
87
app/models/user.js
Normal file
87
app/models/user.js
Normal file
@@ -0,0 +1,87 @@
|
||||
var mongoose = require('mongoose'),
|
||||
Schema = mongoose.Schema,
|
||||
bcrypt = require('bcrypt'),
|
||||
SALT_WORK_FACTOR = 10;
|
||||
|
||||
var UserSchema = new Schema({
|
||||
username: { type: String, required: true, index: { unique: true } },
|
||||
password: { type: String, required: true },
|
||||
language: {type: String, required: true, default: 'en'},
|
||||
created_at: {type: Date, 'default': Date.now}
|
||||
});
|
||||
|
||||
UserSchema.statics.getAuthenticated = function(username, password, callback) {
|
||||
this.findOne({ username: username }, function(error, user) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return callback(error);
|
||||
}
|
||||
// make sure the user exists
|
||||
if (!user) {
|
||||
return callback(null, null, 404);
|
||||
}
|
||||
|
||||
user.comparePassword(password, function(error, isMatch) {
|
||||
if (isMatch) {
|
||||
return callback(null, user);
|
||||
}
|
||||
|
||||
return callback(null, null, 401);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
UserSchema.pre('save', function(next) {
|
||||
var user = this;
|
||||
|
||||
// only hash the password if it has been modified (or is new)
|
||||
if (!user.isModified('password')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// generate a salt
|
||||
bcrypt.genSalt(SALT_WORK_FACTOR, function(error, salt) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// hash the password using our new salt
|
||||
bcrypt.hash(user.password, salt, function(error, hash) {
|
||||
if (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// override the cleartext password with the hashed one
|
||||
user.password = hash;
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
UserSchema.methods.comparePassword = function(candidatePassword, callback) {
|
||||
bcrypt.compare(candidatePassword, this.password, function(error, isMatch) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
callback(null, isMatch);
|
||||
});
|
||||
};
|
||||
|
||||
var User = mongoose.model('User', UserSchema);
|
||||
|
||||
User.schema.path('username').validate(function (username) {
|
||||
return username.length;
|
||||
}, 'Username cannot be blank');
|
||||
|
||||
User.schema.path('password').validate(function(password) {
|
||||
return password.length;
|
||||
}, 'Password cannot be blank');
|
||||
|
||||
User.schema.path('language').validate(function(language) {
|
||||
return /en|fr/i.test(language);
|
||||
}, 'Unknown language ("en" or "fr" only)')
|
||||
|
||||
module.exports = User;
|
||||
16
app/routes.js
Normal file
16
app/routes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = function(app) {
|
||||
|
||||
var routes_path = __dirname + '/routes'
|
||||
fs.readdirSync(routes_path).forEach(function (file) {
|
||||
if (~file.indexOf('.js')) {
|
||||
var route = require(routes_path + '/' + file);
|
||||
route(app);
|
||||
}
|
||||
})
|
||||
|
||||
app.get('*', function(req, res) {
|
||||
res.sendfile('./public/views/index.html');
|
||||
});
|
||||
};
|
||||
22
app/routes/accounts.js
Normal file
22
app/routes/accounts.js
Normal file
@@ -0,0 +1,22 @@
|
||||
var passport = require('../security/passport'),
|
||||
AccountController = require('../controllers/accounts');
|
||||
|
||||
module.exports = function(app) {
|
||||
|
||||
app.post('/api/accounts', passport.jwt, AccountController.create);
|
||||
|
||||
app.delete('/api/accounts/:account_id', passport.jwt, AccountController.delete);
|
||||
|
||||
app.get('/api/accounts/:account_id', passport.jwt, AccountController.get);
|
||||
|
||||
app.put('/api/accounts/:account_id', passport.jwt, AccountController.modify);
|
||||
|
||||
app.post('/api/accounts/:account_id/entries', passport.jwt, AccountController.add_entry);
|
||||
|
||||
app.put('/api/accounts/:account_id/entries/:entry_id', passport.jwt, AccountController.modify_entry);
|
||||
|
||||
app.delete('/api/accounts/:account_id/entries/:entry_id', passport.jwt, AccountController.delete_entry);
|
||||
|
||||
app.get('/api/accounts/:account_id/entries', passport.jwt, AccountController.list_entries);
|
||||
|
||||
};
|
||||
13
app/routes/users.js
Normal file
13
app/routes/users.js
Normal file
@@ -0,0 +1,13 @@
|
||||
var passport = require('../security/passport'),
|
||||
UserController = require('../controllers/users');
|
||||
|
||||
module.exports = function(app) {
|
||||
|
||||
app.post('/api/users/login', passport.local, UserController.login);
|
||||
|
||||
app.delete('/api/users/login', UserController.logout);
|
||||
|
||||
app.post('/api/users', UserController.subscribe);
|
||||
|
||||
app.delete('/api/users', passport.jwt, UserController.unsubscribe);
|
||||
};
|
||||
40
app/security/passport.js
Normal file
40
app/security/passport.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var mongoose = require('mongoose'),
|
||||
User = mongoose.model('User'),
|
||||
passport = require('passport'),
|
||||
LocalStrategy = require('passport-local'),
|
||||
JwtStrategy = require('passport-jwt').Strategy,
|
||||
security = require('../../config/security');
|
||||
|
||||
passport.use( new LocalStrategy(
|
||||
function(username, password, done) {
|
||||
User.getAuthenticated(username, password, function(error, user, errorStatus) {
|
||||
if( error ) {
|
||||
return done(error, null);
|
||||
}
|
||||
|
||||
if( !user ) {
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
passport.use( new JwtStrategy(security.jwt, function(jwt_payload, done) {
|
||||
User.findById(jwt_payload.user_id, function(error, user) {
|
||||
if( error ) {
|
||||
return done(error, null);
|
||||
}
|
||||
if( user ) {
|
||||
return done(null, user);
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports = {
|
||||
jwt: passport.authenticate('jwt', {session: false}),
|
||||
local: passport.authenticate('local', {session: false})
|
||||
}
|
||||
11
bower.json
Normal file
11
bower.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "cloud-budget",
|
||||
"version": "0.0.1-SNAPSHOT",
|
||||
"dependencies": {
|
||||
"bootstrap": "~3.3.5",
|
||||
"font-awesome": "~4.3.0",
|
||||
"animate.css": "~3.3.0",
|
||||
"angular": "~1.4.1",
|
||||
"angular-route": "~1.4.1"
|
||||
}
|
||||
}
|
||||
11
config/db.js
Normal file
11
config/db.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
development: {
|
||||
url: 'mongodb://' + process.env.IP + ':27017/cloudbudget_dev'
|
||||
},
|
||||
test: {
|
||||
url: 'mongodb://localhost:27017/cloudbudget_test'
|
||||
},
|
||||
production: {
|
||||
url: 'mongodb://' + process.env.IP + ':27017/cloudbudget'
|
||||
},
|
||||
}
|
||||
7
config/security.js
Normal file
7
config/security.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
jwt : {
|
||||
secretOrKey : 's3cr3t',
|
||||
issuer : undefined, // accounts.examplesoft.com
|
||||
audience : undefined // yoursite.net
|
||||
}
|
||||
}
|
||||
17
config/server.js
Normal file
17
config/server.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
development: {
|
||||
port : process.env.PORT || 3000,
|
||||
server : process.env.IP || '0.0.0.0',
|
||||
errorHandlerOptions: {"dumpExceptions": true, "showStack": true}
|
||||
},
|
||||
test: {
|
||||
port : 3000,
|
||||
server : 'localhost',
|
||||
errorHandlerOptions: {"dumpExceptions": false, "showStack": false}
|
||||
},
|
||||
production: {
|
||||
port : process.env.PORT || 3000,
|
||||
server : process.env.IP || '0.0.0.0',
|
||||
errorHandlerOptions: {"dumpExceptions": false, "showStack": false}
|
||||
},
|
||||
}
|
||||
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "cloud-budget",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"express": "~4.5.1",
|
||||
"mongoose": "~4.0.8",
|
||||
"body-parser": "~1.4.2",
|
||||
"method-override": "~2.0.2",
|
||||
"morgan": "~1.6.0",
|
||||
"file-stream-rotator": "~0.0.6",
|
||||
"errorhandler": "~1.4.1",
|
||||
|
||||
"jsonwebtoken": "~5.0.4",
|
||||
"bcrypt": "~0.8.3",
|
||||
|
||||
"passport": "~0.2.2",
|
||||
"passport-local": "~1.0.0",
|
||||
"passport-jwt": "~1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "~2.2.5",
|
||||
"supertest": "~1.0.1",
|
||||
"should": "~7.0.2",
|
||||
"sinon": "~1.15.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test mocha",
|
||||
"start": "app.js"
|
||||
}
|
||||
}
|
||||
39
public/views/index.html
Normal file
39
public/views/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<base href="/" />
|
||||
|
||||
<title>MEAN Demo Single Page Application</title>
|
||||
|
||||
<link rel="stylesheet" href="libs/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
|
||||
|
||||
<script src="libs/angular/angular.min.js" ></script>
|
||||
<script src="libs/angular-route/angular-route.min.js" ></script>
|
||||
|
||||
<script src="js/controllers/MainCtrl.js"></script>
|
||||
<script src="js/controllers/NerdCtrl.js"></script>
|
||||
<script src="js/services/NerdService.js"></script>
|
||||
<script src="js/appRoutes.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body ng-app="sampleApp" ng-controller="NerdController">
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">MEAN-Demo</a>
|
||||
</div>
|
||||
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/nerds">Nerds</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
0
public/views/user.html
Normal file
0
public/views/user.html
Normal file
64
server.js
Normal file
64
server.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// modules
|
||||
var express = require('express'),
|
||||
app = express(),
|
||||
bodyParser = require('body-parser'),
|
||||
methodOverride = require('method-override'),
|
||||
morgan = require('morgan'),
|
||||
errorHandler = require('errorhandler'),
|
||||
FileStreamRotator = require('file-stream-rotator'),
|
||||
fs = require('fs'),
|
||||
mongoose = require('mongoose');
|
||||
|
||||
//config
|
||||
var db = require('./config/db')[process.env.NODE_ENV],
|
||||
server = require('./config/server')[process.env.NODE_ENV],
|
||||
logDir = __dirname + '/logs';
|
||||
|
||||
fs.existsSync(logDir) || fs.mkdirSync(logDir);
|
||||
|
||||
var accessLogStream = FileStreamRotator.getStream({
|
||||
filename : logDir + '/access-%DATE%.log',
|
||||
frequency : 'daily',
|
||||
verbose : false,
|
||||
date_format: 'YYYY-MM-DD'
|
||||
});
|
||||
|
||||
mongoose.connect(db.url);
|
||||
|
||||
/** Hack to load Models before routing **/
|
||||
var models_path = __dirname + '/app/models'
|
||||
fs.readdirSync(models_path).forEach(function (file) {
|
||||
if (~file.indexOf('.js')) require(models_path + '/' + file)
|
||||
})
|
||||
|
||||
switch(process.env.NODE_ENV) {
|
||||
case 'development' :
|
||||
app.use(morgan('dev'));
|
||||
break;
|
||||
case 'production' :
|
||||
app.use(morgan('combined', {stream: accessLogStream}));
|
||||
break;
|
||||
}
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.json({type: 'application/vnd.api+json'}));
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
app.use(methodOverride('X-HTTP-Method-Override'));
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
app.use(errorHandler(server.errorHandlerOptions));
|
||||
|
||||
require('./app/routes')(app);
|
||||
|
||||
this.app = app;
|
||||
this.server = server;
|
||||
|
||||
exports.listen = function () {
|
||||
if( process.env.NODE_ENV !== 'test' ) {
|
||||
console.log('Server running in ' + process.env.NODE_ENV + ' mode on port ' + this.server.port );
|
||||
}
|
||||
return this.app.listen.apply(this.app, [this.server.port]);
|
||||
};
|
||||
|
||||
exports.close = function (callback) {
|
||||
this.app.close.apply(callback);
|
||||
};
|
||||
543
test/accounts.js
Normal file
543
test/accounts.js
Normal file
@@ -0,0 +1,543 @@
|
||||
var should = require('should'),
|
||||
request = require('supertest'),
|
||||
app = require('../server.js'),
|
||||
Db = require('./db.js'),
|
||||
globalServer, token, hacker_token, account_id;
|
||||
|
||||
describe('API /accounts', function() {
|
||||
|
||||
before( function(done) {
|
||||
globalServer = app.listen();
|
||||
token = Db.get_user_token();
|
||||
hacker_token = Db.get_hacker_token();
|
||||
account_id = Db.ACCOUNT_ID;
|
||||
Db.init(done);
|
||||
});
|
||||
|
||||
after( function() {
|
||||
globalServer.close();
|
||||
});
|
||||
|
||||
describe('* Creation', function() {
|
||||
it('should create an account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.send({
|
||||
name: 'Home',
|
||||
reference: '1234567890'
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.set('Accept', 'application/json')
|
||||
.expect(201)
|
||||
.expect('Content-Type', /json/)
|
||||
.end( function(error, result) {
|
||||
should.not.exist(error);
|
||||
var account = result.body;
|
||||
should.exist(account);
|
||||
account.name.should.be.equal('Home');
|
||||
account.reference.should.be.equal('1234567890');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create account without params', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400)
|
||||
.expect('Content-Type', /json/)
|
||||
.end( function(error, result) {
|
||||
var errors = result.body;
|
||||
should.exist(errors);
|
||||
errors.should.be.instanceof(Array).and.have.lengthOf(1);
|
||||
var error = errors[0];
|
||||
error.field.should.be.equal('name');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create account without valid token', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.send({
|
||||
name: 'Home',
|
||||
reference: '1234567890'
|
||||
})
|
||||
.set('Authorization', 'JWT fake')
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should fail to create account without token', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.send({
|
||||
name: 'Home',
|
||||
reference: '1234567890'
|
||||
})
|
||||
.expect(401, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Deletion', function() {
|
||||
it('should delete the given account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.send({
|
||||
name: 'Todelete',
|
||||
reference: '0987654321'
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var account_to_delete_id = result.body._id;
|
||||
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_to_delete_id)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.set('Accept', 'application/json')
|
||||
.expect(204, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to delete unknown account', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/4fc67871349bb7bf6a000002')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to delete invalid account', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/1')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to delete account for another user', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts')
|
||||
.send({
|
||||
name: 'Todelete',
|
||||
reference: '0987654321'
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var account_to_delete_id = result.body._id;
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_to_delete_id)
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(401, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Retrieve', function() {
|
||||
it('should retrieve the given account', function(done) {
|
||||
request(globalServer)
|
||||
.get('/api/accounts/' + account_id)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end( function(error, result) {
|
||||
should.not.exist(error);
|
||||
|
||||
var account = result.body;
|
||||
should.exist(account);
|
||||
account.name.should.be.equal('Default');
|
||||
account.reference.should.be.equal('1234567890');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should fail to retrieve an unknown account', function(done) {
|
||||
request(globalServer)
|
||||
.get('/api/accounts/4fc67871349bb7bf6a000002')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to retrieve an invalid account', function(done) {
|
||||
request(globalServer)
|
||||
.get('/api/accounts/1')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to retrieve the account for another user', function(done) {
|
||||
request(globalServer)
|
||||
.get('/api/accounts/' + account_id)
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(401, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Modify', function() {
|
||||
it('should modify the given account', function(done) {
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id)
|
||||
.send( {
|
||||
name: 'Home 2',
|
||||
reference: '0987654321'
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end(function(error, result) {
|
||||
should.not.exist(error);
|
||||
|
||||
var account = result.body;
|
||||
should.exist(account);
|
||||
account.name.should.be.equal('Home 2');
|
||||
account.reference.should.be.equal('0987654321');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify without arguments', function(done) {
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(400, done)
|
||||
});
|
||||
|
||||
it('should fail to modify missing arguments', function(done) {
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id)
|
||||
.send({reference: 'AZERTY'})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(400, done);
|
||||
});
|
||||
|
||||
it('should fail to modify invalid account', function(done) {
|
||||
request(globalServer)
|
||||
.put('/api/accounts/1')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done)
|
||||
});
|
||||
|
||||
it('should fail to modify account for another user', function(done) {
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id)
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(401, done)
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Entries', function() {
|
||||
describe('* Creation', function() {
|
||||
it('should create an entry with minimal data (DEPOSIT)' , function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
amount: 1000,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(201)
|
||||
.expect('Content-Type', /json/)
|
||||
.end(function(error, result) {
|
||||
should.not.exist(error);
|
||||
|
||||
var entry = result.body;
|
||||
should.exist(entry);
|
||||
entry.amount.should.be.equal(1000);
|
||||
new Date(entry.date).should.eql(new Date(2014, 11, 8));
|
||||
entry.type.should.be.equal('DEPOSIT');
|
||||
should.not.exist(entry.category);
|
||||
should.not.exist(entry.sub_category);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should create an entry with minimal data (BILL)' , function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: -1000,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(201)
|
||||
.expect('Content-Type', /json/)
|
||||
.end(function(error, result) {
|
||||
should.not.exist(error);
|
||||
|
||||
var entry = result.body;
|
||||
should.exist(entry);
|
||||
entry.amount.should.be.equal(-1000);
|
||||
new Date(entry.date).should.eql(new Date(2014, 11, 8));
|
||||
entry.type.should.be.equal('BILL');
|
||||
should.not.exist(entry.category);
|
||||
should.not.exist(entry.sub_category);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create entry without data', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(400, done);
|
||||
});
|
||||
|
||||
it('should fail to create entry for not owned account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: -1000,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should fail to create entry for not valid account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/1/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: -1000,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to create entry for unknown account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + token + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: -1000,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Modify', function() {
|
||||
it('should modify the given entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id + '/entries/' + entry_id)
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end( function(errors, result) {
|
||||
should.not.exist(errors);
|
||||
|
||||
var entry = result.body;
|
||||
should.exist(entry);
|
||||
entry.label.should.be.equal('modified');
|
||||
entry.amount.should.be.equal(55);
|
||||
new Date(entry.date).should.eql(new Date(2014,11,9));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify the given entry without data', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id + '/entries/' + entry_id)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(400, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify unknown entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id + '/entries/' + token)
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify invalid entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id + '/entries/1')
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify the given entry for unknown account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + token + '/entries/' + entry_id)
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify the given entry for invalid account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/1/entries/' + entry_id)
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to modify the given not owned entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.put('/api/accounts/' + account_id + '/entries/' + entry_id)
|
||||
.send({
|
||||
label: 'modified',
|
||||
amount: 55,
|
||||
date: new Date('2014-12-09')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(401, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Deletion', function() {
|
||||
it('should delete the given entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_id + '/entries/' + entry_id)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(204, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to delete an unknown entry', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_id + '/entries/' + token)
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to delete an invalid entry', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_id + '/entries/1')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(404, done);
|
||||
});
|
||||
|
||||
it('should fail to delete the not owned given entry', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/accounts/' + account_id + '/entries')
|
||||
.send({
|
||||
label: 'test',
|
||||
amount: 50,
|
||||
date: new Date('2014-12-08')
|
||||
})
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.end(function(error, result) {
|
||||
var entry_id = result.body._id;
|
||||
request(globalServer)
|
||||
.delete('/api/accounts/' + account_id + '/entries/' + entry_id)
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(401, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
119
test/db.js
Normal file
119
test/db.js
Normal file
@@ -0,0 +1,119 @@
|
||||
var mongoose = require('mongoose'),
|
||||
jwt = require('jsonwebtoken'),
|
||||
security = require('../config/security');
|
||||
|
||||
|
||||
var USER_ID = '55c9e2e3d300cc798928cc87',
|
||||
HACKER_ID = '55c9e2e4d300cc798928cc88',
|
||||
ACCOUNT_ID = '55c9e2fcd300cc798928cc8b';
|
||||
|
||||
var DATA = {
|
||||
User: [
|
||||
{
|
||||
_id: USER_ID,
|
||||
username: 'test',
|
||||
password: 's3cr3t'
|
||||
},
|
||||
{
|
||||
_id: HACKER_ID,
|
||||
username: 'hacker',
|
||||
password: 'bl4ckh4t'
|
||||
}
|
||||
],
|
||||
Account: [
|
||||
{
|
||||
_id: ACCOUNT_ID,
|
||||
name: 'Default',
|
||||
reference: '1234567890',
|
||||
user_id: USER_ID,
|
||||
categories: [{key: 'test', label: 'Test', sub_categories: []}]
|
||||
}
|
||||
],
|
||||
Entry: [
|
||||
{
|
||||
account_id: ACCOUNT_ID,
|
||||
label: 'Test bill',
|
||||
type: 'BILL',
|
||||
amount: -100,
|
||||
date: '2015-08-13',
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
process_collection = function(collection, data, done) {
|
||||
mongoose.connection.base.models[collection].find({}).remove(function(err) {
|
||||
if (err) {
|
||||
console.log('Can\'t delete collection ' + collection, err );
|
||||
}
|
||||
var res = [];
|
||||
for( var item in data ) {
|
||||
mongoose.connection.base.models[collection].create(data[item], function(err, newItem) {
|
||||
res.push(err);
|
||||
if( err ) {
|
||||
console.log('Can\'t insert document', err);
|
||||
}
|
||||
if (res.length === data.length) {
|
||||
return done();
|
||||
}
|
||||
|
||||
newItem.save(function(error) {
|
||||
res.push(err);
|
||||
if (res.length === data.length) {
|
||||
return done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
drop_collection = function(collection, data, done) {
|
||||
mongoose.connection.base.models[collection].find({}).remove(function(err) {
|
||||
if (err) {
|
||||
console.log('Can\'t delete collection ' + collection, err );
|
||||
}
|
||||
return done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
USER_ID: USER_ID,
|
||||
HACKER_ID: HACKER_ID,
|
||||
ACCOUNT_ID: ACCOUNT_ID,
|
||||
|
||||
init : function(done) {
|
||||
var collections_to_process = Object.keys(DATA).length,
|
||||
collectionsDone = 0;
|
||||
|
||||
for( var collection in DATA ) {
|
||||
process_collection(collection, DATA[collection], function() {
|
||||
collectionsDone++;
|
||||
if( collectionsDone === collections_to_process ) {
|
||||
done();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
drop : function(done) {
|
||||
var collections_to_process = Object.keys(DATA).length,
|
||||
collectionsDone = 0;
|
||||
|
||||
for( var collection in DATA ) {
|
||||
drop_collection(collection, DATA[collection], function() {
|
||||
collectionsDone++;
|
||||
if( collectionsDone === collections_to_process ) {
|
||||
done();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
get_user_token: function() {
|
||||
return jwt.sign( { user_id: USER_ID}, security.jwt.secretOrKey);
|
||||
},
|
||||
|
||||
get_hacker_token: function() {
|
||||
return jwt.sign( { user_id: HACKER_ID}, security.jwt.secretOrKey);
|
||||
}
|
||||
}
|
||||
23
test/server.js
Normal file
23
test/server.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var should = require('should'),
|
||||
request = require('supertest'),
|
||||
app = require('../server.js'),
|
||||
globalServer;
|
||||
|
||||
describe('Static resources', function(){
|
||||
|
||||
before(function () {
|
||||
globalServer = app.listen();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
globalServer.close();
|
||||
});
|
||||
|
||||
it('should send index.html', function(done){
|
||||
request(globalServer)
|
||||
.get('/')
|
||||
.set('Accept', 'text/html')
|
||||
.expect('Content-Type', /html/)
|
||||
.expect(200, done);
|
||||
})
|
||||
})
|
||||
198
test/users.js
Normal file
198
test/users.js
Normal file
@@ -0,0 +1,198 @@
|
||||
var should = require('should'),
|
||||
request = require('supertest'),
|
||||
app = require('../server.js'),
|
||||
mongoose = require('mongoose'),
|
||||
User = mongoose.model('User'),
|
||||
Db = require('./db.js'),
|
||||
sinon = require('sinon'),
|
||||
EventEmitter = require('../app/events/listeners'),
|
||||
globalServer, token, hacker_token, account_id, user_id;
|
||||
|
||||
describe('API /users', function() {
|
||||
|
||||
before(function(done) {
|
||||
globalServer = app.listen();
|
||||
token = Db.get_user_token();
|
||||
hacker_token = Db.get_hacker_token();
|
||||
account_id = Db.ACCOUNT_ID;
|
||||
user_id = Db.USER_ID;
|
||||
Db.init(done);
|
||||
});
|
||||
|
||||
after( function() {
|
||||
globalServer.close();
|
||||
});
|
||||
|
||||
describe('* Login', function() {
|
||||
|
||||
it('should log successfully', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users/login')
|
||||
.send({
|
||||
username: 'test',
|
||||
password: 's3cr3t'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.end( function(error, result) {
|
||||
should.not.exist(error);
|
||||
|
||||
var user = result.body;
|
||||
should.exist(user);
|
||||
user.username.should.be.equal('test');
|
||||
should.exist(user.token);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail login', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users/login')
|
||||
.send({
|
||||
username: 'test',
|
||||
password: 'secret'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should logout', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/users/login')
|
||||
.expect(200, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('* Registration', function() {
|
||||
|
||||
it('should fail without any params', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400)
|
||||
.end(function(err, result) {
|
||||
var errors = result.body;
|
||||
should.exist(errors);
|
||||
errors.should.be.instanceof(Array).and.have.lengthOf(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail without a password', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users')
|
||||
.send( { username: 'registration'})
|
||||
.expect(400, done);
|
||||
});
|
||||
|
||||
it('should fail without an username', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users')
|
||||
.send({password: 'secret'})
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400, done);
|
||||
});
|
||||
|
||||
it('should fail on duplicate account', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
username: 'test',
|
||||
password: 'secret'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect(409, done);
|
||||
});
|
||||
|
||||
it('should register successfully', function(done) {
|
||||
request(globalServer)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
username: 'registration',
|
||||
password: 'secret'
|
||||
})
|
||||
.set('Accept', 'application/json')
|
||||
.expect(201)
|
||||
.end(function(error, result) {
|
||||
|
||||
should.not.exist(error);
|
||||
var user = result.body;
|
||||
should.exist(user);
|
||||
user.username.should.be.equal('registration');
|
||||
should.exist(user.token);
|
||||
User.getAuthenticated('registration', 'secret', function(error, user) {
|
||||
should.not.exist(error);
|
||||
should.exist(user);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('* Deregistration', function() {
|
||||
it('should fail to delete user account without security token', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/users')
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should fail to delete user account with fake security token', function(done) {
|
||||
request(globalServer)
|
||||
.delete('/api/users')
|
||||
.set('Authorization', 'JWT fake_token')
|
||||
.expect(401, done);
|
||||
});
|
||||
|
||||
it('should delete user with accounts and entries', function(done) {
|
||||
var eventEmitter= EventEmitter.eventEmitter,
|
||||
spy_accounts = sinon.spy(),
|
||||
spy_entries = sinon.spy();
|
||||
|
||||
eventEmitter.on(EventEmitter.events.ACCOUNTS_DELETE_BY_USER_ID_EVT, spy_accounts);
|
||||
eventEmitter.on(EventEmitter.events.ENTRIES_DELETE_BY_ACCOUNT_EVT, spy_entries)
|
||||
|
||||
request(globalServer)
|
||||
.delete('/api/users')
|
||||
.set('Authorization', 'JWT ' + token)
|
||||
.expect(204)
|
||||
.end(function(error, result) {
|
||||
User.findOne({username: 'test'}, function(error, user) {
|
||||
should.not.exist(error);
|
||||
should.not.exist(user);
|
||||
|
||||
sinon.assert.calledWith(spy_accounts, user_id);
|
||||
spy_entries.called.should.equal.true;
|
||||
spy_entries.args[0][0].id.should.be.equal(account_id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete user without account', function(done) {
|
||||
var eventEmitter= EventEmitter.eventEmitter,
|
||||
spy_accounts = sinon.spy(),
|
||||
spy_entries = sinon.spy();
|
||||
|
||||
eventEmitter.on(EventEmitter.events.ACCOUNTS_DELETE_BY_USER_ID_EVT, spy_accounts);
|
||||
eventEmitter.on(EventEmitter.events.ENTRIES_DELETE_BY_ACCOUNT_EVT, spy_entries)
|
||||
|
||||
request(globalServer)
|
||||
.delete('/api/users')
|
||||
.set('Authorization', 'JWT ' + hacker_token)
|
||||
.expect(204)
|
||||
.end(function(error, result) {
|
||||
User.findOne({username: 'hacker'}, function(error, user) {
|
||||
should.not.exist(error);
|
||||
should.not.exist(user);
|
||||
|
||||
spy_accounts.called.should.equal.true;
|
||||
spy_entries.called.should.equal.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user