1
0
mirror of https://github.com/Foltik/Shimapan synced 2025-01-07 08:42:49 -05:00

Redo uploading and tests

This commit is contained in:
Jack Foltz 2018-07-26 13:17:37 -04:00
parent 58e25e4e2a
commit 15db9a47ba
Signed by: foltik
GPG Key ID: 303F88F996E95541
4 changed files with 214 additions and 252 deletions

View File

@ -3,17 +3,44 @@ var mongoose = require('mongoose');
var UploadSchema = mongoose.Schema({ var UploadSchema = mongoose.Schema({
name: { name: {
type: String, type: String,
required: true
},
id: {
type: String,
unique: true, unique: true,
required: true required: true
}, },
views: { views: {
type: Number, type: Number,
default: 0 default: 0
}, },
uploader: String,
uploadKey: String, uploader: {
date: Date, type: String,
file: Object required: true
},
uploaderKey: {
type: String,
default: null
},
date: {
type: Date,
default: Date.now
},
mime: {
type: String,
required: true,
},
file: {
type: Object,
required: true
}
}); });
module.exports = mongoose.model('Upload', UploadSchema); module.exports = mongoose.model('Upload', UploadSchema);

View File

@ -1,7 +1,6 @@
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();
var mongoose = require('mongoose');
var User = require('../models/User.js'); var User = require('../models/User.js');
var Upload = require('../models/Upload.js'); var Upload = require('../models/Upload.js');
var Key = require('../models/Key.js'); var Key = require('../models/Key.js');
@ -9,106 +8,62 @@ var Key = require('../models/Key.js');
var multer = require('multer'); var multer = require('multer');
var dest = multer({dest: 'uploads/'}); var dest = multer({dest: 'uploads/'});
function fileNameExists(name) { const requireAuth = require('../util/requireAuth').requireAuth;
Upload.count({name: name}, function (err, count) { const wrap = require('../util/wrap.js').wrap;
return count !== 0;
});
}
function genFileName() {
var charset = "abcdefghijklmnopqrstuvwxyz";
do {
var chars = [];
for (var i = 0; i < 6; i++)
chars.push(charset.charAt(Math.floor(Math.random() * charset.length)));
} while (fileNameExists(chars.join('')));
return chars.join('');
}
function updateStats(type, id, size) { const generatedIdExists = async id =>
if (type === 'session') { await Upload.countDocuments({id: id}) === 1;
User.updateOne({username: id}, {$inc: {uploadCount: 1, uploadSize: size}}, function (err) {
if (err) throw err;
});
} else if (type === 'apikey') {
Key.updateOne({key: id}, {$inc: {uploadCount: 1, uploadSize: size}}, function (err) {
if (err) throw err;
});
}
}
var checkApiKey = function (key, cb) { const generateId = async () => {
Key.find({key: key}, function (err, res) { const charset = "abcdefghijklmnopqrstuvwxyz";
if (err) throw err; const len = 6;
cb(res.length === 1, res);
}); const id = [...Array(len)]
.map(() => charset.charAt(Math.floor(Math.random() * charset.length)))
.join('');
return await generatedIdExists(id)
? generateId()
: id;
}; };
var checkScope = function (type, id, perm, cb) { const updateStats = async req =>
if (type === 'session') { Promise.all([
User.findOne({username: id}, function (err, user) { User.updateOne({username: req.authUser}, {$inc: {uploadCount: 1, uploadSize: req.file.size}}),
if (err) throw err; req.authKey
cb(user.scope.indexOf(perm) !== -1); ? Key.updateOne({key: req.authKey}, {$inc: {uploadCount: 1, uploadSize: req.file.size}})
}); : Promise.resolve()
} else { ]);
Key.findOne({key: id}, function (err, key) {
if (err) throw err;
cb(key.scope.indexOf(perm) !== -1);
});
}
};
function uploadFile(req, res, type, key) {
router.post('/', requireAuth('file.upload'), dest.single('file'), wrap(async (req, res, next) => {
if (!req.file) if (!req.file)
return res.status(400).json({'message': 'No file specified.'}); return res.status(400).json({message: 'No file specified.'});
// Size must be below 128 Megabytes (1024*1024*128 Bytes) // Max file size is 128 MiB
if (req.file.size >= 134217728) if (req.file.size > 1024 * 1024 * 128)
return res.status(413).json({'message': 'File too large.'}); return res.status(413).json({message: 'File too large.'});
var uploader = type === 'session' ? req.session.passport.user : key[0].username; const upload = {
var uploadKey = type === 'apikey' ? key[0].key : null; name: req.file.originalname,
var id = type === 'session' ? req.session.passport.user : key[0].key; id: await generateId(),
uploader: req.authUser,
uploaderKey: req.authKey,
date: Date.now(),
mime: req.file.mimetype,
file: req.file
};
checkScope(type, id, 'file.upload', function (valid) { await Promise.all([
if (!valid) Upload.create(upload),
return res.status(403).json({'message': 'No permission.'}); updateStats(req)
]);
var entry = { res.status(200).json({
name: genFileName(), id: upload.id,
uploader: uploader, url: 'https://shimapan.rocks/v/' + upload.id
uploadKey: uploadKey,
date: Date.now(),
file: req.file
};
updateStats(type, id, req.file.size);
Upload.create(entry, function (err) {
if (err) throw err;
res.status(200).json({
name: entry.name,
url: 'https://shimapan.rocks/v/' + entry.name
});
});
}); });
} }));
router.post('/', dest.single('file'), function (req, res) {
if (!req.session || !req.session.passport) {
if (!req.body.apikey) {
return res.sendStatus(401);
} else {
checkApiKey(req.body.apikey, function (valid, key) {
if (!valid)
return res.sendStatus(401);
else
uploadFile(req, res, 'apikey', key);
});
}
} else {
uploadFile(req, res, 'session');
}
});
module.exports = router; module.exports = router;

View File

@ -1,8 +1,5 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
const app = require('../server');
const server = app.server;
const chai = require('chai'); const chai = require('chai');
chai.use(require('chai-http')); chai.use(require('chai-http'));
const should = chai.should(); const should = chai.should();
@ -14,6 +11,21 @@ const Upload = require('../app/models/Upload.js');
const util = require('./testUtil.js'); const util = require('./testUtil.js');
const canonicalize = require('../app/util/canonicalize').canonicalize; const canonicalize = require('../app/util/canonicalize').canonicalize;
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();
});
describe('Users', function() { describe('Users', function() {
beforeEach(async () => util.clearDatabase()); beforeEach(async () => util.clearDatabase());
@ -22,7 +34,7 @@ describe('Users', function() {
async function verifySuccessfulRegister(user) { async function verifySuccessfulRegister(user) {
await util.createTestInvite(); await util.createTestInvite();
const res = await util.registerUser(user); const res = await util.registerUser(user, agent);
res.should.have.status(200); res.should.have.status(200);
res.body.should.be.a('object'); res.body.should.be.a('object');
@ -49,11 +61,11 @@ describe('Users', function() {
async function verifyRejectedInvite(invite, message) { async function verifyRejectedInvite(invite, message) {
const user = {username: 'user', password: 'pass', invite: 'code'}; const user = {username: 'user', password: 'pass', invite: 'code'};
if (invite) { if (invite) {
await util.createInvite(invite); await util.createInvite(invite, agent);
user.invite = invite.code; user.invite = invite.code;
} }
const res = await(util.registerUser(user)); const res = await(util.registerUser(user, agent));
res.should.have.status(422); res.should.have.status(422);
res.body.should.be.a('object'); res.body.should.be.a('object');
res.body.should.have.property('message').eql(message); res.body.should.have.property('message').eql(message);
@ -64,18 +76,18 @@ describe('Users', function() {
); );
it('MUST NOT register a used invite', async () => it('MUST NOT register a used invite', async () =>
verifyRejectedInvite({used: new Date()}, 'Invite already used.') verifyRejectedInvite({code: 'code', used: new Date()}, 'Invite already used.')
); );
it('MUST NOT register an expired invite', async () => it('MUST NOT register an expired invite', async () =>
verifyRejectedInvite({exp: new Date()}, 'Invite expired.') verifyRejectedInvite({code: 'code', exp: new Date()}, 'Invite expired.')
); );
}); });
describe('2 Invalid Usernames', () => { describe('2 Invalid Usernames', () => {
async function verifyRejectedUsername(user, message) { async function verifyRejectedUsername(user, message) {
const res = await util.registerUser(user); const res = await util.registerUser(user, agent);
res.should.have.status(422); res.should.have.status(422);
res.body.should.be.a('object'); res.body.should.be.a('object');
res.body.should.have.property('message').eql(message); res.body.should.have.property('message').eql(message);
@ -86,7 +98,7 @@ describe('Users', function() {
const user0 = {username: 'user', password: 'pass', invite: 'code0'}; const user0 = {username: 'user', password: 'pass', invite: 'code0'};
const user1 = {username: 'user', password: 'diff', invite: 'code1'}; const user1 = {username: 'user', password: 'diff', invite: 'code1'};
await util.registerUser(user0); await util.registerUser(user0, agent);
return verifyRejectedUsername(user1, 'Username in use.'); return verifyRejectedUsername(user1, 'Username in use.');
}); });
@ -95,7 +107,7 @@ describe('Users', function() {
const user0 = {username: 'bigbird', password: 'pass', invite: 'code0'}; const user0 = {username: 'bigbird', password: 'pass', invite: 'code0'};
const user1 = {username: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'diff', invite: 'code1'}; const user1 = {username: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'diff', invite: 'code1'};
await util.registerUser(user0); await util.registerUser(user0, agent);
return verifyRejectedUsername(user1, 'Username in use.'); return verifyRejectedUsername(user1, 'Username in use.');
}); });
@ -127,22 +139,17 @@ describe('Users', function() {
describe('/POST login', () => { describe('/POST login', () => {
async function verifySuccessfulLogin(credentials) { async function verifySuccessfulLogin(credentials) {
const agent = chai.request.agent(server);
const res = await util.login(credentials, agent); const res = await util.login(credentials, agent);
res.should.have.status(200); res.should.have.status(200);
res.body.should.have.property('message').equal('Logged in.'); res.body.should.have.property('message').equal('Logged in.');
res.should.have.cookie('session.id'); res.should.have.cookie('session.id');
const ping = await util.ping(agent); const whoami = await util.whoami(agent);
ping.should.have.status(200); whoami.should.have.status(200);
ping.body.should.have.property('message').equal('pong');
agent.close();
} }
async function verifyFailedLogin(credentials) { async function verifyFailedLogin(credentials) {
const res = await util.login(credentials); const res = await util.login(credentials, agent);
res.should.have.status(401); res.should.have.status(401);
res.body.should.be.a('object'); res.body.should.be.a('object');
res.body.should.have.property('message').equal('Unauthorized.'); res.body.should.have.property('message').equal('Unauthorized.');
@ -150,15 +157,19 @@ describe('Users', function() {
describe('0 Valid Request', () => { describe('0 Valid Request', () => {
it('SHOULD accept a valid user with a valid password', async () => { it('SHOULD accept a valid user with a valid password', async () => {
await util.createTestUser(); await util.createTestUser(agent);
return verifySuccessfulLogin({username: 'user', password: 'pass'}); return verifySuccessfulLogin({username: 'user', password: 'pass'});
}); });
it('SHOULD accept any non-normalized variant of a username with a valid password', async () => {
await util.create
})
}); });
describe('1 Invalid Password', () => { describe('1 Invalid Password', () => {
it('SHOULD NOT accept an invalid password', async () => { it('SHOULD NOT accept an invalid password', async () => {
await util.createTestUser(); await util.createTestUser(agent);
return verifyFailedLogin({username: 'user', password: 'bogus'}); return verifyFailedLogin({username: 'user', password: 'bogus'});
}); });
}); });
@ -171,48 +182,87 @@ describe('Users', function() {
}); });
}); });
/*describe('Uploads', () => { describe('Uploads', () => {
describe('/POST upload', () => { beforeEach(async () => util.clearDatabase());
describe('/POST upload', () => {
async function verifySuccessfulUpload(file) {
const res = await util.upload(file, agent);
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('url');
res.body.should.have.property('id').match(/^[a-z]{6}$/);
}
async function verifyFailedUpload(file, status, message) {
const res = await util.upload(file, agent);
res.should.have.status(status);
res.body.should.be.a('object');
res.body.should.have.property('message').equal(message);
}
describe('0 Valid Request', () => { describe('0 Valid Request', () => {
it('SHOULD accept logged in valid upload', function(done) { it('SHOULD accept logged in valid upload', async () => {
util.verifySuccessfulUpload({ await Promise.all([
username: 'TestUser2', util.createTestSession(agent),
password: 'TestPassword' util.createTestFile(2048, 'test.bin')
}, done); ]);
await verifySuccessfulUpload('test.bin');
return Promise.all([
util.logout(agent),
util.deleteTestFile('test.bin')
]);
}); });
}); });
describe('1 Invalid Authentication', () => { describe('1 Invalid Authentication', () => {
it('SHOULD NOT accept unauthenticated request', function(done) { it('SHOULD NOT accept an unauthenticated request', async () =>
util.verifyFailedAuthUpload(done); verifyFailedUpload(null, 401, 'Unauthorized.')
}); );
});
describe('2 Invalid Permission', () => { it('SHOULD NOT accept a request without file.upload scope', async () => {
it('SHOULD NOT accept without file.upload permission', function(done) { await util.createInvite({code: 'code', scope: []});
util.verifyFailedPermissionUpload({ await util.registerUser({username: 'user', password: 'pass', invite: 'code'}, agent);
username: 'TestUser1', await util.login({username: 'user', password: 'pass'}, agent);
password: 'TestPassword'
}, done); await util.createTestFile(2048, 'test.bin');
await verifyFailedUpload('test.bin', 403, 'Forbidden.');
return Promise.all([
util.logout(agent),
util.deleteTestFile('test.bin')
]);
}); });
}); });
describe('3 Invalid File', () => { describe('3 Invalid File', () => {
it('SHOULD NOT accept invalid size', function(done) { it('SHOULD NOT accept a too large file', async () => {
util.verifyFailedSizeUpload({ await Promise.all([
username: 'TestUser2', util.createTestSession(agent),
password: 'TestPassword' util.createTestFile(1024 * 1024 * 129, 'large.bin') // 129 MiB, limit is 128 MiB
}, done); ]);
})
await verifyFailedUpload('large.bin', 413, 'File too large.');
return Promise.all([
util.logout(agent),
util.deleteTestFile('large.bin')
]);
});
}); });
describe('4 Invalid Request', () => {
it('SHOULD NOT accept a request with no file attached', async () => {
await util.createTestSession(agent);
await verifyFailedUpload(null, 400, 'No file specified.');
return util.logout(agent);
})
})
}); });
});*/ });
after(() => server.close(() => process.exit(0))); after(() => server.close(() => process.exit(0)));

View File

@ -1,8 +1,5 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
const app = require('../server');
const server = app.server;
const chai = require('chai'); const chai = require('chai');
chai.use(require('chai-http')); chai.use(require('chai-http'));
const should = chai.should(); const should = chai.should();
@ -11,6 +8,10 @@ const User = require('../app/models/User.js');
const Invite = require('../app/models/Invite.js'); const Invite = require('../app/models/Invite.js');
const Upload = require('../app/models/Upload.js'); const Upload = require('../app/models/Upload.js');
const Buffer = require('buffer').Buffer;
const crypto = require('crypto');
const fs = require('fs').promises;
//---------------- DATABASE UTIL ----------------// //---------------- DATABASE UTIL ----------------//
exports.clearDatabase = async () => exports.clearDatabase = async () =>
@ -22,129 +23,58 @@ exports.clearDatabase = async () =>
//---------------- API ROUTES ----------------// //---------------- API ROUTES ----------------//
exports.login = async (credentials, agent) => { exports.login = async (credentials, agent) =>
return (agent ? agent : chai.request(server)) agent
.post('/api/auth/login') .post('/api/auth/login')
.send(credentials); .send(credentials);
};
exports.createInvite = async (invite) => { exports.logout = agent =>
if (!invite.code) invite.code = 'code'; agent
if (!invite.scope) invite.scope = ['test.perm', 'file.upload']; .post('/api/auth/logout');
if (!invite.issuer) invite.issuer = 'Mocha';
if (!invite.issued) invite.issued = new Date();
return Invite.create(invite);
};
exports.registerUser = async (user) => { exports.createInvite = async (invite) =>
if (!user.username) user.username = 'user'; Invite.create(invite);
if (!user.password) user.password = 'pass';
if (!user.invite) user.invite = 'code'; exports.registerUser = async (user, agent) =>
return chai.request(server) agent
.post('/api/auth/register') .post('/api/auth/register')
.send(user); .send(user);
};
exports.ping = async (agent) => exports.whoami = async (agent) =>
(agent ? agent : chai.request(server)) agent
.get('/api/auth/ping') .get('/api/auth/whoami')
.send(); .send();
//---------------- TEST ENTRY CREATION ----------------// //---------------- TEST ENTRY CREATION ----------------//
exports.createTestInvite = async () => exports.createTestInvite = async () =>
exports.createInvite({}); exports.createInvite({code: 'code', scope: ['file.upload']});
exports.createTestInvites = async (n) => { exports.createTestInvites = async (n) =>
const codes = Array.from(new Array(n), (val, index) => 'code' + index); Promise.all(
return Promise.all(codes.map(code => exports.createInvite({code: code}))); Array.from(new Array(n), (val, index) => 'code' + index)
}; .map(code => exports.createInvite({code: code}))
);
exports.createTestUser = async () => { exports.createTestUser = async agent => {
await exports.createTestInvite(); await exports.createTestInvite();
return exports.registerUser({}); return exports.registerUser({username: 'user', password: 'pass', invite: 'code'}, agent);
}; };
//---------------- UPLOAD API ----------------// exports.createTestSession = async agent => {
await exports.createTestUser(agent);
await exports.login({username: 'user', password: 'pass'}, agent);
};
var upload = function(token, file, cb) { exports.createTestFile = async (size, name) =>
chai.request(server) fs.writeFile(name, Buffer.allocUnsafe(size));
exports.deleteTestFile = async name =>
fs.unlink(name);
//---------------- UPLOADS ----------------//
exports.upload = (file, agent) =>
agent
.post('/api/upload') .post('/api/upload')
.attach('file', file) .attach('file', file);
.set('Authorization', 'Bearer ' + token)
.end(cb);
};
var loginUpload = function(user, cb) {
login(user, function(err, res) {
upload(res.body.token, 'test/test.png', cb);
});
};
var loginUploadFile = function(user, file, cb) {
login(user, function(err, res) {
upload(res.body.token, file, cb);
});
};
var verifySuccessfulUpload = function(user, done) {
loginUpload(user, function(err, res) {
res.should.have.status(200);
res.body.should.have.be.a('object');
res.body.should.have.property('url');
res.body.should.have.property('name');
expect(res.body.name).to.match(/^[a-z]{6}$/);
done();
});
};
var verifyFailedSizeUpload = function(user, done) {
loginUploadFile(user, 'test/large.bin', function(err, res) {
res.should.have.status(413);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('File too large.');
done();
});
};
var verifyFailedPermissionUpload = function(user, done) {
loginUpload(user, function(err, res) {
res.should.have.status(403);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('Permission error.');
done();
});
};
var verifyFailedAuthUpload = function(done) {
async.parallel([
function(cb) {
upload('bogus', 'test/test.png', function(err, res) {
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('UnauthorizedError: jwt malformed');
cb();
});
},
function(cb) {
upload('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.' +
'eyJpc3MiOiJzaGltYXBhbi5yb2NrcyIsImlhd' +
'CI6MTUwNzkyNTAyNSwiZXhwIjoxNTM5NDYxMD' +
'I1LCJhdWQiOiJ3d3cuc2hpbWFwYW4ucm9ja3M' +
'iLCJzdWIiOiJUZXN0VXNlciIsInVzZXJuYW1l' +
'IjoiVGVzdFVzZXIiLCJzY29wZSI6ImZpbGUud' +
'XBsb2FkIn0.e746_BNNuxlbXKESKKYsxl6e5j' +
'8JwmEFxO3zRf66tWo',
'test/test.png',
function(err, res) {
res.should.have.status(401);
res.body.should.be.a('object');
res.body.should.have.property('message').eql('UnauthorizedError: invalid signature');
cb();
})
}
], function(err, res) {
if (err) console.log(err);
done();
});
};