mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
This fixes the tests for #8714. The Images type now has the callbacks only for backwards compatibility with local uploads. Next step is to remove the obsolete code and make S3 configuration parameters required.
This commit is contained in:
parent
32927ea96e
commit
c9b429878a
@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/require-await */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||||
|
import { DeleteObjectCommand } from '@aws-sdk/client-s3'
|
||||||
|
import { Upload } from '@aws-sdk/lib-storage'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
|
|
||||||
import Factory, { cleanDatabase } from '@db/factories'
|
import Factory, { cleanDatabase } from '@db/factories'
|
||||||
@ -17,11 +16,30 @@ import { images } from './imagesS3'
|
|||||||
import type { ImageInput } from './images'
|
import type { ImageInput } from './images'
|
||||||
import type { FileUpload } from 'graphql-upload'
|
import type { FileUpload } from 'graphql-upload'
|
||||||
|
|
||||||
|
jest.mock('@aws-sdk/client-s3', () => {
|
||||||
|
return {
|
||||||
|
S3Client: jest.fn().mockImplementation(() => ({
|
||||||
|
send: jest.fn(),
|
||||||
|
})),
|
||||||
|
ObjectCannedACL: { public_read: 'public_read' },
|
||||||
|
DeleteObjectCommand: jest.fn().mockImplementation(() => ({})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.mock('@aws-sdk/lib-storage', () => {
|
||||||
|
return {
|
||||||
|
Upload: jest.fn().mockImplementation(({ params: { Key } }: { params: { Key: string } }) => ({
|
||||||
|
done: () => Promise.resolve({ Location: `http://your-objectstorage.com/bucket/${Key}` }),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockUpload = jest.mocked(Upload)
|
||||||
|
const mockDeleteObjectCommand = jest.mocked(DeleteObjectCommand)
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
|
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||||
let uploadCallback
|
|
||||||
let deleteCallback
|
|
||||||
|
|
||||||
const config: S3Configured = {
|
const config: S3Configured = {
|
||||||
AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID',
|
AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID',
|
||||||
@ -41,16 +59,10 @@ afterAll(async () => {
|
|||||||
await driver.close()
|
await driver.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
uploadCallback = jest.fn(
|
|
||||||
({ uniqueFilename }) => `http://your-objectstorage.com/bucket/${uniqueFilename}`,
|
|
||||||
)
|
|
||||||
deleteCallback = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleteImage', () => {
|
describe('deleteImage', () => {
|
||||||
@ -73,13 +85,13 @@ describe('deleteImage', () => {
|
|||||||
|
|
||||||
it('deletes `Image` node', async () => {
|
it('deletes `Image` node', async () => {
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||||
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
|
await deleteImage(user, 'AVATAR_IMAGE')
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls deleteCallback', async () => {
|
it('calls deleteCallback', async () => {
|
||||||
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
|
await deleteImage(user, 'AVATAR_IMAGE')
|
||||||
expect(deleteCallback).toHaveBeenCalled()
|
expect(mockDeleteObjectCommand).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given a transaction parameter', () => {
|
describe('given a transaction parameter', () => {
|
||||||
@ -89,7 +101,6 @@ describe('deleteImage', () => {
|
|||||||
try {
|
try {
|
||||||
someString = await session.writeTransaction(async (transaction) => {
|
someString = await session.writeTransaction(async (transaction) => {
|
||||||
await deleteImage(user, 'AVATAR_IMAGE', {
|
await deleteImage(user, 'AVATAR_IMAGE', {
|
||||||
deleteCallback,
|
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
const txResult = await transaction.run('RETURN "Hello" as result')
|
const txResult = await transaction.run('RETURN "Hello" as result')
|
||||||
@ -109,7 +120,6 @@ describe('deleteImage', () => {
|
|||||||
try {
|
try {
|
||||||
await session.writeTransaction(async (transaction) => {
|
await session.writeTransaction(async (transaction) => {
|
||||||
await deleteImage(user, 'AVATAR_IMAGE', {
|
await deleteImage(user, 'AVATAR_IMAGE', {
|
||||||
deleteCallback,
|
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
throw new Error('Ouch!')
|
throw new Error('Ouch!')
|
||||||
@ -169,22 +179,20 @@ describe('mergeImage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns new image', async () => {
|
it('returns new image', async () => {
|
||||||
await expect(
|
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
|
||||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
url: expect.any(String),
|
url: expect.any(String),
|
||||||
alt: 'A description of the new image',
|
alt: 'A description of the new image',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls upload callback', async () => {
|
it('calls upload callback', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
expect(uploadCallback).toHaveBeenCalled()
|
expect(mockUpload).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates `:Image` node', async () => {
|
it('creates `:Image` node', async () => {
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -195,11 +203,9 @@ describe('mergeImage', () => {
|
|||||||
const upload = await imageInput.upload
|
const upload = await imageInput.upload
|
||||||
upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
|
upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
|
||||||
imageInput.upload = Promise.resolve(upload)
|
imageInput.upload = Promise.resolve(upload)
|
||||||
await expect(
|
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
|
||||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
url: expect.stringMatching(
|
url: expect.stringMatching(
|
||||||
new RegExp(`^http://your-objectstorage.com/bucket/${uuid}-foo-bar-avatar.jpg`),
|
new RegExp(`^http://your-objectstorage.com/bucket/original/${uuid}-foo-bar-avatar.jpg`),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -210,26 +216,25 @@ describe('mergeImage', () => {
|
|||||||
S3_PUBLIC_GATEWAY: 'http://s3-public-gateway.com',
|
S3_PUBLIC_GATEWAY: 'http://s3-public-gateway.com',
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line jest/no-disabled-tests
|
it('changes the domain of the URL to a server that could e.g. apply image transformations', async () => {
|
||||||
it.skip('changes the domain of the URL to a server that could e.g. apply image transformations', async () => {
|
|
||||||
if (!imageInput.upload) {
|
if (!imageInput.upload) {
|
||||||
throw new Error('Test imageInput was not setup correctly.')
|
throw new Error('Test imageInput was not setup correctly.')
|
||||||
}
|
}
|
||||||
const upload = await imageInput.upload
|
const upload = await imageInput.upload
|
||||||
upload.filename = '/path/to/file-location/foo-bar-avatar.jpg'
|
upload.filename = '/path/to/file-location/foo-bar-avatar.jpg'
|
||||||
imageInput.upload = Promise.resolve(upload)
|
imageInput.upload = Promise.resolve(upload)
|
||||||
await expect(
|
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
|
||||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
url: expect.stringMatching(
|
url: expect.stringMatching(
|
||||||
new RegExp(`^http://s3-public-gateway.com/bucket/${uuid}-foo-bar-avatar.jpg`),
|
new RegExp(
|
||||||
|
`^http://s3-public-gateway.com/bucket/original/${uuid}-foo-bar-avatar.jpg`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('connects resource with image via given image type', async () => {
|
it('connects resource with image via given image type', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
const result = await neode.cypher(
|
const result = await neode.cypher(
|
||||||
`MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`,
|
`MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`,
|
||||||
{},
|
{},
|
||||||
@ -241,7 +246,7 @@ describe('mergeImage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets metadata', async () => {
|
it('sets metadata', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
const image = await neode.first<typeof Image>('Image', {}, undefined)
|
const image = await neode.first<typeof Image>('Image', {}, undefined)
|
||||||
await expect(image.toJson()).resolves.toMatchObject({
|
await expect(image.toJson()).resolves.toMatchObject({
|
||||||
alt: 'A description of the new image',
|
alt: 'A description of the new image',
|
||||||
@ -256,8 +261,6 @@ describe('mergeImage', () => {
|
|||||||
try {
|
try {
|
||||||
await session.writeTransaction(async (transaction) => {
|
await session.writeTransaction(async (transaction) => {
|
||||||
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
||||||
uploadCallback,
|
|
||||||
deleteCallback,
|
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
return transaction.run(
|
return transaction.run(
|
||||||
@ -287,8 +290,6 @@ describe('mergeImage', () => {
|
|||||||
try {
|
try {
|
||||||
await session.writeTransaction(async (transaction) => {
|
await session.writeTransaction(async (transaction) => {
|
||||||
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
||||||
uploadCallback,
|
|
||||||
deleteCallback,
|
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
return transaction.run('Ooops invalid cypher!', { image })
|
return transaction.run('Ooops invalid cypher!', { image })
|
||||||
@ -314,18 +315,18 @@ describe('mergeImage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls deleteCallback', async () => {
|
it('calls deleteCallback', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
expect(deleteCallback).toHaveBeenCalled()
|
expect(mockDeleteObjectCommand).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls uploadCallback', async () => {
|
it('calls Upload', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
expect(uploadCallback).toHaveBeenCalled()
|
expect(mockUpload).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates metadata of existing image node', async () => {
|
it('updates metadata of existing image node', async () => {
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||||
const image = await neode.first<typeof Image>('Image', {}, undefined)
|
const image = await neode.first<typeof Image>('Image', {}, undefined)
|
||||||
await expect(image.toJson()).resolves.toMatchObject({
|
await expect(image.toJson()).resolves.toMatchObject({
|
||||||
@ -375,17 +376,17 @@ describe('mergeImage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('does not call deleteCallback', async () => {
|
it('does not call deleteCallback', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
expect(deleteCallback).not.toHaveBeenCalled()
|
expect(mockDeleteObjectCommand).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not call uploadCallback', async () => {
|
it('does not call Upload', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
expect(uploadCallback).not.toHaveBeenCalled()
|
expect(mockUpload).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates metadata', async () => {
|
it('updates metadata', async () => {
|
||||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||||
const images = await neode.all('Image')
|
const images = await neode.all('Image')
|
||||||
expect(images).toHaveLength(1)
|
expect(images).toHaveLength(1)
|
||||||
await expect(images.first().toJson()).resolves.toMatchObject({
|
await expect(images.first().toJson()).resolves.toMatchObject({
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { v4 as uuid } from 'uuid'
|
|||||||
|
|
||||||
import type { S3Configured } from '@config/index'
|
import type { S3Configured } from '@config/index'
|
||||||
import { s3Service } from '@src/uploads/s3Service'
|
import { s3Service } from '@src/uploads/s3Service'
|
||||||
import { FileUploadCallback } from '@src/uploads/types'
|
|
||||||
|
|
||||||
import { wrapTransaction } from './wrapTransaction'
|
import { wrapTransaction } from './wrapTransaction'
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ export const images = (config: S3Configured) => {
|
|||||||
const s3 = s3Service(config, 'original')
|
const s3 = s3Service(config, 'original')
|
||||||
|
|
||||||
const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => {
|
const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => {
|
||||||
const { transaction, deleteCallback = s3.deleteFile } = opts
|
const { transaction } = opts
|
||||||
if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts)
|
if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts)
|
||||||
const txResult = await transaction.run(
|
const txResult = await transaction.run(
|
||||||
`
|
`
|
||||||
@ -34,7 +33,7 @@ export const images = (config: S3Configured) => {
|
|||||||
// of an error (so throw an error). If we bulk delete an image, it
|
// of an error (so throw an error). If we bulk delete an image, it
|
||||||
// could very well be that there is no image for the resource.
|
// could very well be that there is no image for the resource.
|
||||||
if (image) {
|
if (image) {
|
||||||
await deleteCallback(image.url)
|
await s3.deleteFile(image.url)
|
||||||
}
|
}
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
@ -47,7 +46,7 @@ export const images = (config: S3Configured) => {
|
|||||||
) => {
|
) => {
|
||||||
if (typeof imageInput === 'undefined') return
|
if (typeof imageInput === 'undefined') return
|
||||||
if (imageInput === null) return deleteImage(resource, relationshipType, opts)
|
if (imageInput === null) return deleteImage(resource, relationshipType, opts)
|
||||||
const { transaction, uploadCallback, deleteCallback = s3.deleteFile } = opts
|
const { transaction } = opts
|
||||||
if (!transaction)
|
if (!transaction)
|
||||||
return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts)
|
return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts)
|
||||||
|
|
||||||
@ -62,9 +61,9 @@ export const images = (config: S3Configured) => {
|
|||||||
const { upload } = imageInput
|
const { upload } = imageInput
|
||||||
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
|
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
|
||||||
if (existingImage && upload) {
|
if (existingImage && upload) {
|
||||||
await deleteCallback(existingImage.url)
|
await s3.deleteFile(existingImage.url)
|
||||||
}
|
}
|
||||||
const url = await uploadImageFile(upload, uploadCallback)
|
const url = await uploadImageFile(upload)
|
||||||
const { alt, sensitive, aspectRatio, type } = imageInput
|
const { alt, sensitive, aspectRatio, type } = imageInput
|
||||||
const image = { alt, sensitive, aspectRatio, url, type }
|
const image = { alt, sensitive, aspectRatio, url, type }
|
||||||
txResult = await transaction.run(
|
txResult = await transaction.run(
|
||||||
@ -82,15 +81,12 @@ export const images = (config: S3Configured) => {
|
|||||||
return mergedImage
|
return mergedImage
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadImageFile = async (
|
const uploadImageFile = async (uploadPromise: Promise<FileUpload> | undefined) => {
|
||||||
uploadPromise: Promise<FileUpload> | undefined,
|
|
||||||
uploadCallback: FileUploadCallback | undefined = s3.uploadFile,
|
|
||||||
) => {
|
|
||||||
if (!uploadPromise) return undefined
|
if (!uploadPromise) return undefined
|
||||||
const upload = await uploadPromise
|
const upload = await uploadPromise
|
||||||
const { name, ext } = path.parse(upload.filename)
|
const { name, ext } = path.parse(upload.filename)
|
||||||
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
||||||
return await uploadCallback({ ...upload, uniqueFilename })
|
return await s3.uploadFile({ ...upload, uniqueFilename })
|
||||||
}
|
}
|
||||||
|
|
||||||
const images: Images = {
|
const images: Images = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user