companion.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. const nock = require('nock')
  2. const request = require('supertest')
  3. const mockOauthState = require('../mockoauthstate')
  4. const { version } = require('../../package.json')
  5. const { nockGoogleDownloadFile } = require('../fixtures/drive')
  6. jest.mock('tus-js-client')
  7. jest.mock('../../src/server/helpers/oauth-state', () => ({
  8. ...jest.requireActual('../../src/server/helpers/oauth-state'),
  9. ...mockOauthState(),
  10. }))
  11. const fakeLocalhost = 'localhost.com'
  12. jest.mock('node:dns', () => {
  13. const actual = jest.requireActual('node:dns')
  14. return {
  15. ...actual,
  16. lookup: (hostname, options, callback) => {
  17. if (fakeLocalhost === hostname) {
  18. return callback(null, '127.0.0.1', 4)
  19. }
  20. return actual.lookup(hostname, options, callback)
  21. },
  22. }
  23. })
  24. const tokenService = require('../../src/server/helpers/jwt')
  25. const { getServer } = require('../mockserver')
  26. // todo don't share server between tests. rewrite to not use env variables
  27. const authServer = getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
  28. const authData = {
  29. dropbox: 'token value',
  30. box: 'token value',
  31. drive: 'token value',
  32. }
  33. const token = tokenService.generateEncryptedToken(authData, process.env.COMPANION_SECRET)
  34. const OAUTH_STATE = 'some-cool-nice-encrytpion'
  35. afterAll(() => {
  36. nock.cleanAll()
  37. nock.restore()
  38. })
  39. describe('validate upload data', () => {
  40. test('invalid upload protocol gets rejected', () => {
  41. nockGoogleDownloadFile()
  42. return request(authServer)
  43. .post('/drive/get/DUMMY-FILE-ID')
  44. .set('uppy-auth-token', token)
  45. .set('Content-Type', 'application/json')
  46. .send({
  47. endpoint: 'http://url.myendpoint.com/files',
  48. protocol: 'tusInvalid',
  49. })
  50. .expect(400)
  51. .then((res) => expect(res.body.message).toBe('unsupported protocol specified'))
  52. })
  53. test('invalid upload fieldname gets rejected', () => {
  54. nockGoogleDownloadFile()
  55. return request(authServer)
  56. .post('/drive/get/DUMMY-FILE-ID')
  57. .set('uppy-auth-token', token)
  58. .set('Content-Type', 'application/json')
  59. .send({
  60. endpoint: 'http://url.myendpoint.com/files',
  61. protocol: 'tus',
  62. fieldname: 390,
  63. })
  64. .expect(400)
  65. .then((res) => expect(res.body.message).toBe('fieldname must be a string'))
  66. })
  67. test('invalid upload metadata gets rejected', () => {
  68. nockGoogleDownloadFile()
  69. return request(authServer)
  70. .post('/drive/get/DUMMY-FILE-ID')
  71. .set('uppy-auth-token', token)
  72. .set('Content-Type', 'application/json')
  73. .send({
  74. endpoint: 'http://url.myendpoint.com/files',
  75. protocol: 'tus',
  76. metadata: 'I am a string instead of object',
  77. })
  78. .expect(400)
  79. .then((res) => expect(res.body.message).toBe('metadata must be an object'))
  80. })
  81. test('invalid upload headers get rejected', () => {
  82. nockGoogleDownloadFile()
  83. return request(authServer)
  84. .post('/drive/get/DUMMY-FILE-ID')
  85. .set('uppy-auth-token', token)
  86. .set('Content-Type', 'application/json')
  87. .send({
  88. endpoint: 'http://url.myendpoint.com/files',
  89. protocol: 'tus',
  90. headers: 'I am a string instead of object',
  91. })
  92. .expect(400)
  93. .then((res) => expect(res.body.message).toBe('headers must be an object'))
  94. })
  95. test('invalid upload HTTP Method gets rejected', () => {
  96. nockGoogleDownloadFile()
  97. return request(authServer)
  98. .post('/drive/get/DUMMY-FILE-ID')
  99. .set('uppy-auth-token', token)
  100. .set('Content-Type', 'application/json')
  101. .send({
  102. endpoint: 'http://url.myendpoint.com/files',
  103. protocol: 'tus',
  104. httpMethod: 'DELETE',
  105. })
  106. .expect(400)
  107. .then((res) => expect(res.body.message).toBe('unsupported HTTP METHOD specified'))
  108. })
  109. test('valid upload data is allowed - tus', () => {
  110. nockGoogleDownloadFile({ times: 2 })
  111. return request(authServer)
  112. .post('/drive/get/DUMMY-FILE-ID')
  113. .set('uppy-auth-token', token)
  114. .set('Content-Type', 'application/json')
  115. .send({
  116. endpoint: 'http://url.myendpoint.com/files',
  117. protocol: 'tus',
  118. httpMethod: 'POST',
  119. headers: {
  120. customheader: 'header value',
  121. },
  122. metadata: {
  123. mymetadata: 'matadata value',
  124. },
  125. fieldname: 'uploadField',
  126. })
  127. .expect(200)
  128. })
  129. test('valid upload data is allowed - s3-multipart', () => {
  130. nockGoogleDownloadFile({ times: 2 })
  131. return request(authServer)
  132. .post('/drive/get/DUMMY-FILE-ID')
  133. .set('uppy-auth-token', token)
  134. .set('Content-Type', 'application/json')
  135. .send({
  136. endpoint: 'http://url.myendpoint.com/files',
  137. protocol: 's3-multipart',
  138. httpMethod: 'PUT',
  139. headers: {
  140. customheader: 'header value',
  141. },
  142. metadata: {
  143. mymetadata: 'matadata value',
  144. },
  145. fieldname: 'uploadField',
  146. })
  147. .expect(200)
  148. })
  149. })
  150. describe('handle main oauth redirect', () => {
  151. const serverWithMainOauth = getServer({
  152. COMPANION_OAUTH_DOMAIN: 'localhost:3040',
  153. })
  154. test('redirect to a valid uppy instance', () => {
  155. return request(serverWithMainOauth)
  156. .get(`/dropbox/redirect?state=${OAUTH_STATE}`)
  157. .set('uppy-auth-token', token)
  158. .expect(302)
  159. .expect('Location', `http://localhost:3020/connect/dropbox/callback?state=${OAUTH_STATE}`)
  160. })
  161. test('do not redirect to invalid uppy instances', () => {
  162. const state = 'state-with-invalid-instance-url' // see mock ../../src/server/helpers/oauth-state above
  163. return request(serverWithMainOauth)
  164. .get(`/dropbox/redirect?state=${state}`)
  165. .set('uppy-auth-token', token)
  166. .expect(400)
  167. })
  168. })
  169. it('periodically pings', (done) => {
  170. nock('http://localhost').post('/ping', (body) => (
  171. body.some === 'value'
  172. && body.version === version
  173. && typeof body.processId === 'string'
  174. )).reply(200, () => done())
  175. getServer({
  176. COMPANION_PERIODIC_PING_URLS: 'http://localhost/ping',
  177. COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD: '{"some": "value"}',
  178. COMPANION_PERIODIC_PING_INTERVAL: '10',
  179. COMPANION_PERIODIC_PING_COUNT: '1',
  180. })
  181. }, 3000)
  182. async function runUrlMetaTest (url) {
  183. const server = getServer()
  184. return request(server)
  185. .post('/url/meta')
  186. .send({ url })
  187. }
  188. async function runUrlGetTest (url) {
  189. const server = getServer()
  190. return request(server)
  191. .post('/url/get')
  192. .send({
  193. fileId: url,
  194. metadata: {},
  195. endpoint: 'http://url.myendpoint.com/files',
  196. protocol: 'tus',
  197. size: null,
  198. url,
  199. })
  200. }
  201. it('respects allowLocalUrls, localhost', async () => {
  202. let res = await runUrlMetaTest('http://localhost/')
  203. expect(res.statusCode).toBe(400)
  204. expect(res.body).toEqual({ error: 'Invalid request body' })
  205. res = await runUrlGetTest('http://localhost/')
  206. expect(res.statusCode).toBe(400)
  207. expect(res.body).toEqual({ error: 'Invalid request body' })
  208. })
  209. it('respects allowLocalUrls, valid hostname that resolves to localhost', async () => {
  210. let res = await runUrlMetaTest(`http://${fakeLocalhost}/`)
  211. expect(res.statusCode).toBe(500)
  212. expect(res.body).toEqual({ message: 'failed to fetch URL metadata' })
  213. res = await runUrlGetTest(`http://${fakeLocalhost}/`)
  214. expect(res.statusCode).toBe(500)
  215. expect(res.body).toEqual({ message: 'failed to fetch URL' })
  216. })