A simple file sharing site with an easy to use API and online panel.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1066 lines
47KB

  1. process.env.NODE_ENV = 'test';
  2. const chai = require('chai');
  3. chai.use(require('chai-http'));
  4. const should = chai.should();
  5. const describe = require('mocha').describe;
  6. const ModelPath = '../app/models/';
  7. const User = require(ModelPath + 'User.js');
  8. const Upload = require(ModelPath + 'Upload.js');
  9. const Key = require(ModelPath + 'Key.js');
  10. const Invite = require(ModelPath + 'Invite.js');
  11. const View = require(ModelPath + 'View.js');
  12. const util = require('./testUtil.js');
  13. const canonicalize = require('../app/util/canonicalize').canonicalize;
  14. const config = require('config');
  15. let app;
  16. let server;
  17. let agent;
  18. before(() => {
  19. const main = require('../server.js');
  20. app = main.app;
  21. server = main.server;
  22. agent = chai.request.agent(app);
  23. });
  24. after(() => {
  25. server.close();
  26. });
  27. beforeEach(() => util.clearDatabase());
  28. describe('Authentication', () => {
  29. describe('/POST register', () => {
  30. describe('0 Valid Request', () => {
  31. async function verifySuccessfulRegister(user) {
  32. await util.createTestInvite();
  33. const res = await util.registerUser(user, agent);
  34. util.verifyResponse(res, 200, 'Registration successful.');
  35. const userCount = await User.countDocuments({displayname: user.displayname});
  36. userCount.should.equal(1, 'The user should have be created in the database');
  37. const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
  38. inviteCount.should.equal(1, 'The invite should be marked as used by the user');
  39. }
  40. it('MUST register a valid user with a valid invite', async () =>
  41. verifySuccessfulRegister({displayname: 'user', password: 'pass', invite: 'code'})
  42. );
  43. it('MUST register a username with unicode symbols and a valid invite', async () =>
  44. verifySuccessfulRegister({displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'})
  45. );
  46. });
  47. describe('1 Invalid Invites', () => {
  48. async function verifyRejectedInvite(invite, message) {
  49. const user = {displayname: 'user', password: 'pass', invite: 'code'};
  50. if (invite) {
  51. await util.insertInvite(invite, agent);
  52. user.invite = invite.code;
  53. }
  54. const res = await(util.registerUser(user, agent));
  55. util.verifyResponse(res, 422, message);
  56. const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
  57. inviteCount.should.equal(0, 'Invite should not be marked as used or received by the user');
  58. }
  59. it('MUST NOT register a nonexistant invite', async () =>
  60. verifyRejectedInvite(null, 'Invalid invite code.')
  61. );
  62. it('MUST NOT register a used invite', async () =>
  63. verifyRejectedInvite({code: 'code', used: new Date(), issuer: 'Mocha'}, 'Invite already used.')
  64. );
  65. it('MUST NOT register an expired invite', async () =>
  66. verifyRejectedInvite({code: 'code', expires: new Date(), issuer: 'Mocha'}, 'Invite expired.')
  67. );
  68. });
  69. describe('2 Invalid Displaynames', () => {
  70. async function verifyRejectedUsername(user, code, message) {
  71. const res = await util.registerUser(user, agent);
  72. util.verifyResponse(res, code, message);
  73. const inviteCount = await Invite.countDocuments({code: user.invite, recipient: canonicalize(user.displayname)});
  74. inviteCount.should.equal(0, 'The invite should not be inserted into the database after rejection');
  75. }
  76. it('MUST NOT register a duplicate username', async () => {
  77. await util.createTestInvites(2);
  78. const user0 = {displayname: 'user', password: 'pass', invite: 'code0'};
  79. const user1 = {displayname: 'user', password: 'diff', invite: 'code1'};
  80. await util.registerUser(user0, agent);
  81. return verifyRejectedUsername(user1, 422, 'Username in use.');
  82. });
  83. it('MUST NOT register a username with a duplicate canonical name', async () => {
  84. await util.createTestInvites(2);
  85. const user0 = {displayname: 'bigbird', password: 'pass', invite: 'code0'};
  86. const user1 = {displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'diff', invite: 'code1'};
  87. await util.registerUser(user0, agent);
  88. return verifyRejectedUsername(user1, 422, 'Username in use.');
  89. });
  90. it('MUST NOT register a username containing whitespace', async () => {
  91. await util.createTestInvites(3);
  92. const users = [
  93. {displayname: 'user name', password: 'pass', invite: 'code0'},
  94. {displayname: 'user name', password: 'pass', invite: 'code1'},
  95. {displayname: 'user name', password: 'pass', invite: 'code2'}
  96. ];
  97. const failMsg = 'displayname contains invalid characters.';
  98. return Promise.all(users.map(user => verifyRejectedUsername(user, 400, failMsg)));
  99. });
  100. it('MUST NOT register a username containing HTML', async () => {
  101. await util.createTestInvite();
  102. const user = {displayname: 'user<svg/onload=alert("XSS")>', password: 'pass', invite: 'code'};
  103. return verifyRejectedUsername(user, 400, 'displayname contains invalid characters.');
  104. });
  105. it('MUST NOT register a username with too many characters', async () => {
  106. await util.createTestInvite();
  107. const user = {displayname: '123456789_123456789_123456789_1234567', password: 'pass', invite: 'code'};
  108. return verifyRejectedUsername(user, 400, 'displayname too long.');
  109. });
  110. });
  111. });
  112. describe('/POST login', () => {
  113. async function verifySuccessfulLogin(credentials) {
  114. // Login with the agent
  115. const res = await util.login(credentials, agent);
  116. util.verifyResponse(res, 200, 'Logged in.');
  117. res.should.have.cookie('session.id');
  118. // Get /api/auth/whoami, which can only be viewed when logged in
  119. const whoami = await util.whoami(agent);
  120. whoami.should.have.status(200);
  121. }
  122. async function verifyFailedLogin(credentials) {
  123. const res = await util.login(credentials, agent);
  124. util.verifyResponse(res, 401, 'Unauthorized.');
  125. }
  126. describe('0 Valid Request', () => {
  127. it('SHOULD accept a valid user with a valid password', async () => {
  128. await util.createTestUser(agent);
  129. return verifySuccessfulLogin({displayname: 'user', password: 'pass'});
  130. });
  131. it('SHOULD accept a username instead of a displayname', async () => {
  132. await util.createTestUser(agent);
  133. return verifySuccessfulLogin({username: 'user', password: 'pass'});
  134. });
  135. it('SHOULD accept any non-normalized variant of a username with a valid password', async () => {
  136. await util.createTestInvite();
  137. await util.registerUser({displayname: 'ᴮᴵᴳᴮᴵᴿᴰ', password: 'pass', invite: 'code'}, agent);
  138. return verifySuccessfulLogin({displayname: 'BiGbIrD', password: 'pass'});
  139. });
  140. });
  141. describe('1 Invalid Password', () => {
  142. it('SHOULD NOT accept an invalid password', async () => {
  143. await util.createTestUser(agent);
  144. return verifyFailedLogin({displayname: 'user', password: 'bogus'});
  145. });
  146. });
  147. describe('2 Invalid User', () => {
  148. it('SHOULD NOT accept an invalid user', async () =>
  149. verifyFailedLogin({displayname: 'bogus', password: 'bogus'})
  150. );
  151. });
  152. });
  153. describe('/POST logout', () => {
  154. async function verifyNoSession() {
  155. const res = await util.whoami(agent);
  156. util.verifyResponse(res, 401, 'Unauthorized.');
  157. }
  158. describe('0 Valid Request', () => {
  159. it('must logout a user with a session', async () => {
  160. await util.createTestSession(agent);
  161. const res = await util.logout(agent);
  162. util.verifyResponse(res, 200, 'Logged out.');
  163. return verifyNoSession();
  164. });
  165. });
  166. describe('1 Invalid Session', () => {
  167. it('must not logout a user without a session', async () => {
  168. const res = await util.logout(agent);
  169. util.verifyResponse(res, 400, 'Not logged in.');
  170. return verifyNoSession();
  171. });
  172. });
  173. });
  174. describe('/POST whoami', () => {
  175. function verifyWhoami(res, username, displayname, scope, key) {
  176. res.should.have.status(200);
  177. res.body.should.be.a('object');
  178. res.body.should.have.property('username').equal(username);
  179. res.body.should.have.property('displayname').equal(displayname);
  180. res.body.should.have.property('scope').deep.equal(scope);
  181. res.body.should.have.property('key').equal(key);
  182. }
  183. describe('0 Valid Request', () => {
  184. it('must respond with a valid session', async () => {
  185. await util.createTestSession(agent);
  186. const res = await util.whoami(agent);
  187. verifyWhoami(res, 'user', 'user', ['file.upload'], null);
  188. return util.logout(agent);
  189. });
  190. it('must respond with a valid api key', async () => {
  191. await util.createTestKey(['file.upload']);
  192. const res = await util.whoami(agent, 'key');
  193. verifyWhoami(res, 'Mocha', 'Mocha', ['file.upload'], 'key');
  194. });
  195. });
  196. describe('1 Invalid Auth', () => {
  197. it('must not respond with an invalid session', async () => {
  198. const res = await util.whoami(agent);
  199. util.verifyResponse(res, 401, 'Unauthorized.');
  200. });
  201. it('must not respond with a banned user with a valid session', async () => {
  202. await util.createTestSession(agent);
  203. await util.setBanned('user', true);
  204. const res = await util.whoami(agent);
  205. util.verifyResponse(res, 403, 'Forbidden.');
  206. });
  207. it('must not respond with a banned users api key', async () => {
  208. await util.createTestUser(agent);
  209. await Promise.all([
  210. util.setBanned('user', true),
  211. util.insertKey({key: 'key', identifier: 'test', scope: ['file.upload'], issuer: 'user'})
  212. ]);
  213. const res = await util.whoami(agent, 'key');
  214. util.verifyResponse(res, 403, 'Forbidden.');
  215. });
  216. });
  217. });
  218. });
  219. describe('Uploading', () => {
  220. after(async () => util.clearDirectory(config.get('Upload.path')));
  221. describe('/POST upload', () => {
  222. async function verifySuccessfulUpload(file, key) {
  223. // Get file stats beforehand
  224. const fileHash = await util.fileHash(file);
  225. // Submit the upload and verify the result
  226. const res = await util.upload(file, agent, key);
  227. res.should.have.status(200);
  228. res.body.should.be.a('object');
  229. res.body.should.have.property('url');
  230. const idLength = config.get('Upload.idLength');
  231. res.body.should.have.property('uid').length(idLength, 'The UID should be a ' + idLength + ' letter lowercase string.');
  232. // Find the uploaded file in the database
  233. const upload = await Upload.findOne({uid: res.body.uid}, {_id: 0, uid: 1, file: 1});
  234. const uploadFile = upload.file.path;
  235. upload.should.be.a('object');
  236. upload.uid.should.equal(res.body.uid, 'The uploaded file in the database should exist and match the reponse ID.');
  237. // Verify the uploaded file is the same as the file now on disk
  238. const uploadHash = await util.fileHash(uploadFile);
  239. uploadHash.should.equal(fileHash, 'The uploaded file and the file on disk should have matching hashes.');
  240. }
  241. async function verifySuccessfulUserUpload(file, username) {
  242. // Get the user's stats beforehand
  243. const userBefore = await User.findOne({username: username}, {_id: 0, uploadCount: 1, uploadSize: 1});
  244. await verifySuccessfulUpload(file);
  245. // Verify the user's stats have been updated correctly
  246. const userAfter = await User.findOne({username: username}, {_id: 0, uploadCount: 1, uploadSize: 1});
  247. const fileSize = await util.fileSize(file);
  248. userAfter.uploadCount.should.equal(userBefore.uploadCount + 1, 'The users upload count should be incremented.');
  249. userAfter.uploadSize.should.equal(userBefore.uploadSize + fileSize, 'The users upload size should be properly increased.');
  250. }
  251. async function verifySuccessfulKeyUpload(file, key) {
  252. // Get the key's stats beforehand
  253. const keyBefore = await Key.findOne({key: key}, {_id: 0, uploadCount: 1, uploadSize: 1});
  254. await verifySuccessfulUpload(file, key);
  255. // Verify the key's stats have been updated correctly
  256. const keyAfter = await Key.findOne({key: key}, {_id: 0, uploadCount: 1, uploadSize: 1});
  257. const fileSize = await util.fileSize(file);
  258. keyAfter.uploadCount.should.equal(keyBefore.uploadCount + 1, 'The keys upload count should be incremented.');
  259. keyAfter.uploadSize.should.equal(keyBefore.uploadSize + fileSize, 'The keys upload size should be properly increased');
  260. }
  261. async function verifyFailedUpload(file, status, message, key) {
  262. const fileCountBefore = await util.directoryFileCount(config.get('Upload.path'));
  263. const uploadCountBefore = await Upload.countDocuments({});
  264. const res = await util.upload(file, agent, key);
  265. util.verifyResponse(res, status, message);
  266. const fileCountAfter = await util.directoryFileCount(config.get('Upload.path'));
  267. fileCountAfter.should.equal(fileCountBefore, 'File should not be written to disk');
  268. const uploadCountAfter = await Upload.countDocuments({});
  269. uploadCountAfter.should.equal(uploadCountBefore, 'No uploads should be written to the database');
  270. }
  271. describe('0 Valid Request', () => {
  272. it('SHOULD accept an upload from a valid session', async () => {
  273. await Promise.all([
  274. util.createTestSession(agent),
  275. util.createTestFile(2048, 'test.bin')
  276. ]);
  277. await verifySuccessfulUserUpload('test.bin', 'user');
  278. return Promise.all([
  279. util.logout(agent),
  280. util.deleteFile('test.bin')
  281. ]);
  282. });
  283. it('SHOULD accept an upload from a valid api key', async () => {
  284. await Promise.all([
  285. util.createTestKey(['file.upload']),
  286. util.createTestFile(2048, 'test.bin')
  287. ]);
  288. await verifySuccessfulKeyUpload('test.bin', 'key');
  289. return util.deleteFile('test.bin');
  290. })
  291. });
  292. describe('1 Invalid Authentication', () => {
  293. it('SHOULD NOT accept an unauthenticated request', async () => {
  294. await util.createTestFile(2048, 'test.bin');
  295. await verifyFailedUpload('test.bin', 401, 'Unauthorized.');
  296. return util.deleteFile('test.bin');
  297. });
  298. it('SHOULD NOT accept a session request without file.upload scope', async () => {
  299. await util.insertInvite({code: 'code', scope: [], issuer: 'Mocha'});
  300. await util.registerUser({displayname: 'user', password: 'pass', invite: 'code'}, agent);
  301. await util.login({displayname: 'user', password: 'pass'}, agent);
  302. await util.createTestFile(2048, 'test.bin');
  303. await verifyFailedUpload('test.bin', 403, 'Forbidden.');
  304. return Promise.all([
  305. util.logout(agent),
  306. util.deleteFile('test.bin')
  307. ]);
  308. });
  309. it('SHOULD NOT accept a key request without file.upload scope', async () => {
  310. await Promise.all([
  311. util.createTestKey([]),
  312. util.createTestFile(2048, 'test.bin')
  313. ]);
  314. await verifyFailedUpload('test.bin', 403, 'Forbidden.', 'key');
  315. return util.deleteFile('test.bin');
  316. })
  317. });
  318. describe('3 Invalid File', () => {
  319. before(() => util.createTestFile(config.get('Upload.maxSize') + 1024, 'large.bin'));
  320. after(() => util.deleteFile('large.bin'));
  321. it('SHOULD NOT accept a too large file', async () => {
  322. await util.createTestSession(agent);
  323. await verifyFailedUpload('large.bin', 413, 'File too large.');
  324. return util.logout(agent);
  325. });
  326. });
  327. describe('4 Malformed Request', () => {
  328. it('SHOULD NOT accept a request with no file attached', async () => {
  329. await util.createTestSession(agent);
  330. await verifyFailedUpload(null, 400, 'Bad request.');
  331. return util.logout(agent);
  332. });
  333. it('must only accept one file from a request with multiple files attached', async () => {
  334. await Promise.all([
  335. util.createTestFile(2048, 'test1.bin'),
  336. util.createTestFile(2048, 'test2.bin'),
  337. util.createTestSession(agent)
  338. ]);
  339. const fileCountBefore = await util.directoryFileCount(config.get('Upload.path'));
  340. const uploadCountBefore = await Upload.countDocuments({});
  341. const res = await agent.post('/api/upload')
  342. .attach('file', 'test1.bin', 'test1.bin')
  343. .attach('file1', 'test2.bin', 'test2.bin');
  344. util.verifyResponse(res, 200, 'File uploaded.');
  345. const fileCountAfter = await util.directoryFileCount(config.get('Upload.path'));
  346. fileCountAfter.should.equal(fileCountBefore + 1, 'Only one file should be written to the disk');
  347. const uploadCountAfter = await Upload.countDocuments({});
  348. uploadCountAfter.should.equal(uploadCountBefore + 1, 'Only one upload should be written to the database');
  349. return Promise.all([
  350. util.deleteFile('test1.bin'),
  351. util.deleteFile('test2.bin')
  352. ]);
  353. })
  354. })
  355. });
  356. });
  357. describe('Viewing', () => {
  358. async function verifyView(file, uid, disposition) {
  359. const uploadViewsBefore = (await Upload.findOne({uid: uid})).views;
  360. const viewsBefore = await View.countDocuments();
  361. const res = await util.view(uid, agent)
  362. .parse(util.binaryFileParser);
  363. res.should.have.status(200);
  364. res.should.have.header('content-disposition', disposition);
  365. const [uploadHash, downloadHash] = await Promise.all([
  366. util.fileHash(file),
  367. util.bufferHash(res.body)
  368. ]);
  369. downloadHash.should.equal(uploadHash, 'Uploaded file and downloaded hash should match');
  370. const viewsAfter = await View.countDocuments();
  371. const uploadViewsAfter = (await Upload.findOne({uid: uid})).views;
  372. uploadViewsAfter.should.equal(uploadViewsBefore + 1, 'The files views should be incremented.');
  373. viewsAfter.should.equal(viewsBefore + 1, 'A view object should have been inserted to the database.');
  374. }
  375. it('must return an uploaded binary file', async () => {
  376. await Promise.all([
  377. util.createTestSession(agent),
  378. util.createTestFile(2048, 'test.bin')
  379. ]);
  380. const upload = await util.upload('test.bin', agent);
  381. await verifyView('test.bin', upload.body.uid, 'attachment; filename="test.bin"');
  382. return util.deleteFile('test.bin');
  383. });
  384. it('must return an uploaded image file inline', async () => {
  385. await Promise.all([
  386. util.createTestSession(agent),
  387. util.createTestFile(2048, 'test.jpg')
  388. ]);
  389. const upload = await util.upload('test.jpg', agent);
  390. await verifyView('test.jpg', upload.body.uid, 'inline');
  391. return util.deleteFile('test.jpg');
  392. });
  393. it('must return an error when file not found', async () => {
  394. const res = await util.view('abcdef', agent);
  395. util.verifyResponse(res, 404, 'File not found.');
  396. });
  397. });
  398. describe('Invites', () => {
  399. describe('/POST create', () => {
  400. async function verifyCreatedInvite(invite) {
  401. const res = await util.createInvite(invite, agent);
  402. util.verifyResponse(res, 200, 'Invite created.');
  403. res.body.should.have.property('code').match(/^[A-Fa-f0-9]+$/, 'The invite should be a hex string.');
  404. const dbInvite = await Invite.findOne({code: res.body.code});
  405. dbInvite.should.not.equal(null);
  406. dbInvite.scope.should.deep.equal(invite.scope, 'The created invites scope should match the request.');
  407. dbInvite.issuer.should.equal('user');
  408. }
  409. describe('0 Valid Request', () => {
  410. it('SHOULD create an invite with valid scope from a valid session', async () => {
  411. await util.createSession(agent, ['invite.create', 'file.upload']);
  412. return verifyCreatedInvite({scope: ['file.upload']});
  413. });
  414. });
  415. describe('1 Invalid Scope', () => {
  416. it('SHOULD NOT create in invite without invite.create scope', async () => {
  417. await util.createSession(agent, ['file.upload']);
  418. const res = await util.createInvite({scope: ['file.upload']}, agent);
  419. util.verifyResponse(res, 403, 'Forbidden.');
  420. });
  421. it('SHOULD NOT create an invite with a scope exceeding the requesters', async () => {
  422. await util.createSession(agent, ['invite.create', 'file.upload']);
  423. const res = await util.createInvite({scope: ['user.ban']}, agent);
  424. util.verifyResponse(res, 403, 'Requested scope exceeds own scope.');
  425. });
  426. });
  427. });
  428. describe('/POST delete', () => {
  429. async function verifyDeletedInvite(code) {
  430. const res = await util.deleteInvite(code, agent);
  431. util.verifyResponse(res, 200, 'Invite deleted.');
  432. const inviteCount = await Invite.countDocuments({code: code});
  433. inviteCount.should.equal(0, 'The invite should be removed from the database.');
  434. }
  435. describe('0 Valid Request', () => {
  436. it('SHOULD delete an invite with valid permission from a valid session', async () => {
  437. await util.createSession(agent, ['invite.create', 'invite.delete', 'file.upload']);
  438. const res = await util.createInvite({scope: ['file.upload']}, agent);
  439. return verifyDeletedInvite(res.body.code);
  440. });
  441. it('SHOULD delete another users invite with invite.delete.others scope', async () => {
  442. await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
  443. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  444. await util.logout(agent);
  445. await util.createSession(agent, ['invite.create', 'invite.delete', 'invite.delete.others'], 'eve');
  446. return verifyDeletedInvite(invite.body.code);
  447. });
  448. it('SHOULD delete a usedinvite with invite.delete.used scope', async () => {
  449. await util.createSession(agent, ['invite.create', 'invite.delete', 'invite.delete.used', 'file.upload'], 'alice');
  450. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  451. await util.registerUser({displayname: 'bob', password: 'hunter2', invite: invite.body.code}, agent);
  452. return verifyDeletedInvite(invite.body.code);
  453. });
  454. });
  455. describe('1 Invalid Scope', () => {
  456. it('SHOULD NOT delete an invite without invite.delete scope', async () => {
  457. await util.createSession(agent, ['invite.create', 'file.upload']);
  458. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  459. const res = await util.deleteInvite(invite.body.code, agent);
  460. util.verifyResponse(res, 403, 'Forbidden.');
  461. });
  462. it('SHOULD NOT delete another users invite without invite.delete.others scope', async () => {
  463. await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
  464. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  465. await util.logout(agent);
  466. await util.createSession(agent, ['invite.create', 'invite.delete'], 'eve');
  467. const res = await util.deleteInvite(invite.body.code, agent);
  468. util.verifyResponse(res, 422, 'Invite not found.');
  469. });
  470. it('SHOULD NOT delete a used invite without invite.delete.used scope', async () => {
  471. await util.createSession(agent, ['invite.create', 'invite.delete', 'file.upload'], 'alice');
  472. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  473. await util.registerUser({displayname: 'bob', password: 'hunter2', invite: invite.body.code}, agent);
  474. const res = await util.deleteInvite(invite.body.code, agent);
  475. util.verifyResponse(res, 403, 'Forbidden to delete used invites.');
  476. });
  477. });
  478. describe('2 Invalid Code', () => {
  479. it('SHOULD return an error when the invite is not found', async () => {
  480. await util.createSession(agent, ['invite.delete']);
  481. const res = await util.deleteInvite('bogus', agent);
  482. util.verifyResponse(res, 422, 'Invite not found.');
  483. });
  484. });
  485. });
  486. describe('/POST get', () => {
  487. async function verifyInviteSearch(codes) {
  488. const res = await util.getInvites({}, agent);
  489. res.should.have.status(200);
  490. res.body.should.be.a('Array');
  491. codes.sort();
  492. const resCodes = res.body.map(invite => invite.code).sort();
  493. resCodes.should.deep.equal(codes, 'All invites should be present in result.');
  494. }
  495. async function verifySingleSearch(code) {
  496. const res = await util.getInvites({code: code}, agent);
  497. res.should.have.status(200);
  498. res.body.should.be.a('Array');
  499. res.body.should.have.length(1, 'Only one invite should be in the array');
  500. res.body[0].code.should.equal(code, 'The found invite should match the request code');
  501. }
  502. describe('0 Valid Request', () => {
  503. it('SHOULD get multiple invites from a valid session', async () => {
  504. await util.createSession(agent, ['invite.create', 'invite.get', 'file.upload']);
  505. const inv1 = await util.createInvite({scope: ['file.upload']}, agent);
  506. const inv2 = await util.createInvite({scope: ['invite.create']}, agent);
  507. return verifyInviteSearch([inv1.body.code, inv2.body.code]);
  508. });
  509. it('SHOULD get a single invite from a valid session', async () => {
  510. await util.createSession(agent, ['invite.create', 'invite.get', 'file.upload']);
  511. const inv = await util.createInvite({scope: ['file.upload']}, agent);
  512. return verifySingleSearch(inv.body.code);
  513. });
  514. it('SHOULD get another users invite with invite.get.others scope', async () => {
  515. await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
  516. const inv = await util.createInvite({scope: ['file.upload']}, agent);
  517. await util.logout(agent);
  518. await util.createSession(agent, ['invite.get', 'invite.get.others'], 'eve');
  519. return verifySingleSearch(inv.body.code);
  520. });
  521. });
  522. describe('1 Invalid Scope', () => {
  523. it('SHOULD NOT get invites without invite.get scope', async () => {
  524. await util.createSession(agent, ['invite.create', 'file.upload']);
  525. const res = await util.getInvites({code: 'bogus'}, agent);
  526. util.verifyResponse(res, 403, 'Forbidden.');
  527. });
  528. it('SHOULD NOT get another users invite without invite.get.others scope', async () => {
  529. await util.createSession(agent, ['invite.create', 'file.upload'], 'alice');
  530. const invite = await util.createInvite({scope: ['file.upload']}, agent);
  531. await util.logout(agent);
  532. await util.createSession(agent, ['invite.get'], 'eve');
  533. const res = await util.getInvites({code: invite.body.code}, agent);
  534. res.should.have.status(200);
  535. res.body.should.be.a('Array');
  536. res.body.should.have.length(0, 'No invites should be found.');
  537. });
  538. });
  539. });
  540. });
  541. describe('Keys', () => {
  542. describe('/POST create', () => {
  543. async function verifyCreatedKey(key) {
  544. const res = await util.createKey(key, agent);
  545. util.verifyResponse(res, 200, 'Key created.');
  546. res.body.should.have.property('key').match(/^[A-Fa-f0-9]+$/, 'The key should be a hex string');
  547. const dbKey = await Key.findOne({key: res.body.key});
  548. dbKey.should.not.equal(null);
  549. dbKey.scope.should.deep.equal(key.scope, 'The created keys scope should match the request.');
  550. dbKey.issuer.should.equal('user');
  551. }
  552. describe('0 Valid Request', () => {
  553. it('SHOULD create a key with valid scope from a valid session', async () => {
  554. await util.createSession(agent, ['key.create', 'file.upload']);
  555. return verifyCreatedKey({identifier: 'key', scope: ['file.upload']});
  556. });
  557. });
  558. describe('1 Invalid Scope', () => {
  559. it('SHOULD NOT create a key without key.create scope', async () => {
  560. await util.createSession(agent, ['file.upload']);
  561. const res = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  562. util.verifyResponse(res, 403, 'Forbidden.');
  563. });
  564. it('SHOULD NOT create a key with scope exceeding the requesters', async () => {
  565. await util.createSession(agent, ['key.create']);
  566. const res = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  567. util.verifyResponse(res, 403, 'Requested scope exceeds own scope.');
  568. });
  569. });
  570. describe('2 Key Limit', () => {
  571. it('must not create additional keys beyond the limit', async () => {
  572. await util.createSession(agent, ['key.create', 'file.upload']);
  573. const limit = config.get('Key.limit');
  574. // Create keys upto the limit (key0, key1, key2, ...)
  575. await Promise.all(
  576. [...Array(limit)]
  577. .map(idx => util.createKey({identifier: 'key' + idx, scope: ['file.upload']}, agent)));
  578. const res = await util.createKey({identifier: 'toomany', scope: ['file.upload']}, agent);
  579. util.verifyResponse(res, 403, 'Key limit reached.');
  580. });
  581. });
  582. });
  583. describe('/POST delete', () => {
  584. async function verifyDeletedKey(key) {
  585. const res = await util.deleteKey(key, agent);
  586. util.verifyResponse(res, 200, 'Key deleted.');
  587. const keyCount = await Key.countDocuments({key: key});
  588. keyCount.should.equal(0, 'The key should be removed from the database.');
  589. }
  590. describe('0 Valid Request', () => {
  591. it('SHOULD delete a key with valid scope from a valid session', async () => {
  592. await util.createSession(agent, ['key.create', 'key.delete', 'file.upload']);
  593. const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  594. return verifyDeletedKey(key.body.key);
  595. });
  596. it('SHOULD delete another users key with key.delete.others scope', async () => {
  597. await util.createSession(agent, ['key.create', 'file.upload'], 'alice');
  598. const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  599. await util.logout(agent);
  600. await util.createSession(agent, ['key.delete', 'key.delete.others'], 'eve');
  601. return verifyDeletedKey(key.body.key);
  602. });
  603. });
  604. describe('1 Invalid Scope', () => {
  605. it('SHOULD NOT delete another users key without key.delete.others scope', async () => {
  606. await util.createSession(agent, ['key.create', 'file.upload'], 'bob');
  607. const key = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  608. await util.logout(agent);
  609. await util.createSession(agent, ['key.delete'], 'eve');
  610. const res = await util.deleteKey(key.body.key, agent);
  611. util.verifyResponse(res, 422, 'Key not found.');
  612. });
  613. });
  614. describe('2 Invalid Key', () => {
  615. it('SHOULD return an error when the key was not found', async () => {
  616. await util.createSession(agent, ['key.delete']);
  617. const res = await util.deleteKey('bogus', agent);
  618. util.verifyResponse(res, 422, 'Key not found.');
  619. });
  620. });
  621. });
  622. describe('/POST get', () => {
  623. async function verifyKeySearch(keys, query = {}) {
  624. const res = await util.getKeys(query, agent);
  625. res.should.have.status(200);
  626. res.body.should.be.a('Array');
  627. keys.sort();
  628. const resKeys = res.body.map(key => key.key).sort();
  629. resKeys.should.deep.equal(keys, 'All keys should be present in the result.');
  630. }
  631. async function verifySingleSearch(key, query = {}) {
  632. const res = await util.getKeys(query, agent);
  633. res.should.have.status(200);
  634. res.body.should.be.a('Array');
  635. res.body.should.have.length(1, 'Only one key should be in the array');
  636. res.body[0].key.should.equal(key, 'The found key should match the request code');
  637. }
  638. describe('0 Valid Request', () => {
  639. it('SHOULD get multiple keys from a valid session', async () => {
  640. await util.createSession(agent, ['key.create', 'key.get', 'file.upload']);
  641. const keys = await Promise.all([
  642. util.createKey({identifier: 'key1', scope: ['file.upload']}, agent),
  643. util.createKey({identifier: 'key2', scope: ['file.upload']}, agent)
  644. ]);
  645. return verifyKeySearch(keys.map(res => res.body.key));
  646. });
  647. it('SHOULD get a key by identifier from a valid session', async () => {
  648. await util.createSession(agent, ['key.create', 'key.get', 'file.upload']);
  649. const keys = await Promise.all([
  650. util.createKey({identifier: 'key1', scope: ['file.upload']}, agent),
  651. util.createKey({identifier: 'key2', scope: ['file.upload']}, agent)
  652. ]);
  653. return verifySingleSearch(keys[1].body.key, {identifier: 'key2'});
  654. });
  655. it('SHOULD get another users key with key.get.others scope', async () => {
  656. await util.createSession(agent, ['key.create', 'file.upload'], 'bob');
  657. const key1 = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  658. await util.logout(agent);
  659. await util.createSession(agent, ['key.create', 'key.get', 'key.get.others', 'file.upload']);
  660. const key2 = await util.createKey({identifier: 'key', scope: ['file.upload']}, agent);
  661. return verifyKeySearch([key1.body.key, key2.body.key]);
  662. });
  663. });
  664. describe('1 Invalid Scope', () => {
  665. it('SHOULD NOT get another users key without key.get.others scope', async () => {
  666. await util.createSession(agent, ['key.create', 'file.upload'], 'alice');
  667. await util.createInvite({identifier: 'private_key', scope: ['file.upload']}, agent);
  668. await util.logout(agent);
  669. await util.createSession(agent, ['key.get'], 'eve');
  670. const res = await util.getKeys({identifier: 'private_key'}, agent);
  671. res.should.have.status(200);
  672. res.body.should.be.a('Array');
  673. res.body.should.have.length(0, 'No invites should be found.');
  674. });
  675. });
  676. });
  677. });
  678. describe('Users', () => {
  679. describe('/GET get', () => {
  680. beforeEach(() => Promise.all([
  681. Promise.all([
  682. util.insertInvite({code: 'test1', scope: ['file.upload'], issuer: 'Mocha'}),
  683. util.insertInvite({code: 'test2', scope: ['file.upload'], issuer: 'Mocha'})
  684. ]),
  685. Promise.all([
  686. util.registerUser({displayname: 'user1', password: 'pass', invite: 'test1'}, agent),
  687. util.registerUser({displayname: 'user2', password: 'pass', invite: 'test2'}, agent)
  688. ])
  689. ]));
  690. async function verifyGetUsers(res, users) {
  691. res.should.have.status(200);
  692. res.body.should.be.a('Array');
  693. res.body.map(user => user.username).sort().should.deep.equal(users.sort());
  694. }
  695. describe('0 Valid Request', () => {
  696. it('must get all users with an empty query', async () => {
  697. await util.createSession(agent, ['user.get'], 'admin');
  698. const res = await util.getUsers({}, agent);
  699. return verifyGetUsers(res, ['user1', 'user2', 'admin']);
  700. });
  701. it('must filter users by username', async () => {
  702. await util.createSession(agent, ['user.get'], 'admin');
  703. const res = await util.getUsers({username: 'user1'}, agent);
  704. return verifyGetUsers(res, ['user1']);
  705. });
  706. it('must filter users by displayname', async () => {
  707. await util.createSession(agent, ['user.get'], 'admin');
  708. const res = await util.getUsers({displayname: 'user1'}, agent);
  709. return verifyGetUsers(res, ['user1']);
  710. });
  711. it('must return an empty array when no users were found', async () => {
  712. await util.createSession(agent, ['user.get'], 'admin');
  713. const res = await util.getUsers({username: 'abc'}, agent);
  714. return verifyGetUsers(res, []);
  715. });
  716. });
  717. });
  718. describe('/POST ban/unban', () => {
  719. beforeEach(async () => {
  720. await util.insertInvite({code: 'test1', scope: ['file.upload'], issuer: 'Mocha'});
  721. await util.registerUser({displayname: 'user', password: 'pass', invite: 'test1'}, agent);
  722. });
  723. async function verifyBanned(res, statusCode, message, username, banStatus) {
  724. util.verifyResponse(res, statusCode, message);
  725. const user = await User.findOne({username: username});
  726. user.should.exist;
  727. user.should.have.property('banned').equal(banStatus, 'The user should have banned status ' + banStatus);
  728. }
  729. describe('0 Valid Request', () => {
  730. it('must ban a not banned user', async () => {
  731. await util.createSession(agent, ['user.ban'], 'admin');
  732. const res = await util.ban('user', agent);
  733. return verifyBanned(res, 200, 'User banned.', 'user', true);
  734. });
  735. it('must unban a banned user', async () => {
  736. await util.setBanned('user', true);
  737. await util.createSession(agent, ['user.unban'], 'admin');
  738. const res = await util.unban('user', agent);
  739. return verifyBanned(res, 200, 'User unbanned.', 'user', false);
  740. });
  741. });
  742. describe('1 Already Requested Ban Status', () => {
  743. it('must not ban an already banned user', async () => {
  744. await util.setBanned('user', true);
  745. await util.createSession(agent, ['user.ban'], 'admin');
  746. const res = await util.ban('user', agent);
  747. return verifyBanned(res, 422, 'User already banned.', 'user', true);
  748. });
  749. it('must not unban a not banned user', async () => {
  750. await util.createSession(agent, ['user.unban'], 'admin');
  751. const res = await util.unban('user', agent);
  752. return verifyBanned(res, 422, 'User not banned.', 'user', false);
  753. });
  754. });
  755. describe('2 Not Found', () => {
  756. it('must not ban a nonexistant user', async () => {
  757. await util.createSession(agent, ['user.ban'], 'admin');
  758. const res = await util.ban('abc', agent);
  759. util.verifyResponse(res, 422, 'User not found.');
  760. });
  761. it('must not unban a nonexistant user', async () => {
  762. await util.createSession(agent, ['user.unban'], 'admin');
  763. const res = await util.unban('abc', agent);
  764. util.verifyResponse(res, 422, 'User not found.');
  765. });
  766. });
  767. });
  768. });
  769. describe('Stats', () => {
  770. const setupUploadsAndViews = async () => {
  771. const oneDay = 1000 * 60 * 60 * 24;
  772. const currentDate = new Date();
  773. const get_uid = i => {
  774. return 'abcde' + String.fromCharCode(i + 97)
  775. };
  776. for (let i = 0; i < 8; i++) {
  777. await util.insertUpload({
  778. uid: get_uid(i),
  779. views: 0,
  780. uploader: 'user',
  781. uploaderKey: null,
  782. date: new Date(currentDate - i * oneDay),
  783. file: {
  784. size: 1
  785. }
  786. });
  787. }
  788. await util.insertUpload({
  789. uid: 'zyxwvu',
  790. uploader: 'someguy',
  791. date: new Date(currentDate - 3 * oneDay),
  792. file: {
  793. size: 1
  794. }
  795. });
  796. for (let i = 0; i < 8; i++) {
  797. await util.insertView({
  798. uid: get_uid(i),
  799. uploader: 'user',
  800. remoteAddress: '::1',
  801. userAgent: 'fiyerfocks',
  802. date: new Date(currentDate - i * oneDay),
  803. });
  804. }
  805. await util.insertView({
  806. uid: 'zyxwvu',
  807. uploader: 'someguy',
  808. remoteAddress: '::1',
  809. userAgent: 'fiyerfocks',
  810. date: new Date(currentDate - 3 * oneDay)
  811. });
  812. };
  813. describe('/GET week', () => {
  814. describe('0 Valid Request', () => {
  815. it('must return valid stats for the past week', async () => {
  816. await setupUploadsAndViews();
  817. const oneDay = 1000 * 60 * 60 * 24;
  818. const currentDate = new Date();
  819. await util.createSession(agent, ['stats.get'], 'user');
  820. const stats = (await util.getStatsWeek(agent)).body;
  821. console.log(stats);
  822. for (let i = 0; i < 7; i++) {
  823. let date = new Date(currentDate - i * oneDay).toISOString();
  824. let dateStr = date.substr(5, 2) + '-' + date.substr(8, 2);
  825. let dayStats = stats[dateStr];
  826. dayStats.should.be.a('object', 'Stats should exist for the day ' + dateStr);
  827. dayStats.uploads.should.equal(1, 'Should be only one upload for the day ' + dateStr);
  828. dayStats.size.should.equal(1, 'Should be only one byte uploaded for the day ' + dateStr);
  829. dayStats.views.should.equal(1, 'Should be only one view for the day ' + dateStr);
  830. }
  831. let pastDate = new Date(currentDate - 7 * oneDay).toISOString();
  832. let pastDateStr = pastDate.substr(5, 2) + '-' + pastDate.substr(8, 2);
  833. stats.should.not.have.property(pastDateStr, 'No stats should exist past 1 week ago');
  834. return util.logout(agent);
  835. });
  836. it('must return an empty set when there are no stats', async () => {
  837. await util.createSession(agent, ['stats.get'], 'user');
  838. const stats_self = (await util.getStatsWeek(agent)).body;
  839. stats_self.should.deep.equal({});
  840. await setupUploadsAndViews();
  841. await util.createSession(agent, ['stats.get'], 'other_user');
  842. const stats_other = (await util.getStatsWeek(agent)).body;
  843. stats_other.should.deep.equal({}, 'No stats should be returned from another user');
  844. return util.logout(agent);
  845. });
  846. });
  847. });
  848. describe('/GET all', () => {
  849. describe('0 Valid Request', () => {
  850. it('must return valid stats for all time', async () => {
  851. await setupUploadsAndViews();
  852. await util.createSession(agent, ['stats.get'], 'user');
  853. const stats = (await util.getStatsAll(agent)).body;
  854. stats.should.have.property('total');
  855. stats.total.should.have.property('count').equal(8);
  856. stats.total.should.have.property('size').equal(8);
  857. stats.total.should.have.property('views').equal(8);
  858. return util.logout(agent);
  859. });
  860. it('must return an empty set when there are no stats', async () => {
  861. await util.createSession(agent, ['stats.get'], 'user');
  862. const stats_self = (await util.getStatsAll(agent)).body;
  863. stats_self.should.deep.equal({}, 'No stats should be returned');
  864. await util.logout(agent);
  865. await setupUploadsAndViews();
  866. await util.createSession(agent, ['stats.get'], 'other_user');
  867. const stats_other = (await util.getStatsAll(agent)).body;
  868. stats_other.should.deep.equal({}, 'No stats should be returned from another user');
  869. return util.logout(agent);
  870. });
  871. });
  872. });
  873. });
  874. after(() => server.close(() => process.exit(0)));