1
0
mirror of https://github.com/Foltik/Shimapan synced 2024-11-10 23:53:31 -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({
name: {
type: String,
required: true
},
id: {
type: String,
unique: true,
required: true
},
views: {
type: Number,
default: 0
},
uploader: String,
uploadKey: String,
date: Date,
file: Object
uploader: {
type: String,
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);

View File

@ -1,7 +1,6 @@
var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var User = require('../models/User.js');
var Upload = require('../models/Upload.js');
var Key = require('../models/Key.js');
@ -9,106 +8,62 @@ var Key = require('../models/Key.js');
var multer = require('multer');
var dest = multer({dest: 'uploads/'});
function fileNameExists(name) {
Upload.count({name: name}, function (err, count) {
return count !== 0;
});
}
const requireAuth = require('../util/requireAuth').requireAuth;
const wrap = require('../util/wrap.js').wrap;
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) {
if (type === 'session') {
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;
});
}
}
const generatedIdExists = async id =>
await Upload.countDocuments({id: id}) === 1;
var checkApiKey = function (key, cb) {
Key.find({key: key}, function (err, res) {
if (err) throw err;
cb(res.length === 1, res);
});
const generateId = async () => {
const charset = "abcdefghijklmnopqrstuvwxyz";
const len = 6;
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) {
if (type === 'session') {
User.findOne({username: id}, function (err, user) {
if (err) throw err;
cb(user.scope.indexOf(perm) !== -1);
});
} else {
Key.findOne({key: id}, function (err, key) {
if (err) throw err;
cb(key.scope.indexOf(perm) !== -1);
});
}
};
const updateStats = async req =>
Promise.all([
User.updateOne({username: req.authUser}, {$inc: {uploadCount: 1, uploadSize: req.file.size}}),
req.authKey
? Key.updateOne({key: req.authKey}, {$inc: {uploadCount: 1, uploadSize: req.file.size}})
: Promise.resolve()
]);
function uploadFile(req, res, type, key) {
router.post('/', requireAuth('file.upload'), dest.single('file'), wrap(async (req, res, next) => {
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)
if (req.file.size >= 134217728)
return res.status(413).json({'message': 'File too large.'});
// Max file size is 128 MiB
if (req.file.size > 1024 * 1024 * 128)
return res.status(413).json({message: 'File too large.'});
var uploader = type === 'session' ? req.session.passport.user : key[0].username;
var uploadKey = type === 'apikey' ? key[0].key : null;
var id = type === 'session' ? req.session.passport.user : key[0].key;
const upload = {
name: req.file.originalname,
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) {
if (!valid)
return res.status(403).json({'message': 'No permission.'});
await Promise.all([
Upload.create(upload),
updateStats(req)
]);
var entry = {
name: genFileName(),
uploader: uploader,
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
});
});
res.status(200).json({
id: upload.id,
url: 'https://shimapan.rocks/v/' + upload.id
});
}
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;

View File

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

View File

@ -1,8 +1,5 @@
process.env.NODE_ENV = 'test';
const app = require('../server');
const server = app.server;
const chai = require('chai');
chai.use(require('chai-http'));
const should = chai.should();
@ -11,6 +8,10 @@ const User = require('../app/models/User.js');
const Invite = require('../app/models/Invite.js');
const Upload = require('../app/models/Upload.js');
const Buffer = require('buffer').Buffer;
const crypto = require('crypto');
const fs = require('fs').promises;
//---------------- DATABASE UTIL ----------------//
exports.clearDatabase = async () =>
@ -22,129 +23,58 @@ exports.clearDatabase = async () =>
//---------------- API ROUTES ----------------//
exports.login = async (credentials, agent) => {
return (agent ? agent : chai.request(server))
exports.login = async (credentials, agent) =>
agent
.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();
return Invite.create(invite);
};
exports.logout = agent =>
agent
.post('/api/auth/logout');
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)
exports.createInvite = async (invite) =>
Invite.create(invite);
exports.registerUser = async (user, agent) =>
agent
.post('/api/auth/register')
.send(user);
};
exports.ping = async (agent) =>
(agent ? agent : chai.request(server))
.get('/api/auth/ping')
exports.whoami = async (agent) =>
agent
.get('/api/auth/whoami')
.send();
//---------------- TEST ENTRY CREATION ----------------//
exports.createTestInvite = async () =>
exports.createInvite({});
exports.createInvite({code: 'code', scope: ['file.upload']});
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})));
};
exports.createTestInvites = async (n) =>
Promise.all(
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();
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) {
chai.request(server)
exports.createTestFile = async (size, name) =>
fs.writeFile(name, Buffer.allocUnsafe(size));
exports.deleteTestFile = async name =>
fs.unlink(name);
//---------------- UPLOADS ----------------//
exports.upload = (file, agent) =>
agent
.post('/api/upload')
.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();
});
};
.attach('file', file);