From 0c8dd19dc28c396c963757db34ab8f8ae7237231 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Mon, 3 Nov 2025 18:34:07 +0800 Subject: [PATCH] Companion option `uploadHeaders` (#5981) closes #5921 also improve tests that currently depend on eachother todo - [x] docs https://github.com/transloadit/uppy.io/pull/394 --- .changeset/nine-rice-brake.md | 5 ++ .../@uppy/companion/src/server/Uploader.js | 16 +++-- .../@uppy/companion/src/standalone/helper.js | 3 + .../@uppy/companion/test/companion.test.js | 6 +- .../@uppy/companion/test/credentials.test.js | 53 +++++++++------ .../companion/test/deauthorization.test.js | 10 ++- .../@uppy/companion/test/http-agent.test.js | 6 +- .../@uppy/companion/test/providers.test.js | 24 +++---- .../@uppy/companion/test/uploader.test.js | 65 ++++++++++--------- packages/@uppy/companion/test/url.test.js | 15 +++-- 10 files changed, 126 insertions(+), 77 deletions(-) create mode 100644 .changeset/nine-rice-brake.md diff --git a/.changeset/nine-rice-brake.md b/.changeset/nine-rice-brake.md new file mode 100644 index 000000000..2bf96bf39 --- /dev/null +++ b/.changeset/nine-rice-brake.md @@ -0,0 +1,5 @@ +--- +"@uppy/companion": patch +--- + +New Companion option `uploadHeaders` which can be used to include a static set of headers with every request sent to all upload destinations. diff --git a/packages/@uppy/companion/src/server/Uploader.js b/packages/@uppy/companion/src/server/Uploader.js index 9ff8e7d34..4e3b71e1a 100644 --- a/packages/@uppy/companion/src/server/Uploader.js +++ b/packages/@uppy/companion/src/server/Uploader.js @@ -148,10 +148,18 @@ export default class Uploader { * @property {number} [chunkSize] * @property {string} [providerName] * - * @param {UploaderOptions} options + * @param {UploaderOptions} optionsIn */ - constructor(options) { - validateOptions(options) + constructor(optionsIn) { + validateOptions(optionsIn) + + const options = { + ...optionsIn, + headers: { + ...optionsIn.headers, + ...optionsIn.companionOptions.uploadHeaders, + }, + } this.providerName = options.providerName this.options = options @@ -682,7 +690,7 @@ export default class Uploader { try { const httpMethod = (this.options.httpMethod || '').toUpperCase() === 'PUT' ? 'put' : 'post' - const runRequest = await got[httpMethod] + const runRequest = got[httpMethod] const response = await runRequest(url, reqOptions) diff --git a/packages/@uppy/companion/src/standalone/helper.js b/packages/@uppy/companion/src/standalone/helper.js index e3adb4fb2..80299e92b 100644 --- a/packages/@uppy/companion/src/standalone/helper.js +++ b/packages/@uppy/companion/src/standalone/helper.js @@ -215,6 +215,9 @@ const getConfigFromEnv = () => { process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS === 'true', testDynamicOauthCredentialsSecret: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET, + uploadHeaders: process.env.COMPANION_UPLOAD_HEADERS + ? JSON.parse(process.env.COMPANION_UPLOAD_HEADERS) + : undefined, } } diff --git a/packages/@uppy/companion/test/companion.test.js b/packages/@uppy/companion/test/companion.test.js index 48f5e5a73..c034760fa 100644 --- a/packages/@uppy/companion/test/companion.test.js +++ b/packages/@uppy/companion/test/companion.test.js @@ -1,6 +1,6 @@ import nock from 'nock' import request from 'supertest' -import { afterAll, describe, expect, it, test, vi } from 'vitest' +import { afterAll, afterEach, describe, expect, it, test, vi } from 'vitest' import packageJson from '../package.json' with { type: 'json' } import * as tokenService from '../src/server/helpers/jwt.js' import * as defaults from './fixtures/constants.js' @@ -37,8 +37,10 @@ const authData = { const token = tokenService.generateEncryptedAuthToken(authData, secret) const OAUTH_STATE = 'some-cool-nice-encrytpion' -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) diff --git a/packages/@uppy/companion/test/credentials.test.js b/packages/@uppy/companion/test/credentials.test.js index 25f4fff84..71a72a3c3 100644 --- a/packages/@uppy/companion/test/credentials.test.js +++ b/packages/@uppy/companion/test/credentials.test.js @@ -1,6 +1,14 @@ import nock from 'nock' import request from 'supertest' -import { afterAll, describe, expect, test, vi } from 'vitest' +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from 'vitest' import * as tokenService from '../src/server/helpers/jwt.js' import { nockZoomRevoke, expects as zoomExpects } from './fixtures/zoom.js' import { getServer } from './mockserver.js' @@ -21,32 +29,35 @@ const authData = { } const token = tokenService.generateEncryptedAuthToken(authData, secret) -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) describe('providers requests with remote oauth keys', () => { - // mocking request module used to fetch custom oauth credentials - nock('http://localhost:2111') - .post('/zoom-keys') - // @ts-ignore - .reply((uri, { provider, parameters }) => { - if (provider !== 'zoom' || parameters !== 'ZOOM-CREDENTIALS-PARAMS') - return [400] + beforeEach(() => { + // mocking request module used to fetch custom oauth credentials + nock('http://localhost:2111') + .post('/zoom-keys') + // @ts-ignore + .reply((uri, { provider, parameters }) => { + if (provider !== 'zoom' || parameters !== 'ZOOM-CREDENTIALS-PARAMS') + return [400] - return [ - 200, - { - credentials: { - key: remoteZoomKey, - secret: remoteZoomSecret, - verificationToken: remoteZoomVerificationToken, + return [ + 200, + { + credentials: { + key: remoteZoomKey, + secret: remoteZoomSecret, + verificationToken: remoteZoomVerificationToken, + }, }, - }, - ] - }) - .persist() + ] + }) + }) test('zoom logout with remote oauth keys happy path', async () => { nockZoomRevoke({ key: remoteZoomKey, secret: remoteZoomSecret }) @@ -69,6 +80,8 @@ describe('providers requests with remote oauth keys', () => { }) test('zoom logout with wrong credentials params', async () => { + nockZoomRevoke({ key: remoteZoomKey, secret: remoteZoomSecret }) + const params = { params: 'WRONG-ZOOM-CREDENTIALS-PARAMS' } const encodedParams = Buffer.from( JSON.stringify(params), diff --git a/packages/@uppy/companion/test/deauthorization.test.js b/packages/@uppy/companion/test/deauthorization.test.js index 1a94c51c4..ab640f58a 100644 --- a/packages/@uppy/companion/test/deauthorization.test.js +++ b/packages/@uppy/companion/test/deauthorization.test.js @@ -1,17 +1,21 @@ import nock from 'nock' import request from 'supertest' -import { afterAll, describe, test, vi } from 'vitest' +import { afterAll, afterEach, beforeEach, describe, test, vi } from 'vitest' import { getServer } from './mockserver.js' vi.mock('express-prom-bundle') -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) describe('handle deauthorization callback', () => { - nock('https://api.zoom.us').post('/oauth/data/compliance').reply(200) + beforeEach(() => { + nock('https://api.zoom.us').post('/oauth/data/compliance').reply(200) + }) test('providers without support for callback endpoint', async () => { return request(await getServer()) diff --git a/packages/@uppy/companion/test/http-agent.test.js b/packages/@uppy/companion/test/http-agent.test.js index 6c24cbb5c..cadce5ba7 100644 --- a/packages/@uppy/companion/test/http-agent.test.js +++ b/packages/@uppy/companion/test/http-agent.test.js @@ -1,12 +1,14 @@ import nock from 'nock' -import { afterAll, describe, expect, test } from 'vitest' +import { afterAll, afterEach, describe, expect, test } from 'vitest' import { FORBIDDEN_IP_ADDRESS, getProtectedGot, } from '../src/server/helpers/request.js' -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) diff --git a/packages/@uppy/companion/test/providers.test.js b/packages/@uppy/companion/test/providers.test.js index 174cd8a3c..ec5f77578 100644 --- a/packages/@uppy/companion/test/providers.test.js +++ b/packages/@uppy/companion/test/providers.test.js @@ -1,6 +1,14 @@ import nock from 'nock' import request from 'supertest' -import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from 'vitest' import * as tokenService from '../src/server/helpers/jwt.js' import * as providerModule from '../src/server/provider/index.js' import * as defaults from './fixtures/constants.js' @@ -76,16 +84,17 @@ function nockGetCurrentAccount(times = 1) { }) } -beforeAll(() => { +beforeEach(() => { const url = new URL(defaults.THUMBNAIL_URL) nock(url.origin) .get(url.pathname) .reply(200, () => '') - .persist() }) -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) @@ -543,13 +552,6 @@ describe('logout of provider', () => { await runTest('box') }) - test('dropbox', async () => { - nock('https://api.dropboxapi.com') - .post('/2/auth/token/revoke') - .reply(200, {}) - await runTest('dropbox') - }) - test('drive', async () => { nock('https://accounts.google.com') .post('/o/oauth2/revoke?token=token+value') diff --git a/packages/@uppy/companion/test/uploader.test.js b/packages/@uppy/companion/test/uploader.test.js index 10624a097..fa5e11be8 100644 --- a/packages/@uppy/companion/test/uploader.test.js +++ b/packages/@uppy/companion/test/uploader.test.js @@ -1,9 +1,7 @@ -import { once } from 'node:events' import fs from 'node:fs' -import { createServer } from 'node:http' import { Readable } from 'node:stream' import nock from 'nock' -import { afterAll, describe, expect, test, vi } from 'vitest' +import { afterAll, afterEach, describe, expect, test, vi } from 'vitest' import Emitter from '../src/server/emitter/index.js' import Uploader, { ValidationError } from '../src/server/Uploader.js' import standalone from '../src/standalone/index.js' @@ -12,8 +10,10 @@ import * as socketClient from './mocksocket.js' vi.mock('tus-js-client') vi.mock('express-prom-bundle') -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) @@ -24,7 +24,7 @@ const { companionOptions } = standalone() const mockReq = {} -describe('uploader with tus protocol', () => { +describe('uploader', () => { test('uploader respects uploadUrls', async () => { const opts = { endpoint: 'http://localhost/files', @@ -207,12 +207,14 @@ describe('uploader with tus protocol', () => { useFormData, includeSize = true, address = 'localhost', + // @ts-ignore + extraCompanionOpts, } = {}) { const fileContent = Buffer.from('Some file content') const stream = Readable.from([fileContent]) const opts = { - companionOptions, + companionOptions: { ...companionOptions, ...extraCompanionOpts }, endpoint: `http://${address}`, protocol: 'multipart', size: includeSize ? fileContent.length : undefined, @@ -227,32 +229,33 @@ describe('uploader with tus protocol', () => { } test('upload functions with xhr protocol', async () => { - let alreadyCalled = false - // We are creating our own test server for this test - // instead of using nock because of a bug when passing a Node.js stream to got. - // Ref: https://github.com/nock/nock/issues/2595 - const server = createServer((req, res) => { - if (alreadyCalled) throw new Error('already called') - alreadyCalled = true - if (req.url === '/' && req.method === 'POST') { - res.writeHead(200) - res.end('OK') - } - }).listen() - try { - await once(server, 'listening') + nock('http://localhost').post('/').reply(200, 'OK') + const ret = await runMultipartTest() + expect(ret).toMatchObject({ + url: null, + extraData: { response: expect.anything(), bytesUploaded: 17 }, + }) + }) - const ret = await runMultipartTest({ - // @ts-ignore - address: `localhost:${server.address().port}`, - }) - expect(ret).toMatchObject({ - url: null, - extraData: { response: expect.anything(), bytesUploaded: 17 }, - }) - } finally { - server.close() - } + test('header companion option gets passed along to destination endpoint', async () => { + nock('http://localhost') + .post('/') + .matchHeader('header-a', '1') + .matchHeader('header-b', '2') + .reply(200, () => '') + + const ret = await runMultipartTest({ + // @ts-ignore + extraCompanionOpts: { + uploadHeaders: { 'header-a': '1', 'header-b': '2' }, + }, + }) + expect(ret).toMatchObject({ + url: null, + extraData: { response: expect.anything(), bytesUploaded: 17 }, + }) + + expect(ret.extraData.response?.headers?.['header-a']).toBeUndefined() // headers sent to destination, not received back }) const formDataNoMetaMatch = diff --git a/packages/@uppy/companion/test/url.test.js b/packages/@uppy/companion/test/url.test.js index f879e0bcc..dfcbc1864 100644 --- a/packages/@uppy/companion/test/url.test.js +++ b/packages/@uppy/companion/test/url.test.js @@ -1,6 +1,6 @@ import nock from 'nock' import request from 'supertest' -import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' +import { afterAll, afterEach, describe, expect, test, vi } from 'vitest' import { getServer } from './mockserver.js' @@ -18,14 +18,15 @@ vi.mock('../src/server/helpers/request.js', async () => { const getMockServer = async () => getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' }) -beforeAll(() => { +const nockUrl = () => nock('http://url.myendpoint.com') .get('/files') .reply(200, () => '') -}) -afterAll(() => { +afterEach(() => { nock.cleanAll() +}) +afterAll(() => { nock.restore() }) @@ -52,6 +53,8 @@ describe('url meta', () => { }) test.each(invalids)('return 400 for invalid url', async (urlCase) => { + nockUrl() + return request(await getMockServer()) .post('/url/meta') .set('Content-Type', 'application/json') @@ -65,6 +68,8 @@ describe('url meta', () => { describe('url get', () => { test('url download gets instanitated', async () => { + nockUrl() + return request(await getMockServer()) .post('/url/get') .set('Content-Type', 'application/json') @@ -80,6 +85,8 @@ describe('url get', () => { test.each(invalids)( 'downloads are not instantiated for invalid urls', async (urlCase) => { + nockUrl() + return request(await getMockServer()) .post('/url/get') .set('Content-Type', 'application/json')