Files
speckle-server/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

310 lines
11 KiB
TypeScript

import { expect } from 'chai'
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
import type { Server } from 'http'
import type { Express } from 'express'
import request from 'supertest'
import { Scopes } from '@/modules/core/helpers/mainConstants'
import cryptoRandomString from 'crypto-random-string'
import { noErrors } from '@/test/helpers'
import { TIME_MS } from '@speckle/shared'
import { initUploadTestEnvironment } from '@/modules/fileuploads/tests/helpers/init'
import { fileURLToPath } from 'url'
const { createStream, createUser, createToken } = initUploadTestEnvironment()
const gqlQueryToListFileUploads = `query ($streamId: String!) {
stream(id: $streamId) {
id
fileUploads {
id
fileName
convertedStatus
}
}
}`
describe('FileUploads @fileuploads integration', () => {
let server: Server
let app: Express
const userOne = {
name: 'User',
email: 'user@example.org',
password: 'jdsadjsadasfdsa'
}
let userOneId: string
let userOneToken: string
let createdStreamId: string
let existingCanonicalUrl: string
let existingPort: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let sendRequest: (token: string, query: string | object) => Promise<any>
let serverAddress: string
let serverPort: string
before(async () => {
const ctx = await beforeEachContext()
server = ctx.server
app = ctx.app
;({ serverAddress, serverPort, sendRequest } = await initializeTestServer(ctx))
//TODO does mocha have a nicer way of temporarily swapping an environment variable, like vitest?
existingCanonicalUrl = process.env['CANONICAL_URL'] || ''
existingPort = process.env['PORT'] || ''
process.env['CANONICAL_URL'] = serverAddress
process.env['PORT'] = serverPort
userOneId = await createUser(userOne)
})
beforeEach(async () => {
createdStreamId = await createStream({ ownerId: userOneId })
;({ token: userOneToken } = await createToken({
userId: userOneId,
name: 'test token',
scopes: [Scopes.Streams.Write]
}))
})
afterEach(async () => {
createdStreamId = ''
})
after(async () => {
process.env['CANONICAL_URL'] = existingCanonicalUrl
process.env['PORT'] = existingPort
await server?.close()
})
describe('Uploads files', () => {
it('Should upload a single file', async () => {
const readmePath = fileURLToPath(import.meta.resolve('@/readme.md'))
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.set('Accept', 'application/json')
.attach('test.ifc', readmePath, 'test.ifc')
expect(response.statusCode).to.equal(201)
expect(response.headers['content-type']).to.contain('application/json;')
expect(response.body.uploadResults).to.have.lengthOf(1)
expect(response.body.uploadResults[0].fileName).to.equal('test.ifc')
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(gqlResponse.body.data.stream.fileUploads).to.have.lengthOf(1)
expect(gqlResponse.body.data.stream.fileUploads[0].fileName).to.equal('test.ifc')
expect(gqlResponse.body.data.stream.fileUploads[0].id).to.equal(
response.body.uploadResults[0].blobId
)
//TODO expect subscription notification
})
it('Uploads from multipart upload', async () => {
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.attach('blob1', fileURLToPath(import.meta.resolve('@/readme.md')), 'test1.ifc')
.attach(
'blob2',
fileURLToPath(import.meta.resolve('@/package.json')),
'test2.ifc'
)
expect(response.status).to.equal(201)
expect(response.headers['content-type']).to.contain('application/json;')
expect(response.body.uploadResults).to.have.lengthOf(2)
expect(
response.body.uploadResults.map((file: { fileName: string }) => file.fileName)
).to.have.members(['test1.ifc', 'test2.ifc'])
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(2)
expect(
gqlResponse.body.data.stream.fileUploads.map(
(file: { fileName: string }) => file.fileName
)
).to.have.members(['test1.ifc', 'test2.ifc'])
expect(
gqlResponse.body.data.stream.fileUploads.map((file: { id: string }) => file.id)
).to.have.members(
response.body.uploadResults.map((file: { blobId: string }) => file.blobId)
)
})
it('Returns 400 for bad form data', async () => {
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.set('Content-type', 'multipart/form-data; boundary=XXX')
// sending an unfinished part
.send('--XXX\r\nCon')
expect(response.status).to.equal(400)
expect(response.headers['content-type']).to.contain('application/json;')
expect(response.body.error).to.contain('Upload request error.')
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(0)
})
it('Returns 400 for missing headers', async () => {
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
// .set('Content-type', 'multipart/form-data; boundary=XXX') // purposely missing content type
expect(response.status).to.equal(400)
expect(response.headers['content-type']).to.contain('application/json;')
expect(response.body.error.message).to.contain('Missing Content-Type')
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(0)
})
it('Returns OK but describes errors for too big files', async () => {
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.attach('toolarge.ifc', Buffer.alloc(114_857_601, 'asdf'), 'toolarge.ifc')
expect(response.status).to.equal(201)
expect(response.headers['content-type']).to.contain('application/json;')
expect(response.body.uploadResults).to.have.lengthOf(1)
expect(
response.body.uploadResults.map((file: { fileName: string }) => file.fileName)
).to.have.members(['toolarge.ifc'])
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(1)
expect(gqlResponse.body.data.stream.fileUploads[0].id).to.equal(
response.body.uploadResults[0].blobId
)
//TODO expect no notifications
})
//TODO test for bad token
it('Returns 403 for token without stream write permissions', async () => {
const { token: badToken } = await createToken({
userId: userOneId,
name: 'test token',
scopes: [Scopes.Streams.Read],
lifespan: TIME_MS.hour
})
const response = await request(app)
.post(`/api/file/autodetect/${createdStreamId}/main`)
.set('Authorization', `Bearer ${badToken}`)
.set('Accept', 'application/json')
.attach(
'test.ifc',
fileURLToPath(import.meta.resolve('@/readme.md')),
'test.ifc'
)
expect(response.statusCode).to.equal(403)
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(0)
//TODO expect no notifications
})
it('Should not upload a file to a non-existent stream', async () => {
const badStreamId = cryptoRandomString({ length: 10 })
const response = await request(app)
.post(`/api/file/autodetect/${badStreamId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.set('Accept', 'application/json')
.attach(
'test.ifc',
fileURLToPath(import.meta.resolve('@/readme.md')),
'test.ifc'
)
expect(response.statusCode).to.equal(404) //FIXME should be 404 (technically a 401, but we don't want to leak existence of stream so 404 is preferrable)
const gqlResponse = await sendRequest(userOneToken, {
query: gqlQueryToListFileUploads,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(0)
//TODO expect no subscription notifications
})
it('Should not upload a file to a stream you do not have access to', async () => {
const userTwo = {
name: 'User Two',
email: 'user2@example.org',
password: 'jdsadjsadasfdsa'
}
const userTwoId = await createUser(userTwo)
const streamTwoId = await createStream({ ownerId: userTwoId })
const response = await request(app)
.post(`/api/file/autodetect/${streamTwoId}/main`)
.set('Authorization', `Bearer ${userOneToken}`)
.set('Accept', 'application/json')
.attach(
'test.ifc',
fileURLToPath(import.meta.resolve('@/readme.md')),
'test.ifc'
)
expect(response.statusCode).to.equal(403)
expect(response.body).to.deep.equal({
error: 'You do not have access to the project'
})
const gqlResponse = await sendRequest(userOneToken, {
query: `query ($streamId: String!) {
stream(id: $streamId) {
id
fileUploads {
id
fileName
convertedStatus
}
}
}`,
variables: { streamId: createdStreamId }
})
expect(noErrors(gqlResponse))
expect(
gqlResponse.body.data.stream.fileUploads,
JSON.stringify(gqlResponse.body.data)
).to.have.lengthOf(0)
//TODO expect no subscription notifications
})
})
})