A simple file sharing site with an easy to use API and online panel.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

1207 строки
52KB

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