1
0
mirror of https://github.com/Foltik/Shimapan synced 2025-03-27 05:23:40 -04:00

Fix upload size bug in upload.js and add more tests

This commit is contained in:
Jack Foltz 2018-07-26 16:54:08 -04:00
parent 827c4cdbfa
commit eda74de1de
Signed by: foltik
GPG Key ID: 303F88F996E95541
6 changed files with 98 additions and 34 deletions

View File

@ -1,17 +1,18 @@
var express = require('express'); const express = require('express');
var router = express.Router(); const router = express.Router();
const config = require('config');
var User = require('../models/User.js'); const User = require('../models/User.js');
var Upload = require('../models/Upload.js'); const Upload = require('../models/Upload.js');
var Key = require('../models/Key.js'); const Key = require('../models/Key.js');
var multer = require('multer'); const multer = require('multer');
var dest = multer({dest: 'uploads/'}); const fileUpload = multer({dest: config.uploadPath}).single('file');
const fsPromises = require('fs').promises;
const requireAuth = require('../util/requireAuth').requireAuth; const requireAuth = require('../util/requireAuth').requireAuth;
const wrap = require('../util/wrap.js').wrap; const wrap = require('../util/wrap.js').wrap;
const generatedIdExists = async id => const generatedIdExists = async id =>
await Upload.countDocuments({id: id}) === 1; await Upload.countDocuments({id: id}) === 1;
@ -37,21 +38,21 @@ const updateStats = async req =>
]); ]);
router.post('/', requireAuth('file.upload'), dest.single('file'), wrap(async (req, res, next) => { router.post('/', requireAuth('file.upload'), fileUpload, 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.'});
// Max file size is 128 MiB // Max file size is 128 MiB
if (req.file.size > 1024 * 1024 * 128) if (req.file.size > 1024 * 1024 * 128) {
await fsPromises.unlink(req.file.path);
return res.status(413).json({message: 'File too large.'}); return res.status(413).json({message: 'File too large.'});
}
const upload = { const upload = {
name: req.file.originalname,
id: await generateId(), id: await generateId(),
uploader: req.authUser, uploader: req.authUser,
uploaderKey: req.authKey, uploaderKey: req.authKey,
date: Date.now(), date: Date.now(),
mime: req.file.mimetype,
file: req.file file: req.file
}; };

View File

@ -1,4 +1,5 @@
{ {
"dbHost": "mongodb://localhost:27017/shimapan", "dbHost": "mongodb://localhost:27017/shimapan",
"uploadPath": "uploads",
"httpLogLevel": "dev" "httpLogLevel": "dev"
} }

View File

@ -1,4 +1,5 @@
{ {
"dbHost": "mongodb://localhost:27017/shimapan", "dbHost": "mongodb://localhost:27017/shimapan",
"uploadPath": "uploads",
"httpLogLevel": "dev" "httpLogLevel": "dev"
} }

View File

@ -1,4 +1,5 @@
{ {
"dbHost": "mongodb://localhost:27017/shimapan-test", "dbHost": "mongodb://localhost:27017/shimapan-test",
"uploadPath": "uploads-test",
"httpLogLevel": "dev" "httpLogLevel": "dev"
} }

View File

@ -7,6 +7,7 @@ const should = chai.should();
const User = require('../app/models/User.js'); 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 Key = require('../app/models/Key.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;
@ -26,7 +27,7 @@ after(() => {
server.close(); server.close();
}); });
describe('Users', function() { describe('Accounts', function() {
beforeEach(async () => util.clearDatabase()); beforeEach(async () => util.clearDatabase());
describe('/POST register', () => { describe('/POST register', () => {
@ -41,10 +42,10 @@ describe('Users', function() {
res.body.should.have.property('message').eql('Registration successful.'); res.body.should.have.property('message').eql('Registration successful.');
const userCount = await User.countDocuments({username: user.username}); const userCount = await User.countDocuments({username: user.username});
userCount.should.eql(1); userCount.should.equal(1);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.username)}); const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.username)});
inviteCount.should.eql(1); inviteCount.should.equal(1);
} }
it('MUST register a valid user with a valid invite', async () => it('MUST register a valid user with a valid invite', async () =>
@ -69,6 +70,9 @@ describe('Users', function() {
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);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.username)});
inviteCount.should.equal(0);
} }
it('MUST NOT register a nonexistant invite', async () => it('MUST NOT register a nonexistant invite', async () =>
@ -90,7 +94,10 @@ describe('Users', function() {
const res = await util.registerUser(user, agent); 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').equal(message);
const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.username)});
inviteCount.should.equal(0);
} }
it('MUST NOT register a duplicate username', async () => { it('MUST NOT register a duplicate username', async () => {
@ -186,19 +193,51 @@ describe('Uploads', () => {
beforeEach(async () => util.clearDatabase()); beforeEach(async () => util.clearDatabase());
describe('/POST upload', () => { describe('/POST upload', () => {
async function verifySuccessfulUpload(file) { async function verifySuccessfulUpload(file, user) {
// Get file stats beforehand
const [fileHash, fileSize] = await Promise.all([util.fileHash(file), util.fileSize(file)]);
// Get the user stats beforehand
const userBefore = await User.findOne({canonicalname: user}, {_id: 0, uploadCount: 1, uploadSize: 1});
// Submit the upload and verify the result
const res = await util.upload(file, agent); const res = await util.upload(file, agent);
res.should.have.status(200); res.should.have.status(200);
res.body.should.be.a('object'); res.body.should.be.a('object');
res.body.should.have.property('url'); res.body.should.have.property('url');
res.body.should.have.property('id').match(/^[a-z]{6}$/); res.body.should.have.property('id').match(/^[a-z]{6}$/);
// Find the uploaded file in the database
const upload = await Upload.findOne({id: res.body.id}, {_id: 0, id: 1, file: 1});
const uploadFile = upload.file.path;
upload.should.be.a('object');
upload.id.should.equal(res.body.id);
// Verify the uploaded file is the same as the file now on disk
const [uploadHash, uploadSize] = await Promise.all([util.fileHash(uploadFile), util.fileSize(uploadFile)]);
uploadHash.should.equal(fileHash);
uploadSize.should.equal(fileSize);
// Verify the user's stats have been updated correctly
const userAfter = await User.findOne({canonicalname: user}, {_id: 0, uploadCount: 1, uploadSize: 1});
userAfter.uploadCount.should.equal(userBefore.uploadCount + 1);
userAfter.uploadSize.should.equal(userBefore.uploadSize + fileSize);
} }
async function verifyFailedUpload(file, status, message) { async function verifyFailedUpload(file, status, message) {
const fileCountBefore = await util.directoryFileCount('uploads');
const uploadCountBefore = await Upload.countDocuments({});
const res = await util.upload(file, agent); const res = await util.upload(file, agent);
res.should.have.status(status); res.should.have.status(status);
res.body.should.be.a('object'); res.body.should.be.a('object');
res.body.should.have.property('message').equal(message); res.body.should.have.property('message').equal(message);
const fileCountAfter = await util.directoryFileCount('uploads');
fileCountAfter.should.equal(fileCountBefore, 'File should not have been written to disk');
const uploadCountAfter = await Upload.countDocuments({});
uploadCountAfter.should.equal(uploadCountBefore, 'No uploads should have been written to the database');
} }
describe('0 Valid Request', () => { describe('0 Valid Request', () => {
@ -208,11 +247,11 @@ describe('Uploads', () => {
util.createTestFile(2048, 'test.bin') util.createTestFile(2048, 'test.bin')
]); ]);
await verifySuccessfulUpload('test.bin'); await verifySuccessfulUpload('test.bin', 'user');
return Promise.all([ return Promise.all([
util.logout(agent), util.logout(agent),
util.deleteTestFile('test.bin') util.deleteFile('test.bin')
]); ]);
}); });
}); });
@ -233,7 +272,7 @@ describe('Uploads', () => {
return Promise.all([ return Promise.all([
util.logout(agent), util.logout(agent),
util.deleteTestFile('test.bin') util.deleteFile('test.bin')
]); ]);
}); });
}); });
@ -249,7 +288,7 @@ describe('Uploads', () => {
return Promise.all([ return Promise.all([
util.logout(agent), util.logout(agent),
util.deleteTestFile('large.bin') util.deleteFile('large.bin')
]); ]);
}); });
}); });

