attachmentApi.tests.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /* eslint-env mocha */
  2. import { expect } from 'chai';
  3. import sinon from 'sinon';
  4. import { Meteor } from 'meteor/meteor';
  5. import { Accounts } from 'meteor/accounts-base';
  6. describe('attachmentApi authentication', function() {
  7. let findOneStub, hashStub;
  8. beforeEach(function() {
  9. hashStub = sinon.stub(Accounts, '_hashLoginToken');
  10. findOneStub = sinon.stub(Meteor.users, 'findOne');
  11. });
  12. afterEach(function() {
  13. if (hashStub) hashStub.restore();
  14. if (findOneStub) findOneStub.restore();
  15. });
  16. // Mock request/response objects
  17. function createMockReq(headers = {}) {
  18. return {
  19. headers,
  20. on: sinon.stub(),
  21. connection: { destroy: sinon.stub() },
  22. };
  23. }
  24. function createMockRes() {
  25. return {
  26. writeHead: sinon.stub(),
  27. end: sinon.stub(),
  28. headersSent: false,
  29. };
  30. }
  31. describe('authenticateApiRequest', function() {
  32. it('denies request with missing X-User-Id header', function() {
  33. const req = createMockReq({ 'x-auth-token': 'sometoken' });
  34. const res = createMockRes();
  35. // Simulate the handler behavior
  36. let errorThrown = false;
  37. try {
  38. if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
  39. throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
  40. }
  41. } catch (error) {
  42. errorThrown = true;
  43. expect(error.error).to.equal('unauthorized');
  44. }
  45. expect(errorThrown).to.equal(true);
  46. });
  47. it('denies request with missing X-Auth-Token header', function() {
  48. const req = createMockReq({ 'x-user-id': 'user123' });
  49. let errorThrown = false;
  50. try {
  51. if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
  52. throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
  53. }
  54. } catch (error) {
  55. errorThrown = true;
  56. expect(error.error).to.equal('unauthorized');
  57. }
  58. expect(errorThrown).to.equal(true);
  59. });
  60. it('denies request with invalid token', function() {
  61. const userId = 'user123';
  62. const token = 'invalidtoken';
  63. const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
  64. hashStub.returns('hashedInvalidToken');
  65. findOneStub.returns(null); // No user found
  66. let errorThrown = false;
  67. try {
  68. const hashedToken = Accounts._hashLoginToken(token);
  69. const user = Meteor.users.findOne({
  70. _id: userId,
  71. 'services.resume.loginTokens.hashedToken': hashedToken,
  72. });
  73. if (!user) {
  74. throw new Meteor.Error('unauthorized', 'Invalid credentials');
  75. }
  76. } catch (error) {
  77. errorThrown = true;
  78. expect(error.error).to.equal('unauthorized');
  79. }
  80. expect(errorThrown).to.equal(true);
  81. expect(hashStub.calledOnce).to.equal(true);
  82. expect(findOneStub.calledOnce).to.equal(true);
  83. });
  84. it('allows request with valid X-User-Id and X-Auth-Token', function() {
  85. const userId = 'user123';
  86. const token = 'validtoken';
  87. const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
  88. const hashedToken = 'hashedValidToken';
  89. hashStub.returns(hashedToken);
  90. findOneStub.returns({ _id: userId }); // User found
  91. let authenticatedUserId = null;
  92. try {
  93. const hashed = Accounts._hashLoginToken(token);
  94. const user = Meteor.users.findOne({
  95. _id: userId,
  96. 'services.resume.loginTokens.hashedToken': hashed,
  97. });
  98. if (!user) {
  99. throw new Meteor.Error('unauthorized', 'Invalid credentials');
  100. }
  101. authenticatedUserId = userId;
  102. } catch (error) {
  103. // Should not throw
  104. }
  105. expect(authenticatedUserId).to.equal(userId);
  106. expect(hashStub.calledOnce).to.equal(true);
  107. expect(hashStub.calledWith(token)).to.equal(true);
  108. expect(findOneStub.calledOnce).to.equal(true);
  109. const queryArg = findOneStub.getCall(0).args[0];
  110. expect(queryArg._id).to.equal(userId);
  111. expect(queryArg['services.resume.loginTokens.hashedToken']).to.equal(hashedToken);
  112. });
  113. it('prevents identity spoofing by validating hashed token', function() {
  114. const victimId = 'victim-user-id';
  115. const attackerToken = 'attacker-token';
  116. const req = createMockReq({ 'x-user-id': victimId, 'x-auth-token': attackerToken });
  117. hashStub.returns('hashedAttackerToken');
  118. // Simulate victim exists but token doesn't match
  119. findOneStub.returns(null);
  120. let errorThrown = false;
  121. try {
  122. const hashed = Accounts._hashLoginToken(attackerToken);
  123. const user = Meteor.users.findOne({
  124. _id: victimId,
  125. 'services.resume.loginTokens.hashedToken': hashed,
  126. });
  127. if (!user) {
  128. throw new Meteor.Error('unauthorized', 'Invalid credentials');
  129. }
  130. } catch (error) {
  131. errorThrown = true;
  132. expect(error.error).to.equal('unauthorized');
  133. }
  134. expect(errorThrown).to.equal(true);
  135. });
  136. });
  137. describe('request handler DoS prevention', function() {
  138. it('enforces timeout on hanging requests', function(done) {
  139. this.timeout(5000);
  140. const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
  141. const res = createMockRes();
  142. // Simulate timeout behavior
  143. const timeout = setTimeout(() => {
  144. if (!res.headersSent) {
  145. res.headersSent = true;
  146. res.writeHead(408, { 'Content-Type': 'application/json' });
  147. res.end(JSON.stringify({ success: false, error: 'Request timeout' }));
  148. }
  149. }, 100); // Short timeout for test
  150. // Wait for timeout
  151. setTimeout(() => {
  152. expect(res.headersSent).to.equal(true);
  153. expect(res.writeHead.calledWith(408)).to.equal(true);
  154. clearTimeout(timeout);
  155. done();
  156. }, 150);
  157. });
  158. it('limits request body size', function() {
  159. const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
  160. let body = '';
  161. const limit = 50 * 1024 * 1024; // 50MB
  162. // Simulate exceeding limit
  163. body = 'a'.repeat(limit + 1);
  164. expect(body.length).to.be.greaterThan(limit);
  165. // Handler should destroy connection
  166. if (body.length > limit) {
  167. req.connection.destroy();
  168. }
  169. expect(req.connection.destroy.calledOnce).to.equal(true);
  170. });
  171. });
  172. });