mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
* Prepare image uploads in chat * files instead of images * Fix file type and query * Add dummy data to resolver * fix graphql types * Fix file upload, remove unncessary code * Re-add fetch * Fix room order after sent message * Update backend/src/graphql/queries/messageQuery.ts * Move room to top of list when a message is received * working prototype chat file upload * remove console * allow to upload all kinds of files * multiple images * revert changes in S3 Images * tag mimetype * accept any file * lint fix * remove snapshot flakyness * remove whitelist test * fix messages spec * fix query * more query fixes * fix seed * made message resolver tests independent * lint * started specc for attachments * more tests & fixes * fix empty room error * remove console logs * fix tests * fix createRoom last Messsage error properly * lint fixes * reduce changeset * simplify config check * reduce changeset * missing change * allow speech capture * Fix file download * Implement proper download --------- Co-authored-by: Maximilian Harz <maxharz@gmail.com>
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
/* eslint-disable @typescript-eslint/require-await */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
import { ReadStream } from 'node:fs'
|
|
import { Readable } from 'node:stream'
|
|
|
|
import { S3Client } from '@aws-sdk/client-s3'
|
|
import { Upload } from '@aws-sdk/lib-storage'
|
|
import { UserInputError } from 'apollo-server'
|
|
import { createTestClient } from 'apollo-server-testing'
|
|
|
|
import databaseContext from '@context/database'
|
|
import Factory, { cleanDatabase } from '@db/factories'
|
|
import File from '@db/models/File'
|
|
import { CreateMessage } from '@graphql/queries/CreateMessage'
|
|
import { createRoomMutation } from '@graphql/queries/createRoomMutation'
|
|
import type { S3Configured } from '@src/config'
|
|
import createServer, { getContext } from '@src/server'
|
|
|
|
import { attachments } from './attachments'
|
|
|
|
import type { FileInput } from './attachments'
|
|
|
|
const s3SendMock = jest.fn()
|
|
jest.spyOn(S3Client.prototype, 'send').mockImplementation(s3SendMock)
|
|
|
|
jest.mock('@aws-sdk/lib-storage')
|
|
|
|
const UploadMock = {
|
|
done: () => {
|
|
return {
|
|
Location: 'http://your-objectstorage.com/bucket/',
|
|
}
|
|
},
|
|
}
|
|
|
|
;(Upload as unknown as jest.Mock).mockImplementation(() => UploadMock)
|
|
|
|
const config: S3Configured = {
|
|
AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID',
|
|
AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY',
|
|
AWS_BUCKET: 'AWS_BUCKET',
|
|
AWS_ENDPOINT: 'AWS_ENDPOINT',
|
|
AWS_REGION: 'AWS_REGION',
|
|
S3_PUBLIC_GATEWAY: undefined,
|
|
}
|
|
|
|
const database = databaseContext()
|
|
|
|
let authenticatedUser, server, mutate
|
|
|
|
beforeAll(async () => {
|
|
await cleanDatabase()
|
|
|
|
const contextUser = async (_req) => authenticatedUser
|
|
const context = getContext({ user: contextUser, database })
|
|
|
|
server = createServer({ context }).server
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
mutate = createTestClient(server).mutate
|
|
})
|
|
|
|
afterAll(async () => {
|
|
void server.stop()
|
|
void database.driver.close()
|
|
database.neode.close()
|
|
})
|
|
|
|
/* uploadCallback = jest.fn(
|
|
({ uniqueFilename }) => `http://your-objectstorage.com/bucket/${uniqueFilename}`,
|
|
)
|
|
*/
|
|
|
|
afterEach(async () => {
|
|
await cleanDatabase()
|
|
})
|
|
|
|
describe('delete Attachment', () => {
|
|
const { del: deleteAttachment } = attachments(config)
|
|
describe('given a resource with an attachment', () => {
|
|
let user: { id: string }
|
|
let chatPartner: { id: string }
|
|
let file: { id: string }
|
|
let message: { id: string }
|
|
beforeEach(async () => {
|
|
const u = await Factory.build('user')
|
|
user = await u.toJson()
|
|
|
|
const u2 = await Factory.build('user')
|
|
chatPartner = await u2.toJson()
|
|
|
|
authenticatedUser = user
|
|
const { data: room } = await mutate({
|
|
mutation: createRoomMutation(),
|
|
variables: {
|
|
userId: chatPartner.id,
|
|
},
|
|
})
|
|
|
|
const f = await Factory.build('file', {
|
|
url: 'http://localhost/some/file/url/',
|
|
name: 'This is a file attached to a message',
|
|
type: 'application/dummy',
|
|
})
|
|
file = await f.toJson()
|
|
|
|
const m = await mutate({
|
|
mutation: CreateMessage,
|
|
variables: {
|
|
roomId: room?.CreateRoom.id,
|
|
content: 'test messsage',
|
|
},
|
|
})
|
|
|
|
message = m.data.CreateMessage
|
|
|
|
await database.write({
|
|
query: `
|
|
MATCH (message:Message {id: $message.id}), (file:File{url: $file.url})
|
|
MERGE (message)-[:ATTACHMENT]->(file)
|
|
`,
|
|
variables: {
|
|
message,
|
|
file,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('deletes `File` node', async () => {
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(1)
|
|
await deleteAttachment(message, 'ATTACHMENT')
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(0)
|
|
})
|
|
|
|
describe('given a transaction parameter', () => {
|
|
it('executes cypher statements within the transaction', async () => {
|
|
const session = database.driver.session()
|
|
let someString: string
|
|
try {
|
|
someString = await session.writeTransaction(async (transaction) => {
|
|
await deleteAttachment(message, 'ATTACHMENT', {
|
|
transaction,
|
|
})
|
|
const txResult = await transaction.run('RETURN "Hello" as result')
|
|
const [result] = txResult.records.map((record) => record.get('result'))
|
|
return result
|
|
})
|
|
} finally {
|
|
await session.close()
|
|
}
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(0)
|
|
expect(someString).toEqual('Hello')
|
|
})
|
|
|
|
it('rolls back the transaction in case of errors', async () => {
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(1)
|
|
const session = database.driver.session()
|
|
try {
|
|
await session.writeTransaction(async (transaction) => {
|
|
await deleteAttachment(message, 'ATTACHMENT', {
|
|
transaction,
|
|
})
|
|
throw new Error('Ouch!')
|
|
})
|
|
// eslint-disable-next-line no-catch-all/no-catch-all
|
|
} catch (err) {
|
|
// nothing has been deleted
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(1)
|
|
// all good
|
|
} finally {
|
|
await session.close()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('add Attachment', () => {
|
|
const { add: addAttachment } = attachments(config)
|
|
let fileInput: FileInput
|
|
let post: { id: string }
|
|
beforeEach(() => {
|
|
fileInput = {
|
|
name: 'The name of the new attachment',
|
|
type: 'application/any',
|
|
}
|
|
})
|
|
|
|
describe('given file.upload', () => {
|
|
beforeEach(() => {
|
|
const file1 = Readable.from('file1')
|
|
fileInput = {
|
|
...fileInput,
|
|
// eslint-disable-next-line promise/avoid-new
|
|
upload: new Promise((resolve) =>
|
|
resolve({
|
|
createReadStream: () => file1 as ReadStream,
|
|
filename: 'file1',
|
|
encoding: '7bit',
|
|
mimetype: 'application/json',
|
|
}),
|
|
),
|
|
}
|
|
})
|
|
|
|
describe('on existing resource', () => {
|
|
beforeEach(async () => {
|
|
const p = await Factory.build(
|
|
'post',
|
|
{ id: 'p99' },
|
|
{
|
|
author: Factory.build('user', {}, { avatar: null }),
|
|
image: null,
|
|
},
|
|
)
|
|
post = await p.toJson()
|
|
})
|
|
|
|
it('returns new file', async () => {
|
|
await expect(addAttachment(post, 'ATTACHMENT', fileInput)).resolves.toMatchObject({
|
|
updatedAt: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
name: 'The name of the new attachment',
|
|
type: 'application/any',
|
|
url: 'http://your-objectstorage.com/bucket/',
|
|
})
|
|
})
|
|
|
|
it('creates `:File` node', async () => {
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(0)
|
|
await addAttachment(post, 'ATTACHMENT', fileInput)
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(1)
|
|
})
|
|
|
|
describe('given a `S3_PUBLIC_GATEWAY` configuration', () => {
|
|
const { add: addAttachment } = attachments({
|
|
...config,
|
|
S3_PUBLIC_GATEWAY: 'http://s3-public-gateway.com',
|
|
})
|
|
|
|
it('changes the domain of the URL to a server that could e.g. apply image transformations', async () => {
|
|
if (!fileInput.upload) {
|
|
throw new Error('Test imageInput was not setup correctly.')
|
|
}
|
|
const upload = await fileInput.upload
|
|
upload.filename = '/path/to/file-location/foo-bar-avatar.jpg'
|
|
fileInput.upload = Promise.resolve(upload)
|
|
await expect(addAttachment(post, 'ATTACHMENT', fileInput)).resolves.toMatchObject({
|
|
url: 'http://s3-public-gateway.com/bucket/',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('connects resource with image via given image type', async () => {
|
|
await addAttachment(post, 'ATTACHMENT', fileInput)
|
|
const result = await database.neode.cypher(
|
|
`MATCH(p:Post {id: "p99"})-[:ATTACHMENT]->(f:File) RETURN f,p`,
|
|
{},
|
|
)
|
|
post = database.neode
|
|
.hydrateFirst<{ id: string }>(result, 'p', database.neode.model('Post'))
|
|
.properties()
|
|
const file = database.neode.hydrateFirst(result, 'f', database.neode.model('File'))
|
|
expect(post).toBeTruthy()
|
|
expect(file).toBeTruthy()
|
|
})
|
|
|
|
it('sets metadata', async () => {
|
|
await addAttachment(post, 'ATTACHMENT', fileInput)
|
|
const file = await database.neode.first<typeof File>('File', {}, undefined)
|
|
await expect(file.toJson()).resolves.toMatchObject({
|
|
name: 'The name of the new attachment',
|
|
type: 'application/any',
|
|
createdAt: expect.any(String),
|
|
updatedAt: expect.any(String),
|
|
url: expect.any(String),
|
|
})
|
|
})
|
|
|
|
describe('given a transaction parameter', () => {
|
|
it('executes cypher statements within the transaction', async () => {
|
|
const session = database.driver.session()
|
|
try {
|
|
await session.writeTransaction(async (transaction) => {
|
|
const file = await addAttachment(
|
|
post,
|
|
'ATTACHMENT',
|
|
fileInput,
|
|
{},
|
|
{
|
|
transaction,
|
|
},
|
|
)
|
|
return transaction.run(
|
|
`
|
|
MATCH(file:File {url: $file.url})
|
|
SET file.name = 'This name text gets overwritten'
|
|
RETURN file {.*}
|
|
`,
|
|
{ file },
|
|
)
|
|
})
|
|
} finally {
|
|
await session.close()
|
|
}
|
|
const file = await database.neode.first<typeof File>(
|
|
'File',
|
|
{ name: 'This name text gets overwritten' },
|
|
undefined,
|
|
)
|
|
await expect(file.toJson()).resolves.toMatchObject({
|
|
name: 'This name text gets overwritten',
|
|
})
|
|
})
|
|
|
|
it('rolls back the transaction in case of errors', async () => {
|
|
const session = database.driver.session()
|
|
try {
|
|
await session.writeTransaction(async (transaction) => {
|
|
const file = await addAttachment(post, 'ATTACHMENT', fileInput, {
|
|
transaction,
|
|
})
|
|
return transaction.run('Ooops invalid cypher!', { file })
|
|
})
|
|
// eslint-disable-next-line no-catch-all/no-catch-all
|
|
} catch (err) {
|
|
// nothing has been created
|
|
await expect(database.neode.all('File')).resolves.toHaveLength(0)
|
|
// all good
|
|
} finally {
|
|
await session.close()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('without image.upload', () => {
|
|
it('throws UserInputError', async () => {
|
|
const p = await Factory.build('post', { id: 'p99' }, { image: null })
|
|
post = await p.toJson()
|
|
await expect(addAttachment(post, 'ATTACHMENT', fileInput)).rejects.toEqual(
|
|
new UserInputError('Cannot find attachment for given resource'),
|
|
)
|
|
})
|
|
})
|
|
})
|