View File

@ -11,11 +11,12 @@ const Key = require('../app/models/Key.js');
const Buffer = require('buffer').Buffer; const Buffer = require('buffer').Buffer;
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs').promises; const fs = require('fs');
const fsPromises = fs.promises;
//---------------- DATABASE UTIL ----------------// //---------------- DATABASE UTIL ----------------//
exports.clearDatabase = async () => exports.clearDatabase = () =>
Promise.all([ Promise.all([
User.remove({}), User.remove({}),
Invite.remove({}), Invite.remove({}),
@ -25,7 +26,7 @@ exports.clearDatabase = async () =>
//---------------- API ROUTES ----------------// //---------------- API ROUTES ----------------//
exports.login = async (credentials, agent) => exports.login = (credentials, agent) =>
agent agent
.post('/api/auth/login') .post('/api/auth/login')
.send(credentials); .send(credentials);
@ -34,25 +35,25 @@ exports.logout = agent =>
agent agent
.post('/api/auth/logout'); .post('/api/auth/logout');
exports.createInvite = async (invite) => exports.createInvite = (invite) =>
Invite.create(invite); Invite.create(invite);
exports.registerUser = async (user, agent) => exports.registerUser = (user, agent) =>
agent agent
.post('/api/auth/register') .post('/api/auth/register')
.send(user); .send(user);
exports.whoami = async (agent) => exports.whoami = (agent) =>
agent agent
.get('/api/auth/whoami') .get('/api/auth/whoami')
.send(); .send();
//---------------- TEST ENTRY CREATION ----------------// //---------------- TEST ENTRY CREATION ----------------//
exports.createTestInvite = async () => exports.createTestInvite = () =>
exports.createInvite({code: 'code', scope: ['file.upload']}); exports.createInvite({code: 'code', scope: ['file.upload']});
exports.createTestInvites = async (n) => exports.createTestInvites = (n) =>
Promise.all( Promise.all(
Array.from(new Array(n), (val, index) => 'code' + index) Array.from(new Array(n), (val, index) => 'code' + index)
.map(code => exports.createInvite({code: code})) .map(code => exports.createInvite({code: code}))
@ -65,14 +66,34 @@ exports.createTestUser = async agent => {
exports.createTestSession = async agent => { exports.createTestSession = async agent => {
await exports.createTestUser(agent); await exports.createTestUser(agent);
await exports.login({username: 'user', password: 'pass'}, agent); return exports.login({username: 'user', password: 'pass'}, agent);
}; };
exports.createTestFile = async (size, name) => exports.createTestFile = (size, name) =>
fs.writeFile(name, Buffer.allocUnsafe(size)); fsPromises.writeFile(name, Buffer.allocUnsafe(size));
exports.deleteTestFile = async name => //---------------- FILESYSTEM ----------------//
fs.unlink(name);
exports.deleteFile = file =>
fsPromises.unlink(file);
exports.fileExists = file =>
fsPromises.access(file, fs.constants.R_OK);
exports.fileSize = async file =>
(await fsPromises.stat(file)).size;
exports.fileHash = file =>
new Promise((resolve, reject) => {
const hash = crypto.createHash('MD5');
fs.createReadStream(file)
.on('error', reject)
.on('data', chunk => hash.update(chunk))
.on('end', () => resolve(hash.digest('hex')));
});
exports.directoryFileCount = async dir =>
(await fsPromises.readdir(dir)).length;
//---------------- UPLOADS ----------------// //---------------- UPLOADS ----------------//