1
0
mirror of https://github.com/Foltik/Shimapan synced 2024-11-15 17:18:05 -05:00
shimapan/test/api.js
2018-12-26 11:20:26 -05:00

1066 lines
47 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

process.env.NODE_ENV = 'test';
const chai = require('chai');
chai.use(require('chai-http'));
const should = chai.should();
const describe = require('mocha').describe;
const ModelPath = '../app/models/';
const User = require(ModelPath + 'User.js');
const Upload = require(ModelPath + 'Upload.js');
const Key = require(ModelPath + 'Key.js');
const Invite = require(ModelPath + 'Invite.js');
const View = require(ModelPath + 'View.js');
const util = require('./testUtil.js');
const canonicalize = require('../app/util/canonicalize').canonicalize;
const config = require('config');
let app;
let server;
let agent;
before(() => {
const main = require('../server.js');
app = main.app;
server = main.server;
agent = chai.request.agent(app);
});
after(() => {
server.close();
});
beforeEach(() => util.clearDatabase());
describe('Authentication', () => {
describe('/POST register', () => {
describe('0 Valid Request', () => {
async function verifySuccessfulRegister(user) {
await util.createTestInvite();
const res = await util.registerUser(user, agent);
util.verifyResponse(res, 200, 'Registration successful.');
const userCount = await User.countDocuments({displayname: user.displayname});
userCount.should.equal(1, 'The user should have be created in the database');
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
inviteCount.should.equal(1, 'The invite should be marked as used by the user');
}
it('MUST register a valid user with a valid invite', async () =>
verifySuccessfulRegister({displayname: 'user', password: 'pass', invite: 'code'})
);
it('MUST register a username with unicode symbols and a valid invite', async () =>
verifySuccessfulRegister({displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'})
);
});
describe('1 Invalid Invites', () => {
async function verifyRejectedInvite(invite, message) {
const user = {displayname: 'user', password: 'pass', invite: 'code'};
if (invite) {
await util.insertInvite(invite, agent);
user.invite = invite.code;
}
const res = await(util.registerUser(user, agent));
util.verifyResponse(res, 422, message);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
inviteCount.should.equal(0, 'Invite should not be marked as used or received by the user');
}
it('MUST NOT register a nonexistant invite', async () =>
verifyRejectedInvite(null, 'Invalid invite code.')
);
it('MUST NOT register a used invite', async () =>
verifyRejectedInvite({code: 'code', used: new Date(), issuer: 'Mocha'}, 'Invite already used.')
);
it('MUST NOT register an expired invite', async () =>
verifyRejectedInvite({code: 'code', expires: new Date(), issuer: 'Mocha'}, 'Invite expired.')
);
});
describe('2 Invalid Displaynames', () => {
async function verifyRejectedUsername(user, code, message) {
const res = await util.registerUser(user, agent);
util.verifyResponse(res, code, message);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
inviteCount.should.equal(0, 'The invite should not be inserted into the database after rejection');
}
it('MUST NOT register a duplicate username', async () => {
await util.createTestInvites(2);
const user0 = {displayname: 'user', password: 'pass', invite: 'code0'};
const user1 = {displayname: 'user', password: 'diff', invite: 'code1'};
await util.registerUser(user0, agent);
return verifyRejectedUsername(user1, 422, 'Username in use.');
});
it('MUST NOT register a username with a duplicate canonical name', async () => {
await util.createTestInvites(2);
const user0 = {displayname: 'bigbird', password: 'pass', invite: 'code0'};
const user1 = {displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'diff', invite: 'code1'};
await util.registerUser(user0, agent);
return verifyRejectedUsername(user1, 422, 'Username in use.');
});
it('MUST NOT register a username containing whitespace', async () => {
await util.createTestInvites(3);
const users = [
{displayname: 'user name', password: 'pass', invite: 'code0'},
{displayname: 'user name', password: 'pass', invite: 'code1'},
{displayname: 'user name', password: 'pass', invite: 'code2'}
];
const failMsg = 'displayname contains invalid characters.';
return Promise.all(users.map(user => verifyRejectedUsername(user, 400, failMsg)));
});
it('MUST NOT register a username containing HTML', async () => {
await util.createTestInvite();
const user = {displayname: 'user<svg/onload=alert("XSS")>', password: 'pass', invite: 'code'};
return verifyRejectedUsername(user, 400, 'displayname contains invalid characters.');
});
it('MUST NOT register a username with too many characters', async () => {
await util.createTestInvite();
const user = {displayname: '123456789_123456789_123456789_1234567', password: 'pass', invite: 'code'};
return verifyRejectedUsername(user, 400, 'displayname too long.');
});
});
});
describe('/POST login', () => {
async function verifySuccessfulLogin(credentials) {
// Login with the agent
const res = await util.login(credentials, agent);
util.verifyResponse(res, 200, 'Logged in.');
res.should.have.cookie('session.id');
// Get /api/auth/whoami, which can only be viewed when logged in
const whoami = await util.whoami(agent);
whoami.should.have.status(200);
}
async function verifyFailedLogin(credentials) {
const res = await util.login(credentials, agent);
util.verifyResponse(res, 401, 'Unauthorized.');
}
describe('0 Valid Request', () => {
it('SHOULD accept a valid user with a valid password', async () => {
await util.createTestUser(agent);
return verifySuccessfulLogin({displayname: 'user', password: 'pass'});
});
it('SHOULD accept a username instead of a displayname', async () => {
await util.createTestUser(agent);
return verifySuccessfulLogin({username: 'user', password: 'pass'});
});
it('SHOULD accept any non-normalized variant of a username with a valid password', async () => {
await util.createTestInvite();
await util.registerUser({displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'}, agent);
return verifySuccessfulLogin({displayname: 'BiGbIrD', password: 'pass'});
});
});
describe('1 Invalid Password', () => {
it('SHOULD NOT accept an invalid password', async () => {
await util.createTestUser(agent);
return verifyFailedLogin({displayname: 'user', password: 'bogus'});
});
});
describe('2 Invalid User', () => {
it('SHOULD NOT accept an invalid user', async () =>
verifyFailedLogin({displayname: 'bogus', password: 'bogus'})
);
});
});
describe('/POST logout', () => {
async function verifyNoSession() {
const res = await util.whoami(agent);
util.verifyResponse(res, 401, 'Unauthorized.');
}
describe('0 Valid Request', () => {
it('must logout a user with a session', async () => {
await util.createTestSession(agent);
const res = await util.logout(agent);
util.verifyResponse(res, 200, 'Logged out.');
return verifyNoSession();
});
});
describe('1 Invalid Session', () => {
it('must not logout a user without a session', async () => {
const res = await util.logout(agent);
util.verifyResponse(res, 400, 'Not logged in.');
return verifyNoSession();
});
});
});
describe('/POST whoami', () => {
function verifyWhoami(res, username, displayname, scope, key) {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('username').equal(username);
res.body.should.have.property('displayname').equal(displayname);
res.body.should.have.property('scope').deep.equal(scope);
res.body.should.have.property('key').equal(key);
}
describe('0 Valid Request', () => {
it('must respond with a valid session', async () => {
await util.createTestSession(agent);
const res = await util.whoami(agent);
verifyWhoami(res, 'user', 'user', ['file.upload'], null);
return util.logout(agent);
});
it('must respond with a valid api key', async () => {
await util.createTestKey(['file.upload']);
const res = await util.whoami(agent, 'key');
verifyWhoami(res, 'Mocha', 'Mocha', ['file.upload'], 'key');
});
});
describe('1 Invalid Auth', () => {
it('must not respond with an invalid session', async () => {
const res = await util.whoami(agent);
util.verifyResponse(res, 401, 'Unauthorized.');
});
it('must not respond with a banned user with a valid session', async () => {
await util.createTestSession(agent);
await util.setBanned('user', true);
const res = await util.whoami(agent);
util.verifyResponse(res, 403, 'Forbidden.');
});
it('must not respond with a banned users api key', async () => {
await util.createTestUser(agent);
await Promise.all([
util.setBanned('user', true),
util.insertKey({key: 'key', identifier: 'test', scope: ['file.upload'], issuer: 'user'})
]);
const res = await util.whoami(agent, 'key');
util.verifyResponse(res, 403, 'Forbidden.');
});
});
});
});
describe('Uploading', () => {
after(async () => util.clearDirectory(config.get('Upload.path')));
describe('/POST upload', () => {
async function verifySuccessfulUpload(file, key) {
// Get file stats beforehand
const fileHash = await util.fileHash(file);
// Submit the upload and verify the result
const res = await util.upload(file, agent, key);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('url');
const idLength = config.get('Upload.idLength');
res.body.should.have.property('uid').length(idLength, 'The UID should be a ' + idLength + ' letter lowercase string.');
// Find the uploaded file in the database
const upload = await Upload.findOne({uid: res.body.uid}, {_id: 0, uid: 1, file: 1});
const uploadFile = upload.file.path;
upload.should.be.a('object');
upload.uid.should.equal(res.body.uid, 'The uploaded file in the database should exist and match the reponse ID.');
// Verify the uploaded file is the same as the file now on disk
const uploadHash = await util.fileHash(uploadFile);
uploadHash.should.equal(fileHash, 'The uploaded file and the file on disk should have matching hashes.');
}
async function verifySuccessfulUserUpload(file, username) {
// Get the user's stats beforehand
const userBefore = await User.findOne({username: username}, {_id: 0, uploadCount: 1, uploadSize: 1});
await verifySuccessfulUpload(file);
// Verify the user's stats have been updated correctly
const userAfter = await User.findOne({username: username}, {_id: 0, uploadCount: 1, uploadSize: 1});
const fileSize = await util.fileSize(file);
userAfter.uploadCount.should.equal(userBefore.uploadCount + 1, 'The users upload count should be incremented.');
userAfter.uploadSize.should.equal(userBefore.uploadSize + fileSize, 'The users upload size should be properly increased.');
}
async function verifySuccessfulKeyUpload(file, key) {
// Get the key's stats beforehand
const keyBefore = await Key.findOne({key: key}, {_id: 0, uploadCount: 1, uploadSize: 1});
await verifySuccessfulUpload(file, key);
// Verify the key's stats have been updated correctly
const keyAfter = await Key.findOne({key: key}, {_id: 0, uploadCount: 1, uploadSize: 1});
const fileSize = await util.fileSize(file);
keyAfter.uploadCount.should.equal(keyBefore.uploadCount + 1, 'The keys upload count should be incremented.');
keyAfter.uploadSize.should.equal(keyBefore.uploadSize + fileSize, 'The keys upload size should be properly increased');
}
async function verifyFailedUpload(file, status, message, key) {
const fileCountBefore = await util.directoryFileCount(config.get('Upload.path'));
const uploadCountBefore = await Upload.countDocuments({});
const res = await util.upload(file, agent, key);
util.verifyResponse(res, status, message);
const fileCountAfter = await util.directoryFileCount(config.get('Upload.path'));
fileCountAfter.should.equal(fileCountBefore, 'File should not be written to disk');
const uploadCountAfter = await Upload.countDocuments({});
uploadCountAfter.should.equal(uploadCountBefore, 'No uploads should be written to the database');
}
describe('0 Valid Request', () => {
it('SHOULD accept an upload from a valid session', async () => {
await Promise.all([
util.createTestSession(agent),
util.createTestFile(2048, 'test.bin')
]);
await verifySuccessfulUserUpload('test.bin', 'user');
return Promise.all([
util.logout(agent),
util.deleteFile('test.bin')
]);
});
it('SHOULD accept an upload from a valid api key', async () => {
await Promise.all([
util.createTestKey(['file.upload']),
util.createTestFile(2048, 'test.bin')
]);
await verifySuccessfulKeyUpload('test.bin', 'key');
return util.deleteFile('test.bin');
})
});
describe('1 Invalid Authentication', () => {
it('SHOULD NOT accept an unauthenticated request', async () => {
await util.createTestFile(2048, 'test.bin');
await verifyFailedUpload('test.bin', 401, 'Unauthorized.');
return util.deleteFile('test.bin');
});
it('SHOULD NOT accept a session request without file.upload scope', async () => {
await util.insertInvite({code: 'code', scope: [], issuer: 'Mocha'});
await util.registerUser({displayname: 'user', password: 'pass', invite: 'code'}, agent);
await util.login({displayname: 'user', password: 'pass'}, agent);
await util.createTestFile(2048, 'test.bin');
await verifyFailedUpload('test.bin', 403, 'Forbidden.');
return Promise.all([
util.logout(agent),
util.deleteFile('test.bin')
]);
});
it('SHOULD NOT accept a key request without file.upload scope', async () => {
await Promise.all([
util.createTestKey([]),
util.createTestFile(2048, 'test.bin')
]);
await verifyFailedUpload('test.bin', 403, 'Forbidden.', 'key');
return util.deleteFile('test.bin');
})
});
describe('3 Invalid File', () => {
before(() => util.createTestFile(config.get('Upload.maxSize') + 1024, 'large.bin'));
after(() => util.deleteFile('large.bin'));
it('SHOULD NOT accept a too large file', async () => {
await util.createTestSession(agent);
await verifyFailedUpload('large.bin', 413, 'File too large.');
return util.logout(agent);
});
});
describe('4 Malformed Request', () => {
it('SHOULD NOT accept a request with no file attached', async () => {
await util.createTestSession(agent);
await verifyFailedUpload(null, 400, 'Bad request.');
return util.logout(agent);
});
it('must only accept one file from a request with multiple files attached', async () => {
await Promise.all([
util.createTestFile(2048, 'test1.bin'),
util.createTestFile(2048, 'test2.bin'),
util.createTestSession(agent)
]);
const fileCountBefore = await util.directoryFileCount(config.get('Upload.path'));
const uploadCountBefore = await Upload.countDocuments({});
const res = await agent.post('/api/upload')
.attach('file', 'test1.bin', 'test1.bin')
.attach('file1', 'test2.bin', 'test2.bin');
util.verifyResponse(res, 200, 'File uploaded.');
const fileCountAfter = await util.directoryFileCount(config.get('Upload.path'));
fileCountAfter.should.equal(fileCountBefore + 1, 'Only one file should be written to the disk');
const uploadCountAfter = await Upload.countDocuments({});
uploadCountAfter.should.equal(uploadCountBefore + 1, 'Only one upload should be written to the database');
return Promise.all([
util.deleteFile('test1.bin'),
util.deleteFile('test2.bin')
]);
})
})
});
});
describe('Viewing', () => {
async function verifyView(file, uid, disposition) {
const uploadViewsBefore = (await Upload.findOne({uid: uid})).views;
const viewsBefore = await View.countDocuments();
const res = await util.view(uid, agent)
.parse(util.binaryFileParser);
res.should.have.status(200);
res.should.have.header('content-disposition', disposition);
const [uploadHash, downloadHash] = await Promise.all([
util.fileHash(file),
util.bufferHash(res.body)
]);
downloadHash.should.equal(uploadHash, 'Uploaded file and downloaded hash should match');
const viewsAfter = await View.countDocuments();
const uploadViewsAfter = (await Upload.findOne({uid: uid})).views;
uploadViewsAfter.should.equal(uploadViewsBefore + 1, 'The files views should be incremented.');
viewsAfter.should.equal(viewsBefore + 1, 'A view object should have been inserted to the database.');
}
it('must return an uploaded binary file', async () => {
await Promise.all([
util.createTestSession(agent),
util.createTestFile(2048, 'test.bin')
]);
const upload = await util.upload('test.bin', agent);
await verifyView('test.bin', upload.body.uid, 'attachment; filename="test.bin"');
return util.deleteFile('test.bin');
});
it('must return an uploaded image file inline', async () => {
await Promise.all([
util.createTestSession(agent),
util.createTestFile(2048, 'test.jpg')
]);
const upload = await util.upload('test.jpg', agent);
await verifyView('test.jpg', upload.body.uid, 'inline');
return util.deleteFile('test.jpg');
});
it('must return an error when file not found', async () => {
const res = await util.view('abcdef', agent);
util.verifyResponse(res, 404, 'File not found.');
});
});
describe('Invites', () => {
describe('/POST create', () => {
async function verifyCreatedInvite(invite) {
const res = await util.createInvite(invite, agent);
util.verifyResponse(res, 200, 'Invite created.');
res.body.should.have.property('code').match(/^[A-Fa-f0-9]+$/, 'The invite should be a hex string.');
const dbInvite = await Invite.findOne({code: res.body.code});
dbInvite.should.not.equal(null);
dbInvite.scope.should.deep.equal(invite.scope, 'The created invites scope should match the request.');
dbInvite.issuer.should.equal('user');
}
describe('0 Valid Request', () => {
it('SHOULD create an invite with valid scope from a valid session', async () => {
await util.createSession(agent, ['invite.create', 'file.upload']);
return verifyCreatedInvite({scope: ['file.upload']});
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT create in invite without invite.create scope', async () => {
await util.createSession(agent, ['file.upload']);
const res = await util.createInvite({scope: ['file.upload']}, agent);
util.verifyResponse(res, 403, 'Forbidden.');
});
it('SHOULD NOT create an invite with a scope exceeding the requesters', async () => {
await util.createSession(agent, ['invite.create', 'file.upload']);
const res = await util.createInvite({scope: ['user.ban']}, agent);
util.verifyResponse(res, 403, 'Requested scope exceeds own scope.');
});
});
});
describe('/POST delete', () => {
async function verifyDeletedInvite(code) {
const res = await util.deleteInvite(code, agent);
util.verifyResponse(res, 200, 'Invite deleted.');
const inviteCount = await Invite.countDocuments({code: code});
inviteCount.should.equal(0, 'The invite should be removed from the database.');
}
describe('0 Valid Request', () => {
it('SHOULD delete an invite with valid permission from a valid session', async () => {
await util.createSession(agent, ['invite.create', 'invite.delete', 'file.upload']);
const res = await util.createInvite({scope: ['file.upload']}, agent);
return verifyDeletedInvite(res.body.code);
});
it('SHOULD delete another users invite with invite.delete.others scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
const invite = await util.createInvite({scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['invite.create', 'invite.delete', 'invite.delete.others'], 'eve');
return verifyDeletedInvite(invite.body.code);
});
it('SHOULD delete a usedinvite with invite.delete.used scope', async () => {
await util.createSession(agent, ['invite.create', 'invite.delete', 'invite.delete.used', 'file.upload'], 'alice');
const invite = await util.createInvite({scope: ['file.upload']}, agent);
await util.registerUser({displayname: 'bob', password: 'hunter2', invite: invite.body.code}, agent);
return verifyDeletedInvite(invite.body.code);
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT delete an invite without invite.delete scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload']);
const invite = await util.createInvite({scope: ['file.upload']}, agent);
const res = await util.deleteInvite(invite.body.code, agent);
util.verifyResponse(res, 403, 'Forbidden.');
});
it('SHOULD NOT delete another users invite without invite.delete.others scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
const invite = await util.createInvite({scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['invite.create', 'invite.delete'], 'eve');
const res = await util.deleteInvite(invite.body.code, agent);
util.verifyResponse(res, 422, 'Invite not found.');
});
it('SHOULD NOT delete a used invite without invite.delete.used scope', async () => {
await util.createSession(agent, ['invite.create', 'invite.delete', 'file.upload'], 'alice');
const invite = await util.createInvite({scope: ['file.upload']}, agent);
await util.registerUser({displayname: 'bob', password: 'hunter2', invite: invite.body.code}, agent);
const res = await util.deleteInvite(invite.body.code, agent);
util.verifyResponse(res, 403, 'Forbidden to delete used invites.');
});
});
describe('2 Invalid Code', () => {
it('SHOULD return an error when the invite is not found', async () => {
await util.createSession(agent, ['invite.delete']);
const res = await util.deleteInvite('bogus', agent);
util.verifyResponse(res, 422, 'Invite not found.');
});
});
});
describe('/POST get', () => {
async function verifyInviteSearch(codes) {
const res = await util.getInvites({}, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
codes.sort();
const resCodes = res.body.map(invite => invite.code).sort();
resCodes.should.deep.equal(codes, 'All invites should be present in result.');
}
async function verifySingleSearch(code) {
const res = await util.getInvites({code: code}, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
res.body.should.have.length(1, 'Only one invite should be in the array');
res.body[0].code.should.equal(code, 'The found invite should match the request code');
}
describe('0 Valid Request', () => {
it('SHOULD get multiple invites from a valid session', async () => {
await util.createSession(agent, ['invite.create', 'invite.get', 'file.upload']);
const inv1 = await util.createInvite({scope: ['file.upload']}, agent);
const inv2 = await util.createInvite({scope: ['invite.create']}, agent);
return verifyInviteSearch([inv1.body.code, inv2.body.code]);
});
it('SHOULD get a single invite from a valid session', async () => {
await util.createSession(agent, ['invite.create', 'invite.get', 'file.upload']);
const inv = await util.createInvite({scope: ['file.upload']}, agent);
return verifySingleSearch(inv.body.code);
});
it('SHOULD get another users invite with invite.get.others scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
const inv = await util.createInvite({scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['invite.get', 'invite.get.others'], 'eve');
return verifySingleSearch(inv.body.code);
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT get invites without invite.get scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload']);
const res = await util.getInvites({code: 'bogus'}, agent);
util.verifyResponse(res, 403, 'Forbidden.');
});
it('SHOULD NOT get another users invite without invite.get.others scope', async () => {
await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
const invite = await util.createInvite({scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['invite.get'], 'eve');
const res = await util.getInvites({code: invite.body.code}, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
res.body.should.have.length(0, 'No invites should be found.');
});
});
});
});
describe('Keys', () => {
describe('/POST create', () => {
async function verifyCreatedKey(key) {
const res = await util.createKey(key, agent);
util.verifyResponse(res, 200, 'Key created.');
res.body.should.have.property('key').match(/^[A-Fa-f0-9]+$/, 'The key should be a hex string');
const dbKey = await Key.findOne({key: res.body.key});
dbKey.should.not.equal(null);
dbKey.scope.should.deep.equal(key.scope, 'The created keys scope should match the request.');
dbKey.issuer.should.equal('user');
}
describe('0 Valid Request', () => {
it('SHOULD create a key with valid scope from a valid session', async () => {
await util.createSession(agent, ['key.create', 'file.upload']);
return verifyCreatedKey({identifier: 'key', scope: ['file.upload']});
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT create a key without key.create scope', async () => {
await util.createSession(agent, ['file.upload']);
const res = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
util.verifyResponse(res, 403, 'Forbidden.');
});
it('SHOULD NOT create a key with scope exceeding the requesters', async () => {
await util.createSession(agent, ['key.create']);
const res = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
util.verifyResponse(res, 403, 'Requested scope exceeds own scope.');
});
});
describe('2 Key Limit', () => {
it('must not create additional keys beyond the limit', async () => {
await util.createSession(agent, ['key.create', 'file.upload']);
const limit = config.get('Key.limit');
// Create keys upto the limit (key0, key1, key2, ...)
await Promise.all(
[...Array(limit)]
.map(idx => util.createKey({identifier: 'key' + idx, scope: ['file.upload']}, agent)));
const res = await util.createKey({identifier: 'toomany', scope: ['file.upload']}, agent);
util.verifyResponse(res, 403, 'Key limit reached.');
});
});
});
describe('/POST delete', () => {
async function verifyDeletedKey(key) {
const res = await util.deleteKey(key, agent);
util.verifyResponse(res, 200, 'Key deleted.');
const keyCount = await Key.countDocuments({key: key});
keyCount.should.equal(0, 'The key should be removed from the database.');
}
describe('0 Valid Request', () => {
it('SHOULD delete a key with valid scope from a valid session', async () => {
await util.createSession(agent, ['key.create', 'key.delete', 'file.upload']);
const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
return verifyDeletedKey(key.body.key);
});
it('SHOULD delete another users key with key.delete.others scope', async () => {
await util.createSession(agent, ['key.create', 'file.upload'], 'alice');
const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['key.delete', 'key.delete.others'], 'eve');
return verifyDeletedKey(key.body.key);
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT delete another users key without key.delete.others scope', async () => {
await util.createSession(agent, ['key.create', 'file.upload'], 'bob');
const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['key.delete'], 'eve');
const res = await util.deleteKey(key.body.key, agent);
util.verifyResponse(res, 422, 'Key not found.');
});
});
describe('2 Invalid Key', () => {
it('SHOULD return an error when the key was not found', async () => {
await util.createSession(agent, ['key.delete']);
const res = await util.deleteKey('bogus', agent);
util.verifyResponse(res, 422, 'Key not found.');
});
});
});
describe('/POST get', () => {
async function verifyKeySearch(keys, query = {}) {
const res = await util.getKeys(query, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
keys.sort();
const resKeys = res.body.map(key => key.key).sort();
resKeys.should.deep.equal(keys, 'All keys should be present in the result.');
}
async function verifySingleSearch(key, query = {}) {
const res = await util.getKeys(query, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
res.body.should.have.length(1, 'Only one key should be in the array');
res.body[0].key.should.equal(key, 'The found key should match the request code');
}
describe('0 Valid Request', () => {
it('SHOULD get multiple keys from a valid session', async () => {
await util.createSession(agent, ['key.create', 'key.get', 'file.upload']);
const keys = await Promise.all([
util.createKey({identifier: 'key1', scope: ['file.upload']}, agent),
util.createKey({identifier: 'key2', scope: ['file.upload']}, agent)
]);
return verifyKeySearch(keys.map(res => res.body.key));
});
it('SHOULD get a key by identifier from a valid session', async () => {
await util.createSession(agent, ['key.create', 'key.get', 'file.upload']);
const keys = await Promise.all([
util.createKey({identifier: 'key1', scope: ['file.upload']}, agent),
util.createKey({identifier: 'key2', scope: ['file.upload']}, agent)
]);
return verifySingleSearch(keys[1].body.key, {identifier: 'key2'});
});
it('SHOULD get another users key with key.get.others scope', async () => {
await util.createSession(agent, ['key.create', 'file.upload'], 'bob');
const key1 = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['key.create', 'key.get', 'key.get.others', 'file.upload']);
const key2 = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
return verifyKeySearch([key1.body.key, key2.body.key]);
});
});
describe('1 Invalid Scope', () => {
it('SHOULD NOT get another users key without key.get.others scope', async () => {
await util.createSession(agent, ['key.create', 'file.upload'], 'alice');
await util.createInvite({identifier: 'private_key', scope: ['file.upload']}, agent);
await util.logout(agent);
await util.createSession(agent, ['key.get'], 'eve');
const res = await util.getKeys({identifier: 'private_key'}, agent);
res.should.have.status(200);
res.body.should.be.a('Array');
res.body.should.have.length(0, 'No invites should be found.');
});
});
});
});
describe('Users', () => {
describe('/GET get', () => {
beforeEach(() => Promise.all([
Promise.all([
util.insertInvite({code: 'test1', scope: ['file.upload'], issuer: 'Mocha'}),
util.insertInvite({code: 'test2', scope: ['file.upload'], issuer: 'Mocha'})
]),
Promise.all([
util.registerUser({displayname: 'user1', password: 'pass', invite: 'test1'}, agent),
util.registerUser({displayname: 'user2', password: 'pass', invite: 'test2'}, agent)
])
]));
async function verifyGetUsers(res, users) {
res.should.have.status(200);
res.body.should.be.a('Array');
res.body.map(user => user.username).sort().should.deep.equal(users.sort());
}
describe('0 Valid Request', () => {
it('must get all users with an empty query', async () => {
await util.createSession(agent, ['user.get'], 'admin');
const res = await util.getUsers({}, agent);
return verifyGetUsers(res, ['user1', 'user2', 'admin']);
});
it('must filter users by username', async () => {
await util.createSession(agent, ['user.get'], 'admin');
const res = await util.getUsers({username: 'user1'}, agent);
return verifyGetUsers(res, ['user1']);
});
it('must filter users by displayname', async () => {
await util.createSession(agent, ['user.get'], 'admin');
const res = await util.getUsers({displayname: 'user1'}, agent);
return verifyGetUsers(res, ['user1']);
});
it('must return an empty array when no users were found', async () => {
await util.createSession(agent, ['user.get'], 'admin');
const res = await util.getUsers({username: 'abc'}, agent);
return verifyGetUsers(res, []);
});
});
});
describe('/POST ban/unban', () => {
beforeEach(async () => {
await util.insertInvite({code: 'test1', scope: ['file.upload'], issuer: 'Mocha'});
await util.registerUser({displayname: 'user', password: 'pass', invite: 'test1'}, agent);
});
async function verifyBanned(res, statusCode, message, username, banStatus) {
util.verifyResponse(res, statusCode, message);
const user = await User.findOne({username: username});
user.should.exist;
user.should.have.property('banned').equal(banStatus, 'The user should have banned status ' + banStatus);
}
describe('0 Valid Request', () => {
it('must ban a not banned user', async () => {
await util.createSession(agent, ['user.ban'], 'admin');
const res = await util.ban('user', agent);
return verifyBanned(res, 200, 'User banned.', 'user', true);
});
it('must unban a banned user', async () => {
await util.setBanned('user', true);
await util.createSession(agent, ['user.unban'], 'admin');
const res = await util.unban('user', agent);
return verifyBanned(res, 200, 'User unbanned.', 'user', false);
});
});
describe('1 Already Requested Ban Status', () => {
it('must not ban an already banned user', async () => {
await util.setBanned('user', true);
await util.createSession(agent, ['user.ban'], 'admin');
const res = await util.ban('user', agent);
return verifyBanned(res, 422, 'User already banned.', 'user', true);
});
it('must not unban a not banned user', async () => {
await util.createSession(agent, ['user.unban'], 'admin');
const res = await util.unban('user', agent);
return verifyBanned(res, 422, 'User not banned.', 'user', false);
});
});
describe('2 Not Found', () => {
it('must not ban a nonexistant user', async () => {
await util.createSession(agent, ['user.ban'], 'admin');
const res = await util.ban('abc', agent);
util.verifyResponse(res, 422, 'User not found.');
});
it('must not unban a nonexistant user', async () => {
await util.createSession(agent, ['user.unban'], 'admin');
const res = await util.unban('abc', agent);
util.verifyResponse(res, 422, 'User not found.');
});
});
});
});
describe('Stats', () => {
const setupUploadsAndViews = async () => {
const oneDay = 1000 * 60 * 60 * 24;
const currentDate = new Date();
const get_uid = i => {
return 'abcde' + String.fromCharCode(i + 97)
};
for (let i = 0; i < 8; i++) {
await util.insertUpload({
uid: get_uid(i),
views: 0,
uploader: 'user',
uploaderKey: null,
date: new Date(currentDate - i * oneDay),
file: {
size: 1
}
});
}
await util.insertUpload({
uid: 'zyxwvu',
uploader: 'someguy',
date: new Date(currentDate - 3 * oneDay),
file: {
size: 1
}
});
for (let i = 0; i < 8; i++) {
await util.insertView({
uid: get_uid(i),
uploader: 'user',
remoteAddress: '::1',
userAgent: 'fiyerfocks',
date: new Date(currentDate - i * oneDay),
});
}
await util.insertView({
uid: 'zyxwvu',
uploader: 'someguy',
remoteAddress: '::1',
userAgent: 'fiyerfocks',
date: new Date(currentDate - 3 * oneDay)
});
};
describe('/GET week', () => {
describe('0 Valid Request', () => {
it('must return valid stats for the past week', async () => {
await setupUploadsAndViews();
const oneDay = 1000 * 60 * 60 * 24;
const currentDate = new Date();
await util.createSession(agent, ['stats.get'], 'user');
const stats = (await util.getStatsWeek(agent)).body;
console.log(stats);
for (let i = 0; i < 7; i++) {
let date = new Date(currentDate - i * oneDay).toISOString();
let dateStr = date.substr(5, 2) + '-' + date.substr(8, 2);
let dayStats = stats[dateStr];
dayStats.should.be.a('object', 'Stats should exist for the day ' + dateStr);
dayStats.uploads.should.equal(1, 'Should be only one upload for the day ' + dateStr);
dayStats.size.should.equal(1, 'Should be only one byte uploaded for the day ' + dateStr);
dayStats.views.should.equal(1, 'Should be only one view for the day ' + dateStr);
}
let pastDate = new Date(currentDate - 7 * oneDay).toISOString();
let pastDateStr = pastDate.substr(5, 2) + '-' + pastDate.substr(8, 2);
stats.should.not.have.property(pastDateStr, 'No stats should exist past 1 week ago');
return util.logout(agent);
});
it('must return an empty set when there are no stats', async () => {
await util.createSession(agent, ['stats.get'], 'user');
const stats_self = (await util.getStatsWeek(agent)).body;
stats_self.should.deep.equal({});
await setupUploadsAndViews();
await util.createSession(agent, ['stats.get'], 'other_user');
const stats_other = (await util.getStatsWeek(agent)).body;
stats_other.should.deep.equal({}, 'No stats should be returned from another user');
return util.logout(agent);
});
});
});
describe('/GET all', () => {
describe('0 Valid Request', () => {
it('must return valid stats for all time', async () => {
await setupUploadsAndViews();
await util.createSession(agent, ['stats.get'], 'user');
const stats = (await util.getStatsAll(agent)).body;
stats.should.have.property('total');
stats.total.should.have.property('count').equal(8);
stats.total.should.have.property('size').equal(8);
stats.total.should.have.property('views').equal(8);
return util.logout(agent);
});
it('must return an empty set when there are no stats', async () => {
await util.createSession(agent, ['stats.get'], 'user');
const stats_self = (await util.getStatsAll(agent)).body;
stats_self.should.deep.equal({}, 'No stats should be returned');
await util.logout(agent);
await setupUploadsAndViews();
await util.createSession(agent, ['stats.get'], 'other_user');
const stats_other = (await util.getStatsAll(agent)).body;
stats_other.should.deep.equal({}, 'No stats should be returned from another user');
return util.logout(agent);
});
});
});
});
after(() => server.close(() => process.exit(0)));