1
0
mirror of https://github.com/Foltik/Shimapan synced 2025-04-15 05:45:06 -04:00

Rewrite User tests to be async

This commit is contained in:
Jack Foltz 2018-07-25 18:45:38 -04:00
parent c246d3738a
commit 2e7ca23d9f
Signed by: foltik
GPG Key ID: 303F88F996E95541
5 changed files with 234 additions and 328 deletions

View File

@ -2,32 +2,35 @@
const express = require('express');
const router = express.Router();
const User = require('../models/User.js');
const Invite = require('../models/Invite.js');
const passport = require('passport');
const async = require('async');
const canonicalizeRequest = require('../util/canonicalize').canonicalizeRequest;
const requireAuth = require('../util/requireAuth').requireAuth;
function memoize(fn) {
let cache = {};
return async function() {
let args = JSON.stringify(arguments);
cache[args] = cache[args] || fn.apply(this, arguments);
return cache[args];
};
// Wraps an async middleware to catch promise rejection
function asyncMiddleware(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
}
}
const asyncMiddleware = fn =>
(req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next);
};
// Wraps passport.authenticate to return a promise
function authenticate(req, res, next) {
return new Promise((resolve) => {
passport.authenticate('local', (err, user) => {
resolve(user);
})(req, res, next);
});
}
// Normalizes, decomposes, and lowercases a utf-8 string
const canonicalizeUsername = username => username.normalize('NFKD').toLowerCase();
// Wraps passport session creation for async usage
function login(user, req) {
return new Promise((resolve) => {
req.login(user, resolve);
});
}
// Check if a canonical name is valid
async function validateUsername(username, canonicalName, sanitize) {
@ -61,113 +64,61 @@ async function validateInvite(code) {
return {valid: true, invite: invite};
}
// Authenticates and creates the required session variables
function setupSession(username, req, res, cb) {
// Body needs to contain canonical name for proper authentication
req.body.canonicalname = canonicalizeUsername(req.body.username);
passport.authenticate('local')(req, res, function () {
req.session.save(function (err) {
if (!err) {
req.session.passport.username = username;
req.session.passport.canonicalname = canonicalizeUsername(username);
}
cb(err);
});
});
}
router.post('/register', asyncMiddleware(async (req, res, next) => {
const reqUsername = req.body.username;
const reqPassword = req.body.password;
const reqInviteCode = req.body.invite;
const canonicalName = canonicalizeUsername(reqUsername);
// memoized verification functions
const checkInvite = memoize(async () => validateInvite(reqInviteCode));
const checkUsername = memoize(async () => validateUsername(reqUsername, canonicalName, req.sanitize));
router.post('/register', canonicalizeRequest, asyncMiddleware(async (req, res, next) => {
// Validate the invite and username
const [inviteStatus, usernameStatus] = await Promise.all([checkInvite(), checkUsername()]);
const [inviteStatus, usernameStatus] =
await Promise.all([
validateInvite(req.body.invite),
validateUsername(req.body.username, req.body.canonicalname, req.sanitize)
]);
// Make sure invite was valid
// Error if validation failed
if (!inviteStatus.valid)
return res.status(422).json({'message': inviteStatus.message});
// Make sure the username was valid
if (!usernameStatus.valid)
return res.status(422).json({'message': usernameStatus.message});
// Create the new user object
const user = new User({
username: reqUsername,
canonicalname: canonicalName,
scope: inviteStatus.invite.scope,
date: Date.now()
});
// memoized password setting, user saving, and invite updating functions
const updateInvite = memoize(async () =>
Invite.updateOne({code: inviteStatus.invite.code}, {recipient: canonicalName, used: Date.now()}));
const setPassword = memoize(async () => user.setPassword(reqPassword));
const saveUser = memoize(async () => {
await setPassword();
return user.save();
});
// Set the password, save the user, and update the invite code
await Promise.all([updateInvite(), setPassword(), saveUser()]);
// Update the database
await Promise.all([
User.register({
username: req.body.username,
canonicalname: req.body.canonicalname,
scope: inviteStatus.invite.scope,
date: Date.now()
}, req.body.password),
Invite.updateOne({code: inviteStatus.invite.code}, {recipient: req.body.canonicalname, used: Date.now()})
]);
res.status(200).json({'message': 'Registration successful.'});
}));
router.post('/login', function (req, res, next) {
// Take 'username' from the form and canonicalize it for authentication.
req.body.canonicalname = canonicalizeUsername(req.body.username);
router.post('/login', canonicalizeRequest, asyncMiddleware(async (req, res, next) => {
// Authenticate
const user = await authenticate(req, res, next);
if (!user)
return res.status(401).json({'message': 'Unauthorized.'});
async.waterfall([
function (cb) {
passport.authenticate('local', function(err, user, info) {
cb(err, user, info);
})(req, res, next);
},
function (user, info, cb) {
if (!user)
cb(info);
else
req.logIn(user, cb);
},
function (cb) {
req.session.passport.username = req.body.username;
req.session.passport.canonicalname = canonicalizeUsername(req.body.username);
cb();
}
], function (err) {
if (err)
res.status(401).json({'message': err});
else
res.status(200).json({'message': 'Login successful.'});
});
});
// Create session
await login(user, req);
// Set scope
req.session.passport.scope = user.scope;
res.status(200).json({'message': 'Logged in.'});
}));
router.get('/logout', function (req, res) {
req.logout();
res.status(200).json({'message': 'Logged out.'});
});
router.get('/session', function (req, res) {
console.log(req.session.passport);
if (req.session.passport.canonicalname) {
User.findOne({canonicalname: req.session.passport.canonicalname}, function (err, user) {
res.status(200).json({
username: user.username,
canonicalname: user.canonicalname,
scope: user.scope
});
});
} else {
res.status(401).json({'message': 'Unauthorized.'});
}
router.get('/session', requireAuth, (req, res, next) => {
res.status(200).json({
username: req.session.passport.username,
canonicalname: req.session.passport.canonicalname,
scope: req.session.passport.scope
});
});
module.exports = router;

8
app/util/canonicalize.js Normal file
View File

@ -0,0 +1,8 @@
// Normalizes, decomposes, and lowercases a utf-8 string
exports.canonicalize = (username) => username.normalize('NFKD').toLowerCase();
exports.canonicalizeRequest =
(req, res, next) => {
req.body.canonicalname = exports.canonicalize(req.body.username);
next();
};

7
app/util/requireAuth.js Normal file
View File

@ -0,0 +1,7 @@
exports.requireAuth =
(req, res, next) => {
if (req.isAuthenticated())
next();
else
res.status(401).json({'message:': 'Unauthorized.'});
};

View File

@ -1,140 +1,165 @@
process.env.NODE_ENV = 'test';
var async = require('async');
const app = require('../server');
const server = app.server;
var mongoose = require('mongoose');
var User = require('../app/models/User.js');
var Invite = require('../app/models/Invite.js');
var Upload = require('../app/models/Upload.js');
const User = require('../app/models/User.js');
const Invite = require('../app/models/Invite.js');
const Upload = require('../app/models/Upload.js');
var chai = require('chai');
var should = chai.should();
var app = require('../server');
var server = app.server;
var util = require('./testUtil.js');
before(util.resetDatabase);
const util = require('./testUtil.js');
const canonicalize = require('../app/util/canonicalize').canonicalize;
describe('Users', function() {
describe('/POST register', function() {
describe('0 Well Formed Requests', function() {
beforeEach((done) => {
async.series([
util.resetDatabase,
util.createTestInvite
], done);
});
beforeEach(async () => util.clearDatabase());
it('MUST register a valid user with a valid invite', function(done) {
util.verifySuccessfulRegister({username: 'user', password: 'pass', invite: 'code'}, done);
});
describe('/POST register', () => {
describe('0 Valid Request', () => {
async function verifySuccessfulRegister(user) {
await util.createTestInvite();
it('MUST register a username with unicode symbols and a valid invite', function(done) {
util.verifySuccessfulRegister({username: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'}, done);
})
const res = await util.registerUser(user);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Registration successful.');
const userCount = await User.countDocuments({username: user.username});
userCount.should.eql(1);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.username)});
inviteCount.should.eql(1);
}
it('MUST register a valid user with a valid invite', async () =>
verifySuccessfulRegister({username: 'user', password: 'pass', invite: 'code'})
);
it('MUST register a username with unicode symbols and a valid invite', async () =>
verifySuccessfulRegister({username: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'})
);
});
describe('1 Invalid Invites', function() {
beforeEach(util.resetDatabase);
describe('1 Invalid Invites', () => {
async function verifyRejectedInvite(invite, message) {
const user = {username: 'user', password: 'pass', invite: 'code'};
if (invite) {
await util.createInvite(invite);
user.invite = invite.code;
}
const verifyRejectedInvite = function(invite, message, done) {
const user = {username: 'user', password: 'pass', invite: invite && invite.code ? invite.code : 'code'};
const create = invite ? util.createInvite : (invite, cb) => cb();
async.series([
(cb) => create(invite, cb),
(cb) => util.verifyFailedRegister(user, message, 422, cb)
], done);
};
const res = await(util.registerUser(user));
res.should.have.status(422);
res.body.should.be.a('object');
res.body.should.have.property('message').eql(message);
}
it('MUST NOT register a nonexistant invite', function(done) {
verifyRejectedInvite(null, 'Invalid invite code.', done);
});
it('MUST NOT register a nonexistant invite', async () =>
verifyRejectedInvite(null, 'Invalid invite code.')
);
it('MUST NOT register a used invite', function(done) {
verifyRejectedInvite({used: new Date()}, 'Invite already used.', done);
});
it('MUST NOT register a used invite', async () =>
verifyRejectedInvite({used: new Date()}, 'Invite already used.')
);
it('MUST NOT register an expired invite', function(done) {
verifyRejectedInvite({exp: new Date()}, 'Invite expired.', done);
})
it('MUST NOT register an expired invite', async () =>
verifyRejectedInvite({exp: new Date()}, 'Invite expired.')
);
});
describe('2 Invalid Usernames', function() {
beforeEach((done) => {
async.series([
util.resetDatabase,
(cb) => util.createTestInvites(3, cb)
], done);
});
describe('2 Invalid Usernames', () => {
async function verifyRejectedUsername(user, message) {
const res = await util.registerUser(user);
res.should.have.status(422);
res.body.should.be.a('object');
res.body.should.have.property('message').eql(message);
}
it('MUST NOT register a duplicate username', function(done) {
it('MUST NOT register a duplicate username', async () => {
await util.createTestInvites(2);
const user0 = {username: 'user', password: 'pass', invite: 'code0'};
const user1 = {username: 'user', password: 'diff', invite: 'code1'};
async.series([
(cb) => util.verifySuccessfulRegister(user0, cb),
(cb) => util.verifyFailedRegister(user1, 'Username in use.', 422, cb)
], done);
await util.registerUser(user0);
return verifyRejectedUsername(user1, 'Username in use.');
});
it('MUST NOT register a username with a duplicate canonical name', function(done) {
it('MUST NOT register a username with a duplicate canonical name', async () => {
await util.createTestInvites(2);
const user0 = {username: 'bigbird', password: 'pass', invite: 'code0'};
const user1 = {username: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'diff', invite: 'code1'};
async.series([
(cb) => util.verifySuccessfulRegister(user0, cb),
(cb) => util.verifyFailedRegister(user1, 'Username in use.', 422, cb)
], done);
await util.registerUser(user0);
return verifyRejectedUsername(user1, 'Username in use.');
});
it('MUST NOT register a username containing whitespace', function(done) {
it('MUST NOT register a username containing whitespace', async () => {
await util.createTestInvites(3);
const users = [
{username: 'user name', password: 'pass', invite: 'code0'},
{username: 'user name', password: 'pass', invite: 'code1'},
{username: 'user name', password: 'pass', invite: 'code2'}
];
const failMsg = 'Username contains invalid characters.';
async.each(users, (user, cb) => util.verifyFailedRegister(user, failMsg, 422, cb), done);
return Promise.all(users.map(user => verifyRejectedUsername(user, failMsg)));
});
it('MUST NOT register a username containing HTML', function(done) {
const user = {username: 'user<svg/onload=alert("XSS")>', password: 'pass', invite: 'code0'};
util.verifyFailedRegister(user, 'Username contains invalid characters.', 422, done);
it('MUST NOT register a username containing HTML', async () => {
await util.createTestInvite();
const user = {username: 'user<svg/onload=alert("XSS")>', password: 'pass', invite: 'code'};
return verifyRejectedUsername(user, 'Username contains invalid characters.');
});
it('MUST NOT register a username with too many characters', function(done) {
const user = {username: '123456789_123456789_123456789_1234567', password: 'pass', invite: 'code0'};
util.verifyFailedRegister(user, 'Username too long.', 422, done);
it('MUST NOT register a username with too many characters', async () => {
await util.createTestInvite();
const user = {username: '123456789_123456789_123456789_1234567', password: 'pass', invite: 'code'};
return verifyRejectedUsername(user, 'Username too long.');
})
});
});
describe('/POST login', function() {
it('SHOULD accept valid user, valid password', function(done) {
util.verifySuccessfulLogin({
username: 'TestUser1',
password: 'TestPassword'
}, done);
describe('/POST login', () => {
async function verifySuccessfulLogin(credentials) {
const res = await util.login(credentials);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Logged in.');
}
async function verifyFailedLogin(credentials) {
const res = await util.login(credentials);
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Unauthorized.');
}
describe('0 Valid Request', () => {
it('SHOULD accept a valid user with a valid password', async () => {
await util.createTestUser();
return verifySuccessfulLogin({username: 'user', password: 'pass'});
});
});
it('SHOULD NOT accept valid user, invalid password', function(done) {
util.verifyFailedPasswordLogin({
username: 'TestUser1',
password: 'bogus'
}, done);
describe('1 Invalid Password', () => {
it('SHOULD NOT accept an invalid password', async () => {
await util.createTestUser();
return verifyFailedLogin({username: 'user', password: 'bogus'});
});
});
it('SHOULD NOT accept invalid user, any password', function(done) {
util.verifyFailedUsernameLogin({
username: 'BogusTestUser',
password: 'bogus'
}, done);
describe('2 Invalid User', () => {
it('SHOULD NOT accept an invalid user', async () =>
verifyFailedLogin({username: 'bogus', password: 'bogus'})
);
});
});
});
describe('Uploads', function() {
/*describe('Uploads', function() {
describe('/POST upload', function() {
it('SHOULD accept logged in valid upload', function(done) {
util.verifySuccessfulUpload({
@ -161,10 +186,6 @@ describe('Uploads', function() {
}, done);
})
});
});
});*/
after(function() {
server.close(function() {
process.exit();
});
});
after(() => server.close(() => process.exit(0)));

View File

@ -1,127 +1,72 @@
process.env.NODE_ENV = 'test';
var async = require('async');
var mongoose = require('mongoose');
var User = require('../app/models/User.js');
var Invite = require('../app/models/Invite.js');
var Upload = require('../app/models/Upload.js');
var chai = require('chai');
var http = require('chai-http');
var app = require('../server');
var server = app.server;
var db = app.db;
var should = chai.should;
var expect = chai.expect;
const User = require('../app/models/User.js');
const Invite = require('../app/models/Invite.js');
const Upload = require('../app/models/Upload.js');
const chai = require('chai');
const http = require('chai-http');
chai.use(http);
const should = chai.should();
const app = require('../server');
const server = app.server;
//TODO: REMOVE
const async = require('async');
const canonicalize = require("../app/util/canonicalize").canonicalize;
//TODO: BAD! Move to a util file!
// Normalizes, decomposes, and lowercases a utf-8 string
const canonicalizeUsername = username => username.normalize('NFKD').toLowerCase();
//---------------- DATABASE UTIL ----------------//
var resetDatabase = function(cb) {
async.each(
[User, Invite, Upload],
(schema, cb) => schema.remove({}, cb),
cb);
};
exports.clearDatabase = async () =>
Promise.all([
User.remove({}),
Invite.remove({}),
Upload.remove({})
]);
const createInvite = function(invite, done) {
//---------------- API ROUTES ----------------//
exports.login = async (credentials) =>
chai.request(server)
.post('/api/auth/login')
.send(credentials);
exports.createInvite = async (invite) => {
if (!invite.code) invite.code = 'code';
if (!invite.scope) invite.scope = ['test.perm', 'file.upload'];
if (!invite.issuer) invite.issuer = 'Mocha';
if (!invite.issued) invite.issued = new Date();
Invite.create(invite, done);
return Invite.create(invite);
};
const createInvites = function(invites, done) {
async.each(invites, createInvite, done);
};
var createTestInvite = function(done) {
createInvite({code: 'code'}, done);
};
var createTestInvites = function(n, done) {
const codes = Array.from(new Array(n), (val, index) => 'code' + index);
async.each(codes, (code, cb) => createInvite({code: code}, cb), done);
};
//---------------- REGISTER UTIL ----------------//
const register = function(user, cb) {
chai.request(server)
exports.registerUser = async (user) => {
if (!user.username) user.username = 'user';
if (!user.password) user.password = 'pass';
if (!user.invite) user.invite = 'code';
return chai.request(server)
.post('/api/auth/register')
.send(user)
.end(cb);
.send(user);
};
const verifySuccessfulRegister = function(user, done) {
register(user, function(err, res) {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Registration successful.');
User.countDocuments({username: user.username}, function(err, count) {
count.should.eql(1);
Invite.countDocuments({recipient: canonicalizeUsername(user.username)}, function(err, count) {
count.should.eql(1);
done();
});
});
});
//---------------- TEST ENTRY CREATION ----------------//
exports.createTestInvite = async () =>
exports.createInvite({});
exports.createTestInvites = async (n) => {
const codes = Array.from(new Array(n), (val, index) => 'code' + index);
return Promise.all(codes.map(code => exports.createInvite({code: code})));
};
const verifyFailedRegister = function(user, message, status, done) {
register(user, function(err, res) {
res.should.have.status(status);
res.body.should.be.a('object');
res.body.should.have.property('message').eql(message);
done();
})
exports.createTestUser = async () => {
await exports.createTestInvite();
return exports.registerUser({});
};
//---------------- LOGIN UTIL ----------------//
var login = function(user, cb) {
chai.request(server)
.post('/api/auth/login')
.send(user)
.end(cb);
};
var verifySuccessfulLogin = function(user, done) {
login(user, function(err, res) {
res.should.have.status(200);
done();
});
};
var verifyFailedUsernameLogin = function(user, done) {
login(user, function(err, res) {
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Invalid username.');
done();
});
};
var verifyFailedPasswordLogin = function(user, done) {
login(user, function(err, res) {
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Invalid password.');
done();
});
};
//---------------- UPLOAD UTIL ----------------//
//---------------- UPLOAD API ----------------//
var upload = function(token, file, cb) {
chai.request(server)
@ -204,29 +149,3 @@ var verifyFailedAuthUpload = function(done) {
done();
});
};
module.exports = {
resetDatabase: resetDatabase,
createInvite: createInvite,
createInvites: createInvites,
createTestInvite: createTestInvite,
createTestInvites: createTestInvites,
register: register,
verifySuccessfulRegister: verifySuccessfulRegister,
verifyFailedRegister: verifyFailedRegister,
login: login,
verifySuccessfulLogin: verifySuccessfulLogin,
verifyFailedUsernameLogin: verifyFailedUsernameLogin,
verifyFailedPasswordLogin: verifyFailedPasswordLogin,
upload: upload,
loginUpload: loginUpload,
verifySuccessfulUpload: verifySuccessfulUpload,
verifyFailedAuthUpload: verifyFailedAuthUpload,
verifyFailedPermissionUpload: verifyFailedPermissionUpload,
verifyFailedSizeUpload: verifyFailedSizeUpload
};