companion.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /* global jest:false, test:false, expect:false, describe:false */
  2. jest.mock('tus-js-client')
  3. jest.mock('purest')
  4. jest.mock('../../src/server/helpers/oauth-state', () => {
  5. return {
  6. generateState: () => 'some-cool-nice-encrytpion',
  7. addToState: () => 'some-cool-nice-encrytpion',
  8. getFromState: (state, key) => {
  9. if (state === 'state-with-invalid-instance-url') {
  10. return 'http://localhost:3452'
  11. }
  12. if (state === 'state-with-older-version' && key === 'clientVersion') {
  13. return '@uppy/companion-client=1.0.1'
  14. }
  15. if (state === 'state-with-newer-version' && key === 'clientVersion') {
  16. return '@uppy/companion-client=1.0.3'
  17. }
  18. if (state === 'state-with-newer-version-old-style' && key === 'clientVersion') {
  19. return 'companion-client:1.0.2'
  20. }
  21. return 'http://localhost:3020'
  22. }
  23. }
  24. })
  25. const request = require('supertest')
  26. const tokenService = require('../../src/server/helpers/jwt')
  27. const { getServer } = require('../mockserver')
  28. const authServer = getServer()
  29. const authData = {
  30. dropbox: 'token value',
  31. drive: 'token value'
  32. }
  33. const token = tokenService.generateToken(authData, process.env.COMPANION_SECRET)
  34. const OAUTH_STATE = 'some-cool-nice-encrytpion'
  35. describe('set i-am header', () => {
  36. test('set i-am header in response', () => {
  37. return request(authServer)
  38. .get('/dropbox/list/')
  39. .set('uppy-auth-token', token)
  40. .expect(200)
  41. .then((res) => expect(res.header['i-am']).toBe('http://localhost:3020'))
  42. })
  43. })
  44. describe('list provider files', () => {
  45. test('list files for dropbox', () => {
  46. return request(authServer)
  47. .get('/dropbox/list/')
  48. .set('uppy-auth-token', token)
  49. .expect(200)
  50. .then((res) => expect(res.body.username).toBe('foo@bar.com'))
  51. })
  52. test('list files for google drive', () => {
  53. return request(authServer)
  54. .get('/drive/list/')
  55. .set('uppy-auth-token', token)
  56. .expect(200)
  57. .then((res) => expect(res.body.username).toBe('ife@bala.com'))
  58. })
  59. })
  60. describe('validate upload data', () => {
  61. test('invalid upload protocol gets rejected', () => {
  62. return request(authServer)
  63. .post('/drive/get/README.md')
  64. .set('uppy-auth-token', token)
  65. .set('Content-Type', 'application/json')
  66. .send({
  67. endpoint: 'http://url.myendpoint.com/files',
  68. protocol: 'tusInvalid'
  69. })
  70. .expect(400)
  71. .then((res) => expect(res.body.message).toBe('unsupported protocol specified'))
  72. })
  73. test('invalid upload fieldname gets rejected', () => {
  74. return request(authServer)
  75. .post('/drive/get/README.md')
  76. .set('uppy-auth-token', token)
  77. .set('Content-Type', 'application/json')
  78. .send({
  79. endpoint: 'http://url.myendpoint.com/files',
  80. protocol: 'tus',
  81. fieldname: 390
  82. })
  83. .expect(400)
  84. .then((res) => expect(res.body.message).toBe('fieldname must be a string'))
  85. })
  86. test('invalid upload metadata gets rejected', () => {
  87. return request(authServer)
  88. .post('/drive/get/README.md')
  89. .set('uppy-auth-token', token)
  90. .set('Content-Type', 'application/json')
  91. .send({
  92. endpoint: 'http://url.myendpoint.com/files',
  93. protocol: 'tus',
  94. metadata: 'I am a string instead of object'
  95. })
  96. .expect(400)
  97. .then((res) => expect(res.body.message).toBe('metadata must be an object'))
  98. })
  99. test('invalid upload headers get rejected', () => {
  100. return request(authServer)
  101. .post('/drive/get/README.md')
  102. .set('uppy-auth-token', token)
  103. .set('Content-Type', 'application/json')
  104. .send({
  105. endpoint: 'http://url.myendpoint.com/files',
  106. protocol: 'tus',
  107. headers: 'I am a string instead of object'
  108. })
  109. .expect(400)
  110. .then((res) => expect(res.body.message).toBe('headers must be an object'))
  111. })
  112. test('invalid upload HTTP Method gets rejected', () => {
  113. return request(authServer)
  114. .post('/drive/get/README.md')
  115. .set('uppy-auth-token', token)
  116. .set('Content-Type', 'application/json')
  117. .send({
  118. endpoint: 'http://url.myendpoint.com/files',
  119. protocol: 'tus',
  120. httpMethod: 'DELETE'
  121. })
  122. .expect(400)
  123. .then((res) => expect(res.body.message).toBe('unsupported HTTP METHOD specified'))
  124. })
  125. test('valid upload data is allowed - tus', () => {
  126. return request(authServer)
  127. .post('/drive/get/README.md')
  128. .set('uppy-auth-token', token)
  129. .set('Content-Type', 'application/json')
  130. .send({
  131. endpoint: 'http://url.myendpoint.com/files',
  132. protocol: 'tus',
  133. httpMethod: 'POST',
  134. headers: {
  135. customheader: 'header value'
  136. },
  137. metadata: {
  138. mymetadata: 'matadata value'
  139. },
  140. fieldname: 'uploadField'
  141. })
  142. .expect(200)
  143. })
  144. test('valid upload data is allowed - s3-multipart', () => {
  145. return request(authServer)
  146. .post('/drive/get/README.md')
  147. .set('uppy-auth-token', token)
  148. .set('Content-Type', 'application/json')
  149. .send({
  150. endpoint: 'http://url.myendpoint.com/files',
  151. protocol: 's3-multipart',
  152. httpMethod: 'PUT',
  153. headers: {
  154. customheader: 'header value'
  155. },
  156. metadata: {
  157. mymetadata: 'matadata value'
  158. },
  159. fieldname: 'uploadField'
  160. })
  161. .expect(200)
  162. })
  163. })
  164. describe('download provdier file', () => {
  165. test('specified file gets downloaded from provider', () => {
  166. return request(authServer)
  167. .post('/drive/get/README.md')
  168. .set('uppy-auth-token', token)
  169. .set('Content-Type', 'application/json')
  170. .send({
  171. endpoint: 'http://master.tus.io/files',
  172. protocol: 'tus'
  173. })
  174. .expect(200)
  175. .then((res) => expect(res.body.token).toBeTruthy())
  176. })
  177. })
  178. describe('test authentication', () => {
  179. test('authentication callback redirects to send-token url', () => {
  180. return request(authServer)
  181. .get('/drive/callback')
  182. .expect(302)
  183. .expect((res) => {
  184. expect(res.header.location).toContain('http://localhost:3020/drive/send-token?uppyAuthToken=')
  185. })
  186. })
  187. test('the token gets sent via cookie and html', () => {
  188. // see mock ../../src/server/helpers/oauth-state above for state values
  189. return request(authServer)
  190. .get(`/dropbox/send-token?uppyAuthToken=${token}&state=state-with-newer-version`)
  191. .expect(200)
  192. .expect((res) => {
  193. const authToken = res.header['set-cookie'][0].split(';')[0].split('uppyAuthToken--dropbox=')[1]
  194. expect(authToken).toEqual(token)
  195. const body = `
  196. <!DOCTYPE html>
  197. <html>
  198. <head>
  199. <meta charset="utf-8" />
  200. <script>
  201. window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
  202. window.close()
  203. </script>
  204. </head>
  205. <body></body>
  206. </html>`
  207. expect(res.text).toBe(body)
  208. })
  209. })
  210. test('the token gets to older clients without stringify', () => {
  211. // see mock ../../src/server/helpers/oauth-state above for state values
  212. return request(authServer)
  213. .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-older-version`)
  214. .expect(200)
  215. .expect((res) => {
  216. const body = `
  217. <!DOCTYPE html>
  218. <html>
  219. <head>
  220. <meta charset="utf-8" />
  221. <script>
  222. window.opener.postMessage({token: "${token}"}, "http://localhost:3020")
  223. window.close()
  224. </script>
  225. </head>
  226. <body></body>
  227. </html>`
  228. expect(res.text).toBe(body)
  229. })
  230. })
  231. test('the token gets sent to newer clients with old version style', () => {
  232. // see mock ../../src/server/helpers/oauth-state above for state values
  233. return request(authServer)
  234. .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version-old-style`)
  235. .expect(200)
  236. .expect((res) => {
  237. const body = `
  238. <!DOCTYPE html>
  239. <html>
  240. <head>
  241. <meta charset="utf-8" />
  242. <script>
  243. window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
  244. window.close()
  245. </script>
  246. </head>
  247. <body></body>
  248. </html>`
  249. expect(res.text).toBe(body)
  250. })
  251. })
  252. test('logout provider', () => {
  253. return request(authServer)
  254. .get('/drive/logout/')
  255. .set('uppy-auth-token', token)
  256. .expect(200)
  257. .then((res) => expect(res.body.ok).toBe(true))
  258. })
  259. })
  260. describe('connect to provider', () => {
  261. test('connect to dropbox via grant.js endpoint', () => {
  262. return request(authServer)
  263. .get('/dropbox/connect?foo=bar')
  264. .set('uppy-auth-token', token)
  265. .expect(302)
  266. .expect('Location', `http://localhost:3020/connect/dropbox?state=${OAUTH_STATE}`)
  267. })
  268. test('connect to drive via grant.js endpoint', () => {
  269. return request(authServer)
  270. .get('/drive/connect?foo=bar')
  271. .set('uppy-auth-token', token)
  272. .expect(302)
  273. .expect('Location', `http://localhost:3020/connect/google?state=${OAUTH_STATE}`)
  274. })
  275. })
  276. describe('handle master oauth redirect', () => {
  277. const serverWithMasterOauth = getServer({
  278. COMPANION_OAUTH_DOMAIN: 'localhost:3040'
  279. })
  280. test('redirect to a valid uppy instance', () => {
  281. return request(serverWithMasterOauth)
  282. .get(`/dropbox/redirect?state=${OAUTH_STATE}`)
  283. .set('uppy-auth-token', token)
  284. .expect(302)
  285. .expect('Location', `http://localhost:3020/connect/dropbox/callback?state=${OAUTH_STATE}`)
  286. })
  287. test('do not redirect to invalid uppy instances', () => {
  288. const state = 'state-with-invalid-instance-url' // see mock ../../src/server/helpers/oauth-state above
  289. return request(serverWithMasterOauth)
  290. .get(`/dropbox/redirect?state=${state}`)
  291. .set('uppy-auth-token', token)
  292. .expect(400)
  293. })
  294. })
  295. // @todo consider moving this to a separate test file (zoom.js maybe)
  296. // in general, we should consider testing all providers endpoints separately
  297. describe('handle deauthorization callback', () => {
  298. test('providers without support for callback endpoint', () => {
  299. return request(authServer)
  300. .post('/dropbox/deauthorization/callback')
  301. .set('Content-Type', 'application/json')
  302. .send({
  303. foo: 'bar'
  304. })
  305. // @todo consider receiving 501 instead
  306. .expect(500)
  307. })
  308. test('validate that request credentials match', () => {
  309. return request(authServer)
  310. .post('/zoom/deauthorization/callback')
  311. .set('Content-Type', 'application/json')
  312. .set('Authorization', 'wrong-verfication-token')
  313. .send({
  314. event: 'app_deauthorized',
  315. payload: {
  316. user_data_retention: 'false',
  317. account_id: 'EabCDEFghiLHMA',
  318. user_id: 'z9jkdsfsdfjhdkfjQ',
  319. signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
  320. deauthorization_time: '2019-06-17T13:52:28.632Z',
  321. client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
  322. }
  323. })
  324. .expect(400)
  325. })
  326. test('validate request credentials is present', () => {
  327. // Authorization header is absent
  328. return request(authServer)
  329. .post('/zoom/deauthorization/callback')
  330. .set('Content-Type', 'application/json')
  331. .send({
  332. event: 'app_deauthorized',
  333. payload: {
  334. user_data_retention: 'false',
  335. account_id: 'EabCDEFghiLHMA',
  336. user_id: 'z9jkdsfsdfjhdkfjQ',
  337. signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
  338. deauthorization_time: '2019-06-17T13:52:28.632Z',
  339. client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
  340. }
  341. })
  342. .expect(400)
  343. })
  344. test('validate request content', () => {
  345. return request(authServer)
  346. .post('/zoom/deauthorization/callback')
  347. .set('Content-Type', 'application/json')
  348. .set('Authorization', 'zoom_verfication_token')
  349. .send({
  350. invalid: 'content'
  351. })
  352. .expect(400)
  353. })
  354. test('validate request content (event name)', () => {
  355. return request(authServer)
  356. .post('/zoom/deauthorization/callback')
  357. .set('Content-Type', 'application/json')
  358. .set('Authorization', 'zoom_verfication_token')
  359. .send({
  360. event: 'wrong_event_name',
  361. payload: {
  362. user_data_retention: 'false',
  363. account_id: 'EabCDEFghiLHMA',
  364. user_id: 'z9jkdsfsdfjhdkfjQ',
  365. signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
  366. deauthorization_time: '2019-06-17T13:52:28.632Z',
  367. client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
  368. }
  369. })
  370. .expect(400)
  371. })
  372. test('allow valid request', () => {
  373. return request(authServer)
  374. .post('/zoom/deauthorization/callback')
  375. .set('Content-Type', 'application/json')
  376. .set('Authorization', 'zoom_verfication_token')
  377. .send({
  378. event: 'app_deauthorized',
  379. payload: {
  380. user_data_retention: 'false',
  381. account_id: 'EabCDEFghiLHMA',
  382. user_id: 'z9jkdsfsdfjhdkfjQ',
  383. signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
  384. deauthorization_time: '2019-06-17T13:52:28.632Z',
  385. client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
  386. }
  387. })
  388. .expect(200)
  389. })
  390. })