uppy/packages/@uppy/companion/test/companion.test.ts
2026-05-21 02:36:00 +05:30

429 lines
12 KiB
TypeScript

import nock from 'nock'
import request from 'supertest'
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 { isRecord } from '../src/server/helpers/type-guards.js'
import * as defaults from './fixtures/constants.js'
import { nockGoogleDownloadFile } from './fixtures/drive.js'
import mockOauthState from './mockoauthstate.js'
import { getServer } from './mockserver.js'
vi.mock('express-prom-bundle')
vi.mock('tus-js-client')
mockOauthState()
const fakeLocalhost = 'localhost.com'
vi.mock('node:dns', () => ({
default: {
lookup: (
hostname: string,
_options: unknown,
callback: (err: Error | null, address?: string, family?: number) => void,
) => {
if (fakeLocalhost === hostname || hostname === 'localhost') {
return callback(null, '127.0.0.1', 4)
}
return callback(new Error(`Unexpected call to hostname ${hostname}`))
},
},
}))
const getServerWithEnv = async () =>
getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
const secret = 'secret'
const authData = {
dropbox: { accessToken: 'token value' },
box: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(authData, secret)
const OAUTH_STATE = 'some-cool-nice-encrytpion'
afterEach(() => {
nock.cleanAll()
})
afterAll(() => {
nock.restore()
})
describe('validate upload data', () => {
test('access token expired or invalid when starting provider download', async () => {
const meta: { size: null; mimeType: string; id: string } = {
size: null,
mimeType: 'video/mp4',
id: defaults.ITEM_ID,
}
nock('https://www.googleapis.com')
.get(`/drive/v3/files/${defaults.ITEM_ID}`)
.query(() => true)
.reply(200, meta)
nock('https://www.googleapis.com')
.get(
`/drive/v3/files/${defaults.ITEM_ID}?alt=media&supportsAllDrives=true`,
)
.reply(401, {
error: {
code: 401,
message:
'Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.',
status: 'UNAUTHENTICATED',
},
})
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
httpMethod: 'POST',
})
.expect(401)
.then((res) =>
expect(res.body.message).toBe(
'HTTP 401: invalid access token detected by Provider',
),
)
})
test('invalid upload protocol gets rejected', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tusInvalid',
})
.expect(400)
.then((res) =>
expect(res.body.message).toBe('unsupported protocol specified'),
)
})
test('invalid upload fieldname gets rejected', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
fieldname: 390,
})
.expect(400)
.then((res) =>
expect(res.body.message).toBe('fieldname must be a string'),
)
})
test('invalid upload metadata gets rejected', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
metadata: 'I am a string instead of object',
})
.expect(400)
.then((res) =>
expect(res.body.message).toBe('metadata must be an object'),
)
})
test('invalid upload headers get rejected', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
headers: 'I am a string instead of object',
})
.expect(400)
.then((res) => expect(res.body.message).toBe('headers must be an object'))
})
test('invalid upload HTTP Method gets rejected', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
httpMethod: 'DELETE',
})
.expect(400)
.then((res) =>
expect(res.body.message).toBe('unsupported HTTP METHOD specified'),
)
})
test('valid upload data is allowed - tus', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
httpMethod: 'POST',
headers: {
customheader: 'header value',
},
metadata: {
mymetadata: 'matadata value',
},
fieldname: 'uploadField',
})
.expect(200)
})
test('valid upload data is allowed - s3-multipart', async () => {
nockGoogleDownloadFile()
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
.send({
endpoint: 'http://url.myendpoint.com/files',
protocol: 's3-multipart',
httpMethod: 'PUT',
headers: {
customheader: 'header value',
},
metadata: {
mymetadata: 'matadata value',
},
fieldname: 'uploadField',
})
.expect(200)
})
})
describe('handle main oauth redirect', async () => {
const serverWithMainOauth = await getServer({
COMPANION_OAUTH_DOMAIN: 'localhost:3040',
})
test('redirect to a valid uppy instance', () => {
return request(serverWithMainOauth)
.get(`/dropbox/redirect?state=${OAUTH_STATE}`)
.set('uppy-auth-token', token)
.expect(302)
.expect(
'Location',
`http://localhost:3020/connect/dropbox/callback?state=${OAUTH_STATE}`,
)
})
test('do not redirect to invalid uppy instances', () => {
const state = 'state-with-invalid-instance-url' // see mock ../../src/server/helpers/oauth-state above
return request(serverWithMainOauth)
.get(`/dropbox/redirect?state=${state}`)
.set('uppy-auth-token', token)
.expect(400)
})
})
it('periodically pings', async () => {
await Promise.all([
getServer({
COMPANION_PERIODIC_PING_URLS: 'http://localhost/ping',
COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD: '{"some": "value"}',
COMPANION_PERIODIC_PING_INTERVAL: '10',
COMPANION_PERIODIC_PING_COUNT: '1',
}),
new Promise<void>((resolve) => {
nock('http://localhost')
.post(
'/ping',
(body: unknown) =>
isRecord(body) &&
body['some'] === 'value' &&
body['version'] === packageJson.version &&
typeof body['processId'] === 'string',
)
.reply(200, () => resolve())
}),
])
}, 3000)
async function runUrlMetaTest(url: string) {
const server = await getServer()
return request(server).post('/url/meta').send({ url })
}
async function runUrlGetTest(url: string) {
const server = await getServer()
return request(server).post('/url/get').send({
fileId: url,
metadata: {},
endpoint: 'http://url.myendpoint.com/files',
protocol: 'tus',
size: null,
url,
})
}
it('respects allowLocalUrls, localhost', async () => {
let res = await runUrlMetaTest('http://localhost/')
expect(res.statusCode).toBe(400)
expect(res.body).toEqual({ error: 'Invalid request body' })
res = await runUrlGetTest('http://localhost/')
expect(res.statusCode).toBe(400)
expect(res.body).toEqual({ error: 'Invalid request body' })
})
describe('respects allowLocalUrls, valid hostname that resolves to localhost', () => {
test('meta', async () => {
const res = await runUrlMetaTest(`http://${fakeLocalhost}/`)
expect(res.statusCode).toBe(500)
expect(res.body).toEqual({ message: 'failed to fetch URL metadata' })
})
test('get', async () => {
const res = await runUrlGetTest(`http://${fakeLocalhost}/`)
expect(res.statusCode).toBe(500)
expect(res.body).toEqual({ message: 'failed to fetch URL' })
})
})
describe('S3 controller', () => {
test('createMultipartUpload rejects missing filename', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.post('/s3/multipart')
.send({
type: 'image/png',
metadata: {},
// filename is intentionally missing
})
.expect(400)
.then((res) =>
expect(res.body.error).toBe(
's3: the "filename" field is required and must be a non-empty string',
),
)
})
test('createMultipartUpload rejects empty filename', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.post('/s3/multipart')
.send({
type: 'image/png',
metadata: {},
filename: '',
})
.expect(400)
.then((res) =>
expect(res.body.error).toBe(
's3: the "filename" field is required and must be a non-empty string',
),
)
})
test('createMultipartUpload rejects non-string filename', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.post('/s3/multipart')
.send({
type: 'image/png',
metadata: {},
filename: 12345,
})
.expect(400)
.then((res) =>
expect(res.body.error).toBe(
's3: the "filename" field is required and must be a non-empty string',
),
)
})
test('getUploadParameters rejects missing filename', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.get('/s3/params')
.query({
type: 'image/png',
// filename is intentionally missing
})
.expect(400)
.then((res) =>
expect(res.body.error).toBe(
's3: the "filename" query parameter is required and must be a non-empty string',
),
)
})
test('getUploadParameters rejects empty filename', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.get('/s3/params')
.query({
type: 'image/png',
filename: '',
})
.expect(400)
.then((res) =>
expect(res.body.error).toBe(
's3: the "filename" query parameter is required and must be a non-empty string',
),
)
})
})