mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-04 08:35:32 +00:00
333 lines
10 KiB
TypeScript
333 lines
10 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 */
|
|
/* eslint-disable jest/no-conditional-expect */
|
|
import { Readable } from 'node:stream'
|
|
|
|
import { S3Client } from '@aws-sdk/client-s3'
|
|
import { Upload } from '@aws-sdk/lib-storage'
|
|
|
|
import Factory, { cleanDatabase } from '@db/factories'
|
|
import { UserInputError } from '@graphql/errors'
|
|
import CreateMessage from '@graphql/queries/messaging/CreateMessage.gql'
|
|
import CreateRoom from '@graphql/queries/messaging/CreateRoom.gql'
|
|
import { createApolloTestSetup } from '@root/test/helpers'
|
|
|
|
import { attachments } from './attachments'
|
|
|
|
import type { FileInput } from './attachments'
|
|
import type File from '@db/models/File'
|
|
import type { ApolloTestSetup } from '@root/test/helpers'
|
|
import type { S3Config } from '@src/config'
|
|
import type { Context } from '@src/context'
|
|
import type { ReadStream } from 'node:fs'
|
|
|
|
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: S3Config = {
|
|
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',
|
|
IMAGOR_SECRET: 'IMAGOR_SECRET',
|
|
IMAGOR_PUBLIC_URL: 'IMAGOR_PUBLIC_URL',
|
|
}
|
|
|
|
let authenticatedUser: Context['user']
|
|
const context = () => ({ authenticatedUser, config })
|
|
let mutate: ApolloTestSetup['mutate']
|
|
let database: ApolloTestSetup['database']
|
|
let server: ApolloTestSetup['server']
|
|
|
|
beforeAll(async () => {
|
|
await cleanDatabase()
|
|
|
|
const apolloSetup = await createApolloTestSetup({ context })
|
|
mutate = apolloSetup.mutate
|
|
database = apolloSetup.database
|
|
server = apolloSetup.server
|
|
})
|
|
|
|
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
|
|
let chatPartner
|
|
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: CreateRoom,
|
|
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 {
|
|
// 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)
|
|
})
|
|
|
|
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 {
|
|
// 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'),
|
|
)
|
|
})
|
|
})
|
|
})
|