浏览代码

companion: add more test cases to companion tests (#2585)

Ifedapo .A. Olarewaju 4 年之前
父节点
当前提交
9c78b171bd
共有 27 个文件被更改,包括 918 次插入611 次删除
  1. 5 6
      packages/@uppy/companion/src/server/controllers/url.js
  2. 33 0
      packages/@uppy/companion/src/server/helpers/request.js
  3. 0 35
      packages/@uppy/companion/src/server/helpers/utils.js
  4. 2 0
      packages/@uppy/companion/src/server/provider/drive/adapter.js
  5. 3 2
      packages/@uppy/companion/src/server/provider/facebook/index.js
  6. 0 74
      packages/@uppy/companion/src/server/provider/instagram/adapter.js
  7. 3 2
      packages/@uppy/companion/src/server/provider/instagram/graph/index.js
  8. 0 169
      packages/@uppy/companion/src/server/provider/instagram/index.js
  9. 2 2
      packages/@uppy/companion/src/server/provider/zoom/index.js
  10. 14 39
      packages/@uppy/companion/test/__mocks__/purest.js
  11. 14 0
      packages/@uppy/companion/test/__mocks__/tus-js-client.js
  12. 92 0
      packages/@uppy/companion/test/__tests__/callback.js
  13. 8 282
      packages/@uppy/companion/test/__tests__/companion.js
  14. 105 0
      packages/@uppy/companion/test/__tests__/deauthorization.js
  15. 106 0
      packages/@uppy/companion/test/__tests__/providers.js
  16. 60 0
      packages/@uppy/companion/test/__tests__/uploader.js
  17. 72 0
      packages/@uppy/companion/test/__tests__/url.js
  18. 9 0
      packages/@uppy/companion/test/fixtures/constants.js
  19. 49 0
      packages/@uppy/companion/test/fixtures/drive.js
  20. 64 0
      packages/@uppy/companion/test/fixtures/dropbox.js
  21. 56 0
      packages/@uppy/companion/test/fixtures/facebook.js
  22. 10 0
      packages/@uppy/companion/test/fixtures/index.js
  23. 39 0
      packages/@uppy/companion/test/fixtures/instagram.js
  24. 61 0
      packages/@uppy/companion/test/fixtures/onedrive.js
  25. 65 0
      packages/@uppy/companion/test/fixtures/zoom.js
  26. 25 0
      packages/@uppy/companion/test/mockoauthstate.js
  27. 21 0
      packages/@uppy/companion/test/mocksocket.js

+ 5 - 6
packages/@uppy/companion/src/server/controllers/url.js

@@ -3,8 +3,7 @@ const request = require('request')
 const { URL } = require('url')
 const Uploader = require('../Uploader')
 const validator = require('validator')
-const utils = require('../helpers/utils')
-const { getProtectedHttpAgent, getRedirectEvaluator } = require('../helpers/request')
+const reqUtil = require('../helpers/request')
 const logger = require('../logger')
 
 module.exports = () => {
@@ -27,7 +26,7 @@ const meta = (req, res) => {
     return res.status(400).json({ error: 'Invalid request body' })
   }
 
-  utils.getURLMeta(req.body.url, !debug)
+  reqUtil.getURLMeta(req.body.url, !debug)
     .then((meta) => res.json(meta))
     .catch((err) => {
       logger.error(err, 'controller.url.meta.error', req.id)
@@ -51,7 +50,7 @@ const get = (req, res) => {
     return res.status(400).json({ error: 'Invalid request body' })
   }
 
-  utils.getURLMeta(req.body.url, !debug)
+  reqUtil.getURLMeta(req.body.url, !debug)
     .then(({ size }) => {
       // @ts-ignore
       logger.debug('Instantiating uploader.', null, req.id)
@@ -119,8 +118,8 @@ const downloadURL = (url, onDataChunk, blockLocalIPs, traceId) => {
   const opts = {
     uri: url,
     method: 'GET',
-    followRedirect: getRedirectEvaluator(url, blockLocalIPs),
-    agentClass: getProtectedHttpAgent((new URL(url)).protocol, blockLocalIPs)
+    followRedirect: reqUtil.getRedirectEvaluator(url, blockLocalIPs),
+    agentClass: reqUtil.getProtectedHttpAgent((new URL(url)).protocol, blockLocalIPs)
   }
 
   request(opts)

+ 33 - 0
packages/@uppy/companion/src/server/helpers/request.js

@@ -3,6 +3,7 @@ const https = require('https')
 const { URL } = require('url')
 const dns = require('dns')
 const ipAddress = require('ip-address')
+const request = require('request')
 const logger = require('../logger')
 const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
 
@@ -150,3 +151,35 @@ class HttpsAgent extends https.Agent {
     return super.createConnection(options, callback)
   }
 }
+
+/**
+ * Gets the size and content type of a url's content
+ *
+ * @param {string} url
+ * @param {boolean=} blockLocalIPs
+ * @return {Promise}
+ */
+exports.getURLMeta = (url, blockLocalIPs = false) => {
+  return new Promise((resolve, reject) => {
+    const opts = {
+      uri: url,
+      method: 'HEAD',
+      followRedirect: exports.getRedirectEvaluator(url, blockLocalIPs),
+      agentClass: exports.getProtectedHttpAgent((new URL(url)).protocol, blockLocalIPs)
+    }
+
+    request(opts, (err, response) => {
+      if (err || response.statusCode >= 300) {
+        // @todo possibly set a status code in the error object to get a more helpful
+        // hint at what the cause of error is.
+        err = err || new Error(`URL server responded with status: ${response.statusCode}`)
+        reject(err)
+      } else {
+        resolve({
+          type: response.headers['content-type'],
+          size: parseInt(response.headers['content-length'])
+        })
+      }
+    })
+  })
+}

+ 0 - 35
packages/@uppy/companion/src/server/helpers/utils.js

@@ -1,7 +1,4 @@
-const request = require('request')
-const { URL } = require('url')
 const crypto = require('crypto')
-const { getProtectedHttpAgent, getRedirectEvaluator } = require('./request')
 
 /**
  *
@@ -43,38 +40,6 @@ exports.sanitizeHtml = (text) => {
   return text ? text.replace(/<\/?[^>]+(>|$)/g, '') : text
 }
 
-/**
- * Gets the size and content type of a url's content
- *
- * @param {string} url
- * @param {boolean=} blockLocalIPs
- * @return {Promise}
- */
-exports.getURLMeta = (url, blockLocalIPs = false) => {
-  return new Promise((resolve, reject) => {
-    const opts = {
-      uri: url,
-      method: 'HEAD',
-      followRedirect: getRedirectEvaluator(url, blockLocalIPs),
-      agentClass: getProtectedHttpAgent((new URL(url)).protocol, blockLocalIPs)
-    }
-
-    request(opts, (err, response) => {
-      if (err || response.statusCode >= 300) {
-        // @todo possibly set a status code in the error object to get a more helpful
-        // hint at what the cause of error is.
-        err = err || new Error(`URL server responded with status: ${response.statusCode}`)
-        reject(err)
-      } else {
-        resolve({
-          type: response.headers['content-type'],
-          size: parseInt(response.headers['content-length'])
-        })
-      }
-    })
-  })
-}
-
 // all paths are assumed to be '/' prepended
 /**
  * Returns a url builder

+ 2 - 0
packages/@uppy/companion/src/server/provider/drive/adapter.js

@@ -1,5 +1,7 @@
 const querystring = require('querystring')
 
+// @todo use the "about" endpoint to get the username instead
+// see: https://developers.google.com/drive/api/v2/reference/about/get
 exports.getUsername = (data) => {
   for (const item of data.files) {
     if (item.ownedByMe && item.permissions) {

+ 3 - 2
packages/@uppy/companion/src/server/provider/facebook/index.js

@@ -2,7 +2,7 @@ const Provider = require('../Provider')
 
 const request = require('request')
 const purest = require('purest')({ request })
-const utils = require('../../helpers/utils')
+const { getURLMeta } = require('../../helpers/request')
 const logger = require('../../logger')
 const adapter = require('./adapter')
 const { ProviderApiError, ProviderAuthError } = require('../error')
@@ -122,7 +122,7 @@ class Facebook extends Provider {
           return done(err)
         }
 
-        utils.getURLMeta(this._getMediaUrl(body))
+        getURLMeta(this._getMediaUrl(body))
           .then(({ size }) => done(null, size))
           .catch((err) => {
             logger.error(err, 'provider.facebook.size.error')
@@ -154,6 +154,7 @@ class Facebook extends Provider {
         icon: adapter.getItemIcon(item),
         name: adapter.getItemName(item),
         mimeType: adapter.getMimeType(item),
+        size: null,
         id: adapter.getItemId(item),
         thumbnail: adapter.getItemThumbnailUrl(item),
         requestPath: adapter.getItemRequestPath(item),

+ 0 - 74
packages/@uppy/companion/src/server/provider/instagram/adapter.js

@@ -1,74 +0,0 @@
-exports.isFolder = (item) => {
-  return false
-}
-
-exports.getItemIcon = (item) => {
-  if (!item.images) {
-    return 'video'
-  }
-  return item.images.low_resolution.url
-}
-
-exports.getItemSubList = (item) => {
-  const subItems = []
-  item.data.forEach((subItem) => {
-    if (subItem.carousel_media) {
-      subItem.carousel_media.forEach((i, index) => {
-        const newSubItem = Object.assign({}, i, {
-          id: subItem.id,
-          created_time: subItem.created_time,
-          carousel_id: index
-        })
-        subItems.push(newSubItem)
-      })
-    } else {
-      subItems.push(subItem)
-    }
-  })
-  return subItems
-}
-
-exports.getItemName = (item) => {
-  if (item && item.created_time) {
-    const ext = item.type === 'video' ? 'mp4' : 'jpeg'
-    const date = new Date(item.created_time * 1000)
-    const name = date.toLocaleDateString([], {
-      year: 'numeric',
-      month: 'short',
-      day: 'numeric',
-      hour: 'numeric',
-      minute: 'numeric'
-    })
-    // adding both date and carousel_id, so the name is unique
-    return `Instagram ${name}${item.carousel_id ? ' ' + item.carousel_id : ''}.${ext}`
-  }
-  return ''
-}
-
-exports.getMimeType = (item) => {
-  return item.type === 'video' ? 'video/mp4' : 'image/jpeg'
-}
-
-exports.getItemId = (item) => {
-  return `${item.id}${item.carousel_id || ''}`
-}
-
-exports.getItemRequestPath = (item) => {
-  const suffix = isNaN(item.carousel_id) ? '' : `?carousel_id=${item.carousel_id}`
-  return `${item.id}${suffix}`
-}
-
-exports.getItemModifiedDate = (item) => {
-  return item.created_time
-}
-
-exports.getItemThumbnailUrl = (item) => {
-  return item.images ? item.images.thumbnail.url : null
-}
-
-exports.getNextPagePath = (data) => {
-  const items = exports.getItemSubList(data)
-  if (items.length) {
-    return `recent?cursor=${exports.getItemId(items[items.length - 1])}`
-  }
-}

+ 3 - 2
packages/@uppy/companion/src/server/provider/instagram/graph/index.js

@@ -2,7 +2,7 @@ const Provider = require('../../Provider')
 
 const request = require('request')
 const purest = require('purest')({ request })
-const utils = require('../../../helpers/utils')
+const { getURLMeta } = require('../../../helpers/request')
 const logger = require('../../../logger')
 const adapter = require('./adapter')
 const { ProviderApiError, ProviderAuthError } = require('../../error')
@@ -118,7 +118,7 @@ class Instagram extends Provider {
           return done(err)
         }
 
-        utils.getURLMeta(body.media_url)
+        getURLMeta(body.media_url)
           .then(({ size }) => done(null, size))
           .catch((err) => {
             logger.error(err, 'provider.instagram.size.error')
@@ -142,6 +142,7 @@ class Instagram extends Provider {
         name: adapter.getItemName(item, i),
         mimeType: adapter.getMimeType(item),
         id: adapter.getItemId(item),
+        size: null,
         thumbnail: adapter.getItemThumbnailUrl(item),
         requestPath: adapter.getItemRequestPath(item),
         modifiedDate: adapter.getItemModifiedDate(item)

+ 0 - 169
packages/@uppy/companion/src/server/provider/instagram/index.js

@@ -1,169 +0,0 @@
-const Provider = require('../Provider')
-
-const request = require('request')
-const purest = require('purest')({ request })
-const utils = require('../../helpers/utils')
-const logger = require('../../logger')
-const adapter = require('./adapter')
-const { ProviderApiError, ProviderAuthError } = require('../error')
-
-/**
- * Adapter for API https://www.instagram.com/developer/endpoints/
- */
-class Instagram extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = options.provider = Instagram.authProvider
-    this.client = purest(options)
-  }
-
-  static get authProvider () {
-    return 'instagram'
-  }
-
-  list ({ directory = 'recent', token, query = { cursor: null, max_id: null } }, done) {
-    const cursor = query.cursor || query.max_id
-    const qs = cursor ? { max_id: cursor } : {}
-    this.client
-      .get(`users/self/media/${directory}`)
-      .qs(qs)
-      .auth(token)
-      .request((err, resp, body) => {
-        if (err || resp.statusCode !== 200) {
-          err = this._error(err, resp)
-          logger.error(err, 'provider.instagram.list.error')
-          return done(err)
-        } else {
-          this._getUsername(token, (err, username) => {
-            err ? done(err) : done(null, this.adaptData(body, username))
-          })
-        }
-      })
-  }
-
-  _getUsername (token, done) {
-    this.client
-      .get('users/self')
-      .auth(token)
-      .request((err, resp, body) => {
-        if (err || resp.statusCode !== 200) {
-          err = this._error(err, resp)
-          logger.error(err, 'provider.instagram.user.error')
-          return done(err)
-        } else {
-          done(null, body.data.username)
-        }
-      })
-  }
-
-  _getMediaUrl (body, carouselId) {
-    let mediaObj
-    let type
-
-    if (body.data.type === 'carousel') {
-      carouselId = carouselId ? parseInt(carouselId) : 0
-      mediaObj = body.data.carousel_media[carouselId]
-      type = mediaObj.type
-    } else {
-      mediaObj = body.data
-      type = body.data.type
-    }
-
-    return mediaObj[`${type}s`].standard_resolution.url
-  }
-
-  download ({ id, token, query = { carousel_id: null } }, onData) {
-    return this.client
-      .get(`media/${id}`)
-      .auth(token)
-      .request((err, resp, body) => {
-        if (err || resp.statusCode !== 200) {
-          err = this._error(err, resp)
-          logger.error(err, 'provider.instagram.download.error')
-          onData(err)
-          return
-        }
-
-        request(this._getMediaUrl(body, query.carousel_id))
-          .on('response', (resp) => {
-            if (resp.statusCode !== 200) {
-              onData(this._error(null, resp))
-            } else {
-              resp.on('data', (chunk) => onData(null, chunk))
-            }
-          })
-          .on('end', () => onData(null, null))
-          .on('error', (err) => {
-            logger.error(err, 'provider.instagram.download.url.error')
-            onData(err)
-          })
-      })
-  }
-
-  thumbnail (_, done) {
-    // not implementing this because a public thumbnail from instagram will be used instead
-    const err = new Error('call to thumbnail is not implemented')
-    logger.error(err, 'provider.instagram.thumbnail.error')
-    return done(err)
-  }
-
-  size ({ id, token, query = { carousel_id: null } }, done) {
-    return this.client
-      .get(`media/${id}`)
-      .auth(token)
-      .request((err, resp, body) => {
-        if (err || resp.statusCode !== 200) {
-          err = this._error(err, resp)
-          logger.error(err, 'provider.instagram.size.error')
-          return done(err)
-        }
-
-        utils.getURLMeta(this._getMediaUrl(body, query.carousel_id))
-          .then(({ size }) => done(null, size))
-          .catch((err) => {
-            logger.error(err, 'provider.instagram.size.error')
-            done()
-          })
-      })
-  }
-
-  logout (_, done) {
-    // access revoke is not supported by Instagram's API
-    done(null, { revoked: false, manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/' })
-  }
-
-  adaptData (res, username) {
-    const data = { username: username, items: [] }
-    const items = adapter.getItemSubList(res)
-    items.forEach((item) => {
-      data.items.push({
-        isFolder: adapter.isFolder(item),
-        icon: adapter.getItemIcon(item),
-        name: adapter.getItemName(item),
-        mimeType: adapter.getMimeType(item),
-        id: adapter.getItemId(item),
-        thumbnail: adapter.getItemThumbnailUrl(item),
-        requestPath: adapter.getItemRequestPath(item),
-        modifiedDate: adapter.getItemModifiedDate(item)
-      })
-    })
-
-    data.nextPagePath = adapter.getNextPagePath(res)
-    return data
-  }
-
-  _error (err, resp) {
-    if (resp) {
-      if (resp.statusCode === 400 && resp.body && resp.body.meta.error_type === 'OAuthAccessTokenException') {
-        return new ProviderAuthError()
-      }
-
-      const msg = `request to ${this.authProvider} returned ${resp.statusCode}`
-      return new ProviderApiError(msg, resp.statusCode)
-    }
-
-    return err
-  }
-}
-
-module.exports = Instagram

+ 2 - 2
packages/@uppy/companion/src/server/provider/zoom/index.js

@@ -47,7 +47,7 @@ class Zoom extends Provider {
         }
 
         if (!from && !to && !meetingId) {
-          const end = cursor && moment.utc(cursor).endOf('day').tz(userResponse.timezone || 'UTC')
+          const end = cursor && moment.utc(cursor).endOf('day').tz(userBody.timezone || 'UTC')
           return done(null, this._initializeData(userResponse.body, end))
         }
 
@@ -150,7 +150,7 @@ class Zoom extends Provider {
     const meetingId = id
     const fileId = query.recordingId
     const recordingStart = query.recordingStart
-    const GET_MEETING_FILES = `/meetings/${meetingId}/recordings`
+    const GET_MEETING_FILES = `/meetings/${encodeURIComponent(meetingId)}/recordings`
 
     return this.client
       .get(`${BASE_URL}${GET_MEETING_FILES}`)

+ 14 - 39
packages/@uppy/companion/test/__mocks__/purest.js

@@ -1,60 +1,35 @@
 const fs = require('fs')
+const qs = require('querystring')
+const fixtures = require('../fixtures').providers
 
 class MockPurest {
   constructor (opts) {
-    const methodsToMock = ['query', 'select', 'where', 'qs', 'auth', 'json']
-    const httpMethodsToMock = ['get', 'put', 'post', 'options', 'head']
+    const methodsToMock = ['query', 'select', 'where', 'auth', 'json', 'options']
+    const httpMethodsToMock = ['get', 'put', 'post', 'head', 'delete']
     methodsToMock.forEach((item) => {
       this[item] = () => this
     })
     httpMethodsToMock.forEach((item) => {
       this[item] = (url) => {
         this._requestUrl = url
+        this._method = item
         return this
       }
     })
     this.opts = opts
   }
 
+  qs (data) {
+    this._query = qs.stringify(data)
+    return this
+  }
+
   request (done) {
     if (typeof done === 'function') {
-      const responses = {
-        zoom: {
-          default: {}
-        },
-        dropbox: {
-          default: {
-            hash: '0a9f95a989dd4b1851f0103c31e304ce',
-            user_email: 'foo@bar.com',
-            email: 'foo@bar.com',
-            entries: [{ rev: 'f24234cd4' }]
-          }
-        },
-        drive: {
-          'files/README.md': {
-            id: '0B2x-PmqQHSKdT013TE1VVjZ3TWs',
-            mimeType: 'image/jpg',
-            ownedByMe: true,
-            permissions: [{ role: 'owner', emailAddress: 'ife@bala.com' }],
-            size: 300,
-            kind: 'drive#file',
-            etag: '"bcIyJ9A3gXa8oTYmz6nzAjQd-lY/eQc3WbZHkXpcItNyGKDuKXM_bNY"'
-          },
-          default: {
-            kind: 'drive#fileList',
-            etag: '"bcIyJ9A3gXa8oTYmz6nzAjQd-lY/eQc3WbZHkXpcItNyGKDuKXM_bNY"',
-            files: [{
-              id: '0B2x-PmqQHSKdT013TE1VVjZ3TWs',
-              mimeType: 'image/jpg',
-              ownedByMe: true,
-              permissions: [{ role: 'owner', emailAddress: 'ife@bala.com' }]
-            }],
-            size: 300
-          }
-        }
-      }
-      const providerResponses = responses[this.opts.providerName]
-      const body = providerResponses[this._requestUrl] || providerResponses.default
+      const responses = fixtures[this.opts.providerName].responses
+      const url = this._query ? `${this._requestUrl}?${this._query}` : this._requestUrl
+      const endpointResponses = responses[url] || responses[this._requestUrl]
+      const body = endpointResponses[this._method]
       done(null, { body, statusCode: 200 }, body)
     }
 

+ 14 - 0
packages/@uppy/companion/test/__mocks__/tus-js-client.js

@@ -1,6 +1,20 @@
 class Upload {
   constructor (file, options) {
     this.url = 'https://tus.endpoint/files/foo-bar'
+    this.options = options
+  }
+
+  _triggerProgressThenSuccess () {
+    this.options.onProgress(this.options.uploadSize, this.options.uploadSize)
+    setTimeout(() => this.options.onSuccess(), 100)
+  }
+
+  start () {
+    setTimeout(this._triggerProgressThenSuccess.bind(this), 100)
+  }
+
+  abort () {
+    // noop
   }
 }
 

+ 92 - 0
packages/@uppy/companion/test/__tests__/callback.js

@@ -0,0 +1,92 @@
+/* global jest:false, test:false, expect:false, describe:false */
+
+jest.mock('../../src/server/helpers/oauth-state', () => require('../mockoauthstate')())
+
+const request = require('supertest')
+const tokenService = require('../../src/server/helpers/jwt')
+const { getServer } = require('../mockserver')
+const authServer = getServer()
+const authData = {
+  dropbox: 'token value',
+  drive: 'token value'
+}
+const token = tokenService.generateToken(authData, process.env.COMPANION_SECRET)
+
+describe('test authentication callback', () => {
+  test('authentication callback redirects to send-token url', () => {
+    return request(authServer)
+      .get('/drive/callback')
+      .expect(302)
+      .expect((res) => {
+        expect(res.header.location).toContain('http://localhost:3020/drive/send-token?uppyAuthToken=')
+      })
+  })
+
+  test('the token gets sent via cookie and html', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
+    return request(authServer)
+      .get(`/dropbox/send-token?uppyAuthToken=${token}&state=state-with-newer-version`)
+      .expect(200)
+      .expect((res) => {
+        const authToken = res.header['set-cookie'][0].split(';')[0].split('uppyAuthToken--dropbox=')[1]
+        expect(authToken).toEqual(token)
+        const body = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="utf-8" />
+        <script>
+          window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
+          window.close()
+        </script>
+    </head>
+    <body></body>
+    </html>`
+        expect(res.text).toBe(body)
+      })
+  })
+
+  test('the token gets to older clients without stringify', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
+    return request(authServer)
+      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-older-version`)
+      .expect(200)
+      .expect((res) => {
+        const body = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="utf-8" />
+        <script>
+          window.opener.postMessage({token: "${token}"}, "http://localhost:3020")
+          window.close()
+        </script>
+    </head>
+    <body></body>
+    </html>`
+        expect(res.text).toBe(body)
+      })
+  })
+
+  test('the token gets sent to newer clients with old version style', () => {
+    // see mock ../../src/server/helpers/oauth-state above for state values
+    return request(authServer)
+      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version-old-style`)
+      .expect(200)
+      .expect((res) => {
+        const body = `
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="utf-8" />
+        <script>
+          window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
+          window.close()
+        </script>
+    </head>
+    <body></body>
+    </html>`
+        expect(res.text).toBe(body)
+      })
+  })
+})

+ 8 - 282
packages/@uppy/companion/test/__tests__/companion.js

@@ -2,31 +2,7 @@
 
 jest.mock('tus-js-client')
 jest.mock('purest')
-jest.mock('../../src/server/helpers/oauth-state', () => {
-  return {
-    generateState: () => 'some-cool-nice-encrytpion',
-    addToState: () => 'some-cool-nice-encrytpion',
-    getFromState: (state, key) => {
-      if (state === 'state-with-invalid-instance-url') {
-        return 'http://localhost:3452'
-      }
-
-      if (state === 'state-with-older-version' && key === 'clientVersion') {
-        return '@uppy/companion-client=1.0.1'
-      }
-
-      if (state === 'state-with-newer-version' && key === 'clientVersion') {
-        return '@uppy/companion-client=1.0.3'
-      }
-
-      if (state === 'state-with-newer-version-old-style' && key === 'clientVersion') {
-        return 'companion-client:1.0.2'
-      }
-
-      return 'http://localhost:3020'
-    }
-  }
-})
+jest.mock('../../src/server/helpers/oauth-state', () => require('../mockoauthstate')())
 
 const request = require('supertest')
 const tokenService = require('../../src/server/helpers/jwt')
@@ -39,38 +15,10 @@ const authData = {
 const token = tokenService.generateToken(authData, process.env.COMPANION_SECRET)
 const OAUTH_STATE = 'some-cool-nice-encrytpion'
 
-describe('set i-am header', () => {
-  test('set i-am header in response', () => {
-    return request(authServer)
-      .get('/dropbox/list/')
-      .set('uppy-auth-token', token)
-      .expect(200)
-      .then((res) => expect(res.header['i-am']).toBe('http://localhost:3020'))
-  })
-})
-
-describe('list provider files', () => {
-  test('list files for dropbox', () => {
-    return request(authServer)
-      .get('/dropbox/list/')
-      .set('uppy-auth-token', token)
-      .expect(200)
-      .then((res) => expect(res.body.username).toBe('foo@bar.com'))
-  })
-
-  test('list files for google drive', () => {
-    return request(authServer)
-      .get('/drive/list/')
-      .set('uppy-auth-token', token)
-      .expect(200)
-      .then((res) => expect(res.body.username).toBe('ife@bala.com'))
-  })
-})
-
 describe('validate upload data', () => {
   test('invalid upload protocol gets rejected', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -83,7 +31,7 @@ describe('validate upload data', () => {
 
   test('invalid upload fieldname gets rejected', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -97,7 +45,7 @@ describe('validate upload data', () => {
 
   test('invalid upload metadata gets rejected', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -111,7 +59,7 @@ describe('validate upload data', () => {
 
   test('invalid upload headers get rejected', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -125,7 +73,7 @@ describe('validate upload data', () => {
 
   test('invalid upload HTTP Method gets rejected', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -139,7 +87,7 @@ describe('validate upload data', () => {
 
   test('valid upload data is allowed - tus', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -159,7 +107,7 @@ describe('validate upload data', () => {
 
   test('valid upload data is allowed - s3-multipart', () => {
     return request(authServer)
-      .post('/drive/get/README.md')
+      .post('/drive/get/DUMMY-FILE-ID')
       .set('uppy-auth-token', token)
       .set('Content-Type', 'application/json')
       .send({
@@ -178,126 +126,6 @@ describe('validate upload data', () => {
   })
 })
 
-describe('download provdier file', () => {
-  test('specified file gets downloaded from provider', () => {
-    return request(authServer)
-      .post('/drive/get/README.md')
-      .set('uppy-auth-token', token)
-      .set('Content-Type', 'application/json')
-      .send({
-        endpoint: 'http://master.tus.io/files',
-        protocol: 'tus'
-      })
-      .expect(200)
-      .then((res) => expect(res.body.token).toBeTruthy())
-  })
-})
-
-describe('test authentication', () => {
-  test('authentication callback redirects to send-token url', () => {
-    return request(authServer)
-      .get('/drive/callback')
-      .expect(302)
-      .expect((res) => {
-        expect(res.header.location).toContain('http://localhost:3020/drive/send-token?uppyAuthToken=')
-      })
-  })
-
-  test('the token gets sent via cookie and html', () => {
-    // see mock ../../src/server/helpers/oauth-state above for state values
-    return request(authServer)
-      .get(`/dropbox/send-token?uppyAuthToken=${token}&state=state-with-newer-version`)
-      .expect(200)
-      .expect((res) => {
-        const authToken = res.header['set-cookie'][0].split(';')[0].split('uppyAuthToken--dropbox=')[1]
-        expect(authToken).toEqual(token)
-        const body = `
-    <!DOCTYPE html>
-    <html>
-    <head>
-        <meta charset="utf-8" />
-        <script>
-          window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
-          window.close()
-        </script>
-    </head>
-    <body></body>
-    </html>`
-        expect(res.text).toBe(body)
-      })
-  })
-
-  test('the token gets to older clients without stringify', () => {
-    // see mock ../../src/server/helpers/oauth-state above for state values
-    return request(authServer)
-      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-older-version`)
-      .expect(200)
-      .expect((res) => {
-        const body = `
-    <!DOCTYPE html>
-    <html>
-    <head>
-        <meta charset="utf-8" />
-        <script>
-          window.opener.postMessage({token: "${token}"}, "http://localhost:3020")
-          window.close()
-        </script>
-    </head>
-    <body></body>
-    </html>`
-        expect(res.text).toBe(body)
-      })
-  })
-
-  test('the token gets sent to newer clients with old version style', () => {
-    // see mock ../../src/server/helpers/oauth-state above for state values
-    return request(authServer)
-      .get(`/drive/send-token?uppyAuthToken=${token}&state=state-with-newer-version-old-style`)
-      .expect(200)
-      .expect((res) => {
-        const body = `
-    <!DOCTYPE html>
-    <html>
-    <head>
-        <meta charset="utf-8" />
-        <script>
-          window.opener.postMessage(JSON.stringify({token: "${token}"}), "http://localhost:3020")
-          window.close()
-        </script>
-    </head>
-    <body></body>
-    </html>`
-        expect(res.text).toBe(body)
-      })
-  })
-
-  test('logout provider', () => {
-    return request(authServer)
-      .get('/drive/logout/')
-      .set('uppy-auth-token', token)
-      .expect(200)
-      .then((res) => expect(res.body.ok).toBe(true))
-  })
-})
-
-describe('connect to provider', () => {
-  test('connect to dropbox via grant.js endpoint', () => {
-    return request(authServer)
-      .get('/dropbox/connect?foo=bar')
-      .set('uppy-auth-token', token)
-      .expect(302)
-      .expect('Location', `http://localhost:3020/connect/dropbox?state=${OAUTH_STATE}`)
-  })
-
-  test('connect to drive via grant.js endpoint', () => {
-    return request(authServer)
-      .get('/drive/connect?foo=bar')
-      .set('uppy-auth-token', token)
-      .expect(302)
-      .expect('Location', `http://localhost:3020/connect/google?state=${OAUTH_STATE}`)
-  })
-})
-
 describe('handle master oauth redirect', () => {
   const serverWithMasterOauth = getServer({
     COMPANION_OAUTH_DOMAIN: 'localhost:3040'
@@ -318,105 +146,3 @@ describe('handle master oauth redirect', () => {
       .expect(400)
   })
 })
-
-// @todo consider moving this to a separate test file (zoom.js maybe)
-// in general, we should consider testing all providers endpoints separately
-describe('handle deauthorization callback', () => {
-  test('providers without support for callback endpoint', () => {
-    return request(authServer)
-      .post('/dropbox/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .send({
-        foo: 'bar'
-      })
-    // @todo consider receiving 501 instead
-      .expect(500)
-  })
-
-  test('validate that request credentials match', () => {
-    return request(authServer)
-      .post('/zoom/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .set('Authorization', 'wrong-verfication-token')
-      .send({
-        event: 'app_deauthorized',
-        payload: {
-          user_data_retention: 'false',
-          account_id: 'EabCDEFghiLHMA',
-          user_id: 'z9jkdsfsdfjhdkfjQ',
-          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
-          deauthorization_time: '2019-06-17T13:52:28.632Z',
-          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
-        }
-      })
-      .expect(400)
-  })
-
-  test('validate request credentials is present', () => {
-    // Authorization header is absent
-    return request(authServer)
-      .post('/zoom/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .send({
-        event: 'app_deauthorized',
-        payload: {
-          user_data_retention: 'false',
-          account_id: 'EabCDEFghiLHMA',
-          user_id: 'z9jkdsfsdfjhdkfjQ',
-          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
-          deauthorization_time: '2019-06-17T13:52:28.632Z',
-          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
-        }
-      })
-      .expect(400)
-  })
-
-  test('validate request content', () => {
-    return request(authServer)
-      .post('/zoom/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .set('Authorization', 'zoom_verfication_token')
-      .send({
-        invalid: 'content'
-      })
-      .expect(400)
-  })
-
-  test('validate request content (event name)', () => {
-    return request(authServer)
-      .post('/zoom/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .set('Authorization', 'zoom_verfication_token')
-      .send({
-        event: 'wrong_event_name',
-        payload: {
-          user_data_retention: 'false',
-          account_id: 'EabCDEFghiLHMA',
-          user_id: 'z9jkdsfsdfjhdkfjQ',
-          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
-          deauthorization_time: '2019-06-17T13:52:28.632Z',
-          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
-        }
-      })
-      .expect(400)
-  })
-
-  test('allow valid request', () => {
-    return request(authServer)
-      .post('/zoom/deauthorization/callback')
-      .set('Content-Type', 'application/json')
-      .set('Authorization', 'zoom_verfication_token')
-      .send({
-        event: 'app_deauthorized',
-        payload: {
-          user_data_retention: 'false',
-          account_id: 'EabCDEFghiLHMA',
-          user_id: 'z9jkdsfsdfjhdkfjQ',
-          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
-          deauthorization_time: '2019-06-17T13:52:28.632Z',
-          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
-        }
-      })
-      .expect(200)
-  })
-})

+ 105 - 0
packages/@uppy/companion/test/__tests__/deauthorization.js

@@ -0,0 +1,105 @@
+/* global test:false, describe:false */
+
+const request = require('supertest')
+const { getServer } = require('../mockserver')
+const authServer = getServer()
+
+describe('handle deauthorization callback', () => {
+  test('providers without support for callback endpoint', () => {
+    return request(authServer)
+      .post('/dropbox/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .send({
+        foo: 'bar'
+      })
+    // @todo consider receiving 501 instead
+      .expect(500)
+  })
+
+  test('validate that request credentials match', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'wrong-verfication-token')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('validate request credentials is present', () => {
+    // Authorization header is absent
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('validate request content', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        invalid: 'content'
+      })
+      .expect(400)
+  })
+
+  test('validate request content (event name)', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        event: 'wrong_event_name',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(400)
+  })
+
+  test('allow valid request', () => {
+    return request(authServer)
+      .post('/zoom/deauthorization/callback')
+      .set('Content-Type', 'application/json')
+      .set('Authorization', 'zoom_verfication_token')
+      .send({
+        event: 'app_deauthorized',
+        payload: {
+          user_data_retention: 'false',
+          account_id: 'EabCDEFghiLHMA',
+          user_id: 'z9jkdsfsdfjhdkfjQ',
+          signature: '827edc3452044f0bc86bdd5684afb7d1e6becfa1a767f24df1b287853cf73000',
+          deauthorization_time: '2019-06-17T13:52:28.632Z',
+          client_id: 'ADZ9k9bTWmGUoUbECUKU_a'
+        }
+      })
+      .expect(200)
+  })
+})

+ 106 - 0
packages/@uppy/companion/test/__tests__/providers.js

@@ -0,0 +1,106 @@
+/* global jest:false, test:false, expect:false, describe:false */
+
+jest.mock('tus-js-client')
+jest.mock('purest')
+jest.mock('../../src/server/helpers/request', () => {
+  return {
+    getURLMeta: () => Promise.resolve({ size: 758051 })
+  }
+})
+jest.mock('../../src/server/helpers/oauth-state', () => require('../mockoauthstate')())
+
+const request = require('supertest')
+const fixtures = require('../fixtures')
+const tokenService = require('../../src/server/helpers/jwt')
+const { getServer } = require('../mockserver')
+const authServer = getServer()
+const OAUTH_STATE = 'some-cool-nice-encrytpion'
+const providers = require('../../src/server/provider').getDefaultProviders()
+const providerNames = Object.keys(providers)
+const AUTH_PROVIDERS = {
+  drive: 'google',
+  onedrive: 'microsoft'
+}
+const authData = {}
+providerNames.forEach((provider) => {
+  authData[provider] = 'token value'
+})
+const token = tokenService.generateToken(authData, process.env.COMPANION_SECRET)
+
+const thisOrThat = (value1, value2) => {
+  if (value1 !== undefined) {
+    return value1
+  }
+
+  return value2
+}
+
+describe('set i-am header', () => {
+  test.each(providerNames)('set i-am header in response (%s)', (providerName) => {
+    const providerFixtures = fixtures.providers[providerName].expects
+    return request(authServer)
+      .get(`/${providerName}/list/${providerFixtures.listPath || ''}`)
+      .set('uppy-auth-token', token)
+      .expect(200)
+      .then((res) => expect(res.header['i-am']).toBe('http://localhost:3020'))
+  })
+})
+
+describe('list provider files', () => {
+  test.each(providerNames)('list files for %s', (providerName) => {
+    const providerFixtures = fixtures.providers[providerName].expects
+    return request(authServer)
+      .get(`/${providerName}/list/${providerFixtures.listPath || ''}`)
+      .set('uppy-auth-token', token)
+      .expect(200)
+      .then((res) => {
+        expect(res.body.username).toBe(fixtures.defaults.USERNAME)
+        const item = res.body.items[0]
+        expect(item.isFolder).toBe(false)
+        expect(item.name).toBe(providerFixtures.itemName || fixtures.defaults.ITEM_NAME)
+        expect(item.mimeType).toBe(providerFixtures.itemMimeType || fixtures.defaults.MIME_TYPE)
+        expect(item.id).toBe(providerFixtures.itemId || fixtures.defaults.ITEM_ID)
+        expect(item.size).toBe(thisOrThat(providerFixtures.itemSize, fixtures.defaults.FILE_SIZE))
+        expect(item.requestPath).toBe(providerFixtures.itemRequestPath || fixtures.defaults.ITEM_ID)
+        expect(item.icon).toBe(providerFixtures.itemIcon || fixtures.defaults.THUMBNAIL_URL)
+      })
+  })
+})
+
+describe('download provdier file', () => {
+  test.each(providerNames)('specified file gets downloaded from %s', (providerName) => {
+    const providerFixtures = fixtures.providers[providerName].expects
+    return request(authServer)
+      .post(`/${providerName}/get/${providerFixtures.itemRequestPath || fixtures.defaults.ITEM_ID}`)
+      .set('uppy-auth-token', token)
+      .set('Content-Type', 'application/json')
+      .send({
+        endpoint: 'http://master.tus.io/files',
+        protocol: 'tus'
+      })
+      .expect(200)
+      .then((res) => expect(res.body.token).toBeTruthy())
+  })
+})
+
+describe('connect to provider', () => {
+  test.each(providerNames)('connect to %s via grant.js endpoint', (providerName) => {
+    const authProvider = AUTH_PROVIDERS[providerName] || providerName
+
+    return request(authServer)
+      .get(`/${providerName}/connect?foo=bar`)
+      .set('uppy-auth-token', token)
+      .expect(302)
+      .expect('Location', `http://localhost:3020/connect/${authProvider}?state=${OAUTH_STATE}`)
+  })
+})
+
+describe('logout of provider', () => {
+  test.each(providerNames)('logout of %s', (providerName) => {
+    return request(authServer)
+      .get(`/${providerName}/logout/`)
+      .set('uppy-auth-token', token)
+      .expect(200)
+      .then((res) => expect(res.body.ok).toBe(true))
+  })
+})

+ 60 - 0
packages/@uppy/companion/test/__tests__/uploader.js

@@ -0,0 +1,60 @@
+/* global jest:false, test:false, expect:false, describe:false */
+
+jest.mock('tus-js-client')
+
+const fs = require('fs')
+const Uploader = require('../../src/server/Uploader')
+const socketClient = require('../mocksocket')
+const { companionOptions } = require('../../src/standalone')
+
+describe('uploader with tus protocol', () => {
+  test('upload functions with tus protocol', () => {
+    const fileContent = Buffer.from('Some file content')
+    const opts = {
+      companionOptions: companionOptions,
+      endpoint: 'http://url.myendpoint.com/files',
+      protocol: 'tus',
+      size: fileContent.length,
+      pathPrefix: companionOptions.filePath
+    }
+
+    const uploader = new Uploader(opts)
+    const uploadToken = uploader.token
+    expect(uploader.hasError()).toBe(false)
+    expect(uploadToken).toBeTruthy()
+
+    return new Promise((resolve) => {
+      // validate that the test is resolved on socket connection
+      uploader.onSocketReady(() => {
+        const fileInfo = fs.statSync(uploader.path)
+        expect(fileInfo.isFile()).toBe(true)
+        expect(fileInfo.size).toBe(0)
+        uploader.handleChunk(null, fileContent)
+        uploader.handleChunk(null, null)
+      })
+
+      let progressReceived = 0
+      // emulate socket connection
+      socketClient.connect(uploadToken)
+      socketClient.onProgress(uploadToken, (message) => {
+        // validate that the file has been downloaded and saved into the file path
+        const fileInfo = fs.statSync(uploader.path)
+        expect(fileInfo.isFile()).toBe(true)
+        expect(fileInfo.size).toBe(fileContent.length)
+
+        progressReceived = message.payload.bytesUploaded
+        expect(message.payload.bytesTotal).toBe(fileContent.length)
+      })
+      socketClient.onUploadSuccess(uploadToken, (message) => {
+        expect(progressReceived).toBe(fileContent.length)
+        // see __mocks__/tus-js-client.js
+        expect(message.payload.url).toBe('https://tus.endpoint/files/foo-bar')
+        setTimeout(() => {
+          // check that file has been cleaned up
+          expect(fs.existsSync(uploader.path)).toBe(false)
+          resolve()
+        }, 100)
+      })
+    })
+  })
+})

+ 72 - 0
packages/@uppy/companion/test/__tests__/url.js

@@ -0,0 +1,72 @@
+/* global jest:false, test:false, expect:false, describe:false */
+
+jest.mock('tus-js-client')
+jest.mock('../../src/server/helpers/request', () => {
+  return {
+    getURLMeta: (url) => {
+      return Promise.resolve({ size: 7580, type: 'image/jpg' })
+    }
+  }
+})
+const { getServer } = require('../mockserver')
+const mockServer = getServer()
+const request = require('supertest')
+const invalids = [
+  // no url at all or unsupported protocol
+  null, '', 'ftp://url.myendpoint.com/files'
+]
+
+describe('url meta', () => {
+  test('return a url\'s meta data', () => {
+    return request(mockServer)
+      .post('/url/meta')
+      .set('Content-Type', 'application/json')
+      .send({
+        url: 'http://url.myendpoint.com/files'
+      })
+      .expect(200)
+      .then((res) => {
+        expect(res.body.size).toBe(7580)
+        expect(res.body.type).toBe('image/jpg')
+      })
+  })
+
+  test.each(invalids)('return 400 for invalid url', (urlCase) => {
+    return request(mockServer)
+      .post('/url/meta')
+      .set('Content-Type', 'application/json')
+      .send({
+        url: urlCase
+      })
+      .expect(400)
+      .then((res) => expect(res.body.error).toBe('Invalid request body'))
+  })
+})
+
+describe('url get', () => {
+  test('url download gets instanitated', () => {
+    return request(mockServer)
+      .post('/url/get')
+      .set('Content-Type', 'application/json')
+      .send({
+        url: 'http://url.myendpoint.com/files',
+        endpoint: 'http://master.tus.io/files',
+        protocol: 'tus'
+      })
+      .expect(200)
+      .then((res) => expect(res.body.token).toBeTruthy())
+  })
+
+  test.each(invalids)('downloads are not instantiated for invalid urls', (urlCase) => {
+    return request(mockServer)
+      .post('/url/get')
+      .set('Content-Type', 'application/json')
+      .send({
+        url: urlCase,
+        endpoint: 'http://master.tus.io/files',
+        protocol: 'tus'
+      })
+      .expect(400)
+      .then((res) => expect(res.body.error).toBe('Invalid request body'))
+  })
+})

+ 9 - 0
packages/@uppy/companion/test/fixtures/constants.js

@@ -0,0 +1,9 @@
+module.exports.NEXT_PAGE_TOKEN = 'DUMMY-NEXT-PAGE-TOKEN'
+module.exports.ITEM_ID = 'DUMMY-FILE-ID'
+module.exports.ITEM_NAME = 'MY DUMMY FILE NAME.mp4'
+module.exports.ICON = 'https://DUMMY-THUMBNAIL.com/file.jpg'
+module.exports.THUMBNAIL_URL = 'https://DUMMY-THUMBNAIL.com/file.jpg'
+module.exports.MODIFIED_DATE = '2016-07-10T20:00:08.096Z'
+module.exports.MIME_TYPE = 'video/mp4'
+module.exports.USERNAME = 'john.doe@transloadit.com'
+module.exports.FILE_SIZE = 758051

+ 49 - 0
packages/@uppy/companion/test/fixtures/drive.js

@@ -0,0 +1,49 @@
+const defaults = require('./constants')
+
+module.exports.responses = {
+  files: {
+    get: {
+      kind: 'drive#fileList',
+      nextPageToken: defaults.NEXT_PAGE_TOKEN,
+      files: [
+        {
+          kind: 'drive#file',
+          id: defaults.ITEM_ID,
+          name: defaults.ITEM_NAME,
+          mimeType: defaults.MIME_TYPE,
+          iconLink: 'https://drive-thirdparty.googleusercontent.com/16/type/video/mp4',
+          thumbnailLink: defaults.THUMBNAIL_URL,
+          modifiedTime: '2016-07-10T20:00:08.096Z',
+          ownedByMe: true,
+          permissions: [{ role: 'owner', emailAddress: defaults.USERNAME }],
+          size: '758051'
+        }
+      ]
+    }
+  },
+  drives: {
+    get: { kind: 'drive#driveList', drives: [] }
+  },
+  [`files/${defaults.ITEM_ID}`]: {
+    get: {
+      kind: 'drive#file',
+      id: defaults.ITEM_ID,
+      name: 'MY DUMMY FILE NAME.mp4',
+      mimeType: 'video/mp4',
+      iconLink: 'https://drive-thirdparty.googleusercontent.com/16/type/video/mp4',
+      thumbnailLink: 'https://DUMMY-THUMBNAIL.com/file.jpg',
+      modifiedTime: '2016-07-10T20:00:08.096Z',
+      ownedByMe: true,
+      permissions: [{ role: 'owner', emailAddress: 'john.doe@transloadit.com' }],
+      size: '758051'
+    }
+  },
+  [`files/${defaults.ITEM_ID}?alt=media&supportsAllDrives=true`]: {
+    get: {}
+  },
+  'https://accounts.google.com/o/oauth2/revoke': {
+    get: {}
+  }
+}
+
+module.exports.expects = {}

+ 64 - 0
packages/@uppy/companion/test/fixtures/dropbox.js

@@ -0,0 +1,64 @@
+const defaults = require('./constants')
+
+module.exports.responses = {
+  'users/get_current_account': {
+    post: {
+      name: {
+        given_name: 'Franz',
+        surname: 'Ferdinand',
+        familiar_name: 'Franz',
+        display_name: 'Franz Ferdinand (Personal)',
+        abbreviated_name: 'FF'
+      },
+      email: defaults.USERNAME,
+      email_verified: true,
+      disabled: false,
+      locale: 'en',
+      referral_link: 'https://db.tt/ZITNuhtI',
+      is_paired: true
+    }
+  },
+  'files/list_folder': {
+    post: {
+      entries: [
+        {
+          '.tag': 'file',
+          name: defaults.ITEM_NAME,
+          id: defaults.ITEM_ID,
+          client_modified: '2015-05-12T15:50:38Z',
+          server_modified: '2015-05-12T15:50:38Z',
+          rev: 'a1c10ce0dd78',
+          size: defaults.FILE_SIZE,
+          path_lower: '/homework/math/prime_numbers.txt',
+          path_display: '/Homework/math/Prime_Numbers.txt',
+          is_downloadable: true,
+          has_explicit_shared_members: false,
+          content_hash: 'e3b0c44298fc1c149afbf41e4649b934ca49',
+          file_lock_info: {
+            is_lockholder: true,
+            lockholder_name: 'Imaginary User',
+            created: '2015-05-12T15:50:38Z'
+          }
+        }
+      ],
+      cursor: 'ZtkX9_EHj3x7PMkVuFIhwKYXEpwpLwyxp9vMKomUhllil9q7eWiAu',
+      has_more: false
+    }
+  },
+  'files/get_metadata': {
+    post: {
+      size: defaults.FILE_SIZE
+    }
+  },
+  'auth/token/revoke': {
+    post: {}
+  },
+  'https://content.dropboxapi.com/2/files/download': {
+    post: {}
+  }
+}
+
+module.exports.expects = {
+  itemIcon: 'file',
+  itemRequestPath: '%2Fhomework%2Fmath%2Fprime_numbers.txt'
+}

+ 56 - 0
packages/@uppy/companion/test/fixtures/facebook.js

@@ -0,0 +1,56 @@
+const defaults = require('./constants')
+
+module.exports.responses = {
+  me: {
+    get: {
+      name: 'Fiona Fox',
+      birthday: '01/01/1985',
+      email: defaults.USERNAME
+    }
+  },
+  'https://graph.facebook.com/ALBUM-ID/photos': {
+    get: {
+      data: [
+        {
+          images: [
+            {
+              height: 1365,
+              source: defaults.THUMBNAIL_URL,
+              width: 2048
+            }
+          ],
+          width: 720,
+          height: 479,
+          created_time: '2015-07-17T17:26:50+0000',
+          id: defaults.ITEM_ID
+        }
+      ],
+      paging: {}
+    }
+  },
+  'me/permissions': {
+    delete: {}
+  },
+  [`https://graph.facebook.com/${defaults.ITEM_ID}?fields=images`]: {
+    get: {
+      images: [
+        {
+          height: 1365,
+          source: defaults.THUMBNAIL_URL,
+          width: 2048
+        }
+      ],
+      id: defaults.ITEM_ID
+    }
+  },
+  [defaults.THUMBNAIL_URL]: {
+    get: {}
+  }
+}
+
+module.exports.expects = {
+  listPath: 'ALBUM-ID',
+  itemName: `${defaults.ITEM_ID} 2015-07-17T17:26:50+0000`,
+  itemMimeType: 'image/jpeg',
+  itemSize: null
+}

+ 10 - 0
packages/@uppy/companion/test/fixtures/index.js

@@ -0,0 +1,10 @@
+module.exports.providers = {
+  drive: require('./drive'),
+  dropbox: require('./dropbox'),
+  instagram: require('./instagram'),
+  onedrive: require('./onedrive'),
+  facebook: require('./facebook'),
+  zoom: require('./zoom')
+}
+
+module.exports.defaults = require('./constants')

+ 39 - 0
packages/@uppy/companion/test/fixtures/instagram.js

@@ -0,0 +1,39 @@
+const defaults = require('./constants')
+
+module.exports.responses = {
+  'https://graph.instagram.com/me': {
+    get: {
+      id: '17841405793187218',
+      username: defaults.USERNAME
+    }
+  },
+  'https://graph.instagram.com/me/media': {
+    get: {
+      data: [
+        {
+          id: defaults.ITEM_ID,
+          media_type: 'IMAGE',
+          timestamp: '2017-08-31T18:10:00+0000',
+          media_url: defaults.THUMBNAIL_URL
+        }
+      ]
+    }
+  },
+  [`https://graph.instagram.com/${defaults.ITEM_ID}`]: {
+    get: {
+      id: defaults.ITEM_ID,
+      media_type: 'IMAGE',
+      media_url: defaults.THUMBNAIL_URL,
+      timestamp: '2017-08-31T18:10:00+0000'
+    }
+  },
+  [defaults.THUMBNAIL_URL]: {
+    get: {}
+  }
+}
+
+module.exports.expects = {
+  itemName: 'Instagram 2017-08-31T18:10:00+00000.jpeg',
+  itemMimeType: 'image/jpeg',
+  itemSize: null
+}

+ 61 - 0
packages/@uppy/companion/test/fixtures/onedrive.js

@@ -0,0 +1,61 @@
+const defaults = require('./constants')
+
+module.exports.responses = {
+  me: {
+    get: {
+      userPrincipalName: defaults.USERNAME,
+      mail: defaults.USERNAME
+    }
+  },
+  '/drive/root/children': {
+    get: {
+      value: [
+        {
+          createdDateTime: '2020-01-31T15:40:26.197Z',
+          id: defaults.ITEM_ID,
+          lastModifiedDateTime: '2020-01-31T15:40:38.723Z',
+          name: defaults.ITEM_NAME,
+          size: defaults.FILE_SIZE,
+          parentReference: {
+            driveId: 'DUMMY-DRIVE-ID',
+            driveType: 'personal',
+            path: '/drive/root:'
+          },
+          file: {
+            mimeType: defaults.MIME_TYPE
+          },
+          thumbnails: [{
+            id: '0',
+            large: {
+              height: 452,
+              url: defaults.THUMBNAIL_URL,
+              width: 800
+            },
+            medium: {
+              height: 100,
+              url: defaults.THUMBNAIL_URL,
+              width: 176
+            },
+            small: {
+              height: 54,
+              url: defaults.THUMBNAIL_URL,
+              width: 96
+            }
+          }]
+        }
+      ]
+    }
+  },
+  [`/drives/DUMMY-DRIVE-ID/items/${defaults.ITEM_ID}`]: {
+    get: {
+      size: defaults.FILE_SIZE
+    }
+  },
+  [`/drives/DUMMY-DRIVE-ID/items/${defaults.ITEM_ID}/content`]: {
+    get: {}
+  }
+}
+
+module.exports.expects = {
+  itemRequestPath: `${defaults.ITEM_ID}?driveId=DUMMY-DRIVE-ID`
+}

+ 65 - 0
packages/@uppy/companion/test/fixtures/zoom.js

@@ -0,0 +1,65 @@
+module.exports.responses = {
+  'https://zoom.us/v2/users/me': {
+    get: {
+      id: 'DUMMY-USER-ID',
+      first_name: 'John',
+      last_name: 'Doe',
+      email: 'john.doe@transloadit.com',
+      timezone: '',
+      dept: '',
+      created_at: '2020-07-21T09:13:30Z',
+      last_login_time: '2020-10-12T07:55:02Z',
+      group_ids: [],
+      im_group_ids: [],
+      account_id: 'DUMMY-ACCOUNT-ID',
+      language: 'en-US'
+    }
+  },
+  'https://zoom.us/v2/meetings/DUMMY-UUID%3D%3D/recordings': {
+    get: {
+      uuid: 'DUMMY-UUID==',
+      id: 12345678900,
+      account_id: 'DUMMY-ACCOUNT-ID',
+      host_id: 'DUMMY-HOST-ID',
+      topic: 'DUMMY TOPIC',
+      type: 2,
+      start_time: '2020-05-29T13:19:40Z',
+      timezone: 'Europe/Amsterdam',
+      duration: 0,
+      total_size: 723389,
+      recording_count: 4,
+      recording_files:
+        [
+          {
+            id: 'DUMMY-FILE-ID',
+            meeting_id: 'DUMMY-UUID==',
+            recording_start: '2020-05-29T13:23:57Z',
+            recording_end: '2020-05-29T13:24:02Z',
+            file_type: 'MP4',
+            file_size: 758051,
+            play_url: 'https://us02web.zoom.us/rec/play/DUMMY-DOWNLOAD-PATH',
+            download_url: 'https://us02web.zoom.us/rec/download/DUMMY-DOWNLOAD-PATH',
+            status: 'completed',
+            recording_type: 'shared_screen_with_speaker_view'
+          }
+        ]
+    }
+  },
+  'https://us02web.zoom.us/rec/play/DUMMY-DOWNLOAD-PATH': {
+    get: {}
+  },
+  'https://api.zoom.us/oauth/data/compliance': {
+    post: {}
+  },
+  'https://zoom.us/oauth/revoke': {
+    post: {}
+  }
+}
+
+module.exports.expects = {
+  listPath: 'DUMMY-UUID%3D%3D',
+  itemName: 'DUMMY TOPIC - shared screen with speaker view (2020-05-29, 13:23).mp4',
+  itemId: 'DUMMY-UUID%3D%3D__DUMMY-FILE-ID',
+  itemRequestPath: 'DUMMY-UUID%3D%3D?recordingId=DUMMY-FILE-ID',
+  itemIcon: 'video'
+}

+ 25 - 0
packages/@uppy/companion/test/mockoauthstate.js

@@ -0,0 +1,25 @@
+module.exports = () => {
+  return {
+    generateState: () => 'some-cool-nice-encrytpion',
+    addToState: () => 'some-cool-nice-encrytpion',
+    getFromState: (state, key) => {
+      if (state === 'state-with-invalid-instance-url') {
+        return 'http://localhost:3452'
+      }
+
+      if (state === 'state-with-older-version' && key === 'clientVersion') {
+        return '@uppy/companion-client=1.0.1'
+      }
+
+      if (state === 'state-with-newer-version' && key === 'clientVersion') {
+        return '@uppy/companion-client=1.0.3'
+      }
+
+      if (state === 'state-with-newer-version-old-style' && key === 'clientVersion') {
+        return 'companion-client:1.0.2'
+      }
+
+      return 'http://localhost:3020'
+    }
+  }
+}

+ 21 - 0
packages/@uppy/companion/test/mocksocket.js

@@ -0,0 +1,21 @@
+const emitter = require('../src/server/emitter')
+
+module.exports.connect = (uploadToken) => {
+  emitter().emit(`connection:${uploadToken}`)
+}
+
+module.exports.onProgress = (uploadToken, cb) => {
+  emitter().on(uploadToken, (message) => {
+    if (message.action === 'progress') {
+      cb(message)
+    }
+  })
+}
+
+module.exports.onUploadSuccess = (uploadToken, cb) => {
+  emitter().on(uploadToken, (message) => {
+    if (message.action === 'success') {
+      cb(message)
+    }
+  })
+}