From 8878c541fdf8edb462f3dd302733fb293e89b1b4 Mon Sep 17 00:00:00 2001 From: Lenny Date: Wed, 27 May 2026 19:17:40 -0400 Subject: [PATCH] fix: include metadata in lifecycle logs for deleteObject --- src/storage/object-delete.test.ts | 2 +- src/storage/object.ts | 8 +- src/test/s3-protocol.test.ts | 518 ++++++++++++++++++++---------- src/test/webhooks.test.ts | 159 +++++++++ 4 files changed, 522 insertions(+), 165 deletions(-) diff --git a/src/storage/object-delete.test.ts b/src/storage/object-delete.test.ts index b0acec7c5..d4a60ba28 100644 --- a/src/storage/object-delete.test.ts +++ b/src/storage/object-delete.test.ts @@ -59,7 +59,7 @@ describe('ObjectStorage.deleteObject', () => { message: 'Access denied', }) - expect(findObject).toHaveBeenCalledWith('bucket', 'private/file.txt', 'id,version', { + expect(findObject).toHaveBeenCalledWith('bucket', 'private/file.txt', 'id,version,metadata', { forUpdate: true, }) expect(deleteObject).toHaveBeenCalledWith('bucket', 'private/file.txt') diff --git a/src/storage/object.ts b/src/storage/object.ts index d06183d84..5248b709f 100644 --- a/src/storage/object.ts +++ b/src/storage/object.ts @@ -127,9 +127,11 @@ export class ObjectStorage { */ async deleteObject(objectName: string) { const obj = await this.db.withTransaction(async (db) => { - const obj = await db.asSuperUser().findObject(this.bucketId, objectName, 'id,version', { - forUpdate: true, - }) + const obj = await db + .asSuperUser() + .findObject(this.bucketId, objectName, 'id,version,metadata', { + forUpdate: true, + }) const deleted = await db.deleteObject(this.bucketId, objectName) diff --git a/src/test/s3-protocol.test.ts b/src/test/s3-protocol.test.ts index 783420afb..efd1e49c6 100644 --- a/src/test/s3-protocol.test.ts +++ b/src/test/s3-protocol.test.ts @@ -37,10 +37,25 @@ import { StorageKnexDB } from '@storage/database' import { Uploader } from '@storage/uploader' import { createHash, createHmac, randomUUID } from 'crypto' import { FastifyInstance } from 'fastify' +import { vi } from 'vitest' import app from '../app' import { getConfig, mergeConfig } from '../config' +import type { ObjectMetadata } from '../storage/backend' +import { ObjectCreatedCopyEvent, ObjectCreatedPostEvent, ObjectRemoved } from '../storage/events' +import type { ObjectRemovedEvent } from '../storage/events/lifecycle/object-removed' import { EMPTY_SHA256_HASH, SignatureV4, SignatureV4Service } from '../storage/protocols/s3' +interface ObjectCreatedEvent { + name: string + version: string + bucketId: string + metadata: ObjectMetadata + uploadType: 'standard' | 'resumable' | 's3' + tenant: { ref: string; host?: string } + reqId: string + sbReqId?: string +} + const { anonKeyAsync, s3ProtocolAccessKeySecret, @@ -917,28 +932,51 @@ describe('S3 Protocol', () => { describe('MultiPart Form Data Upload', () => { it('can upload using multipart/form-data', async () => { - const bucketName = await createBucket(client) - const signedURL = await createPresignedPost(client, { - Bucket: bucketName, - Key: 'test.jpg', - Expires: 5000, - Fields: { - 'Content-Type': 'image/jpg', - 'X-Amz-Meta-Custom': 'meta-field', - }, - }) + const webhookSpy = vi + .spyOn(ObjectCreatedPostEvent, 'sendWebhook') + .mockResolvedValue(undefined) - const formData = new FormData() - Object.keys(signedURL.fields).forEach((key) => { - formData.set(key, signedURL.fields[key]) - }) + try { + const bucketName = await createBucket(client) + const signedURL = await createPresignedPost(client, { + Bucket: bucketName, + Key: 'test.jpg', + Expires: 5000, + Fields: { + 'Content-Type': 'image/jpg', + 'X-Amz-Meta-Custom': 'meta-field', + }, + }) - const data = Buffer.alloc(1024) - formData.set('file', new Blob([data]), 'test.jpg') + const formData = new FormData() + Object.keys(signedURL.fields).forEach((key) => { + formData.set(key, signedURL.fields[key]) + }) - const resp = await fetch(signedURL.url, { method: 'POST', body: formData }) + const data = Buffer.alloc(1024) + formData.set('file', new Blob([data]), 'test.jpg') + + const resp = await fetch(signedURL.url, { method: 'POST', body: formData }) - expect(resp.status).toBe(200) + expect(resp.status).toBe(200) + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as ObjectCreatedEvent + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + uploadType: 's3', + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('prevent uploading files larger than the maxFileSize limit', async () => { @@ -1023,45 +1061,68 @@ describe('S3 Protocol', () => { }) it('completes a multipart upload', async () => { - const bucketName = await createBucket(client) - const createMultiPartUpload = new CreateMultipartUploadCommand({ - Bucket: bucketName, - Key: 'test-1.jpg', - ContentType: 'image/jpg', - CacheControl: 'max-age=2000', - }) - const resp = await client.send(createMultiPartUpload) - expect(resp.UploadId).toBeTruthy() + const webhookSpy = vi + .spyOn(ObjectCreatedPostEvent, 'sendWebhook') + .mockResolvedValue(undefined) - const data = Buffer.alloc(1024 * 5) - const uploadPart = new UploadPartCommand({ - Bucket: bucketName, - Key: 'test-1.jpg', - ContentLength: data.length, - UploadId: resp.UploadId, - Body: data, - PartNumber: 1, - }) + try { + const bucketName = await createBucket(client) + const createMultiPartUpload = new CreateMultipartUploadCommand({ + Bucket: bucketName, + Key: 'test-1.jpg', + ContentType: 'image/jpg', + CacheControl: 'max-age=2000', + }) + const resp = await client.send(createMultiPartUpload) + expect(resp.UploadId).toBeTruthy() - const part1 = await client.send(uploadPart) + const data = Buffer.alloc(1024 * 5) + const uploadPart = new UploadPartCommand({ + Bucket: bucketName, + Key: 'test-1.jpg', + ContentLength: data.length, + UploadId: resp.UploadId, + Body: data, + PartNumber: 1, + }) - const completeMultiPartUpload = new CompleteMultipartUploadCommand({ - Bucket: bucketName, - Key: 'test-1.jpg', - UploadId: resp.UploadId, - MultipartUpload: { - Parts: [ - { - PartNumber: 1, - ETag: part1.ETag, - }, - ], - }, - }) + const part1 = await client.send(uploadPart) - const completeResp = await client.send(completeMultiPartUpload) - expect(completeResp.$metadata.httpStatusCode).toBe(200) - expect(completeResp.Key).toEqual('test-1.jpg') + const completeMultiPartUpload = new CompleteMultipartUploadCommand({ + Bucket: bucketName, + Key: 'test-1.jpg', + UploadId: resp.UploadId, + MultipartUpload: { + Parts: [ + { + PartNumber: 1, + ETag: part1.ETag, + }, + ], + }, + }) + + const completeResp = await client.send(completeMultiPartUpload) + expect(completeResp.$metadata.httpStatusCode).toBe(200) + expect(completeResp.Key).toEqual('test-1.jpg') + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as ObjectCreatedEvent + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test-1.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + uploadType: 's3', + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('does not complete multipart upload on malformed xml body', async () => { @@ -1341,16 +1402,39 @@ describe('S3 Protocol', () => { }) it('upload a file using putObject', async () => { - const bucketName = await createBucket(client) + const webhookSpy = vi + .spyOn(ObjectCreatedPostEvent, 'sendWebhook') + .mockResolvedValue(undefined) - const putObject = new PutObjectCommand({ - Bucket: bucketName, - Key: 'test-1-put-object.jpg', - Body: Buffer.alloc(1024 * 12), - }) + try { + const bucketName = await createBucket(client) - const resp = await client.send(putObject) - expect(resp.$metadata.httpStatusCode).toEqual(200) + const putObject = new PutObjectCommand({ + Bucket: bucketName, + Key: 'test-1-put-object.jpg', + Body: Buffer.alloc(1024 * 12), + }) + + const resp = await client.send(putObject) + expect(resp.$metadata.httpStatusCode).toEqual(200) + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as ObjectCreatedEvent + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test-1-put-object.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + uploadType: 's3', + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('upload a broken JSON body using putObject ', async () => { @@ -1600,21 +1684,44 @@ describe('S3 Protocol', () => { }) it('upload a file using multipart upload', async () => { - const bucketName = await createBucket(client) + const webhookSpy = vi + .spyOn(ObjectCreatedPostEvent, 'sendWebhook') + .mockResolvedValue(undefined) - const uploader = new Upload({ - client, - params: { - Bucket: bucketName, - Key: 'test-1.jpg', - ContentType: 'image/jpg', - Body: Buffer.alloc(1024 * 12), - }, - }) + try { + const bucketName = await createBucket(client) - const resp = await uploader.done() + const uploader = new Upload({ + client, + params: { + Bucket: bucketName, + Key: 'test-1.jpg', + ContentType: 'image/jpg', + Body: Buffer.alloc(1024 * 12), + }, + }) - expect(resp.$metadata).toBeTruthy() + const resp = await uploader.done() + + expect(resp.$metadata).toBeTruthy() + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as ObjectCreatedEvent + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test-1.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + uploadType: 's3', + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('does not mutate in_progress_size when canUpload (RLS) fails', async () => { @@ -1746,27 +1853,47 @@ describe('S3 Protocol', () => { describe('DeleteObjectCommand', () => { it('can delete an existing object', async () => { - const bucketName = await createBucket(client) - const key = 'test-1.jpg' - await uploadFile(client, bucketName, key, 1) + const webhookSpy = vi.spyOn(ObjectRemoved, 'sendWebhook').mockResolvedValue(undefined) - const deleteObject = new DeleteObjectCommand({ - Bucket: bucketName, - Key: key, - }) + try { + const bucketName = await createBucket(client) + const key = 'test-1.jpg' + await uploadFile(client, bucketName, key, 1) - const deleteResp = await client.send(deleteObject) - expect(deleteResp.$metadata.httpStatusCode).toEqual(204) + const deleteObject = new DeleteObjectCommand({ + Bucket: bucketName, + Key: key, + }) - const getObject = new GetObjectCommand({ - Bucket: bucketName, - Key: key, - }) + const deleteResp = await client.send(deleteObject) + expect(deleteResp.$metadata.httpStatusCode).toEqual(204) - try { - await client.send(getObject) - } catch (e) { - expect((e as S3ServiceException).$metadata.httpStatusCode).toEqual(404) + const getObject = new GetObjectCommand({ + Bucket: bucketName, + Key: key, + }) + + try { + await client.send(getObject) + } catch (e) { + expect((e as S3ServiceException).$metadata.httpStatusCode).toEqual(404) + } + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as Omit + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: key, + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() } }) @@ -1801,81 +1928,127 @@ describe('S3 Protocol', () => { describe('DeleteObjectsCommand', () => { it('can delete a single object', async () => { - const bucketName = await createBucket(client) - await Promise.all([uploadFile(client, bucketName, 'test-1.jpg', 1)]) + const webhookSpy = vi.spyOn(ObjectRemoved, 'sendWebhook').mockResolvedValue(undefined) - const deleteObjectsCommand = new DeleteObjectsCommand({ - Bucket: bucketName, - Delete: { - Objects: [ - { - Key: 'test-1.jpg', - }, - ], - }, - }) + try { + const bucketName = await createBucket(client) + await Promise.all([uploadFile(client, bucketName, 'test-1.jpg', 1)]) - const deleteResp = await client.send(deleteObjectsCommand) + const deleteObjectsCommand = new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: [ + { + Key: 'test-1.jpg', + }, + ], + }, + }) - expect(deleteResp.Deleted).toEqual([ - { - Key: 'test-1.jpg', - }, - ]) + const deleteResp = await client.send(deleteObjectsCommand) - const listObjectsCommand = new ListObjectsV2Command({ - Bucket: bucketName, - }) + expect(deleteResp.Deleted).toEqual([ + { + Key: 'test-1.jpg', + }, + ]) - const resp = await client.send(listObjectsCommand) - expect(resp.Contents).toBe(undefined) + const listObjectsCommand = new ListObjectsV2Command({ + Bucket: bucketName, + }) + + const resp = await client.send(listObjectsCommand) + expect(resp.Contents).toBe(undefined) + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as Omit + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test-1.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('can delete multiple objects', async () => { - const bucketName = await createBucket(client) - await Promise.all([ - uploadFile(client, bucketName, 'test-1.jpg', 1), - uploadFile(client, bucketName, 'test-2.jpg', 1), - uploadFile(client, bucketName, 'test-3.jpg', 1), - ]) + const webhookSpy = vi.spyOn(ObjectRemoved, 'sendWebhook').mockResolvedValue(undefined) - const deleteObjectsCommand = new DeleteObjectsCommand({ - Bucket: bucketName, - Delete: { - Objects: [ - { - Key: 'test-1.jpg', - }, - { - Key: 'test-2.jpg', - }, - { - Key: 'test-3.jpg', - }, - ], - }, - }) + try { + const bucketName = await createBucket(client) + await Promise.all([ + uploadFile(client, bucketName, 'test-1.jpg', 1), + uploadFile(client, bucketName, 'test-2.jpg', 1), + uploadFile(client, bucketName, 'test-3.jpg', 1), + ]) - const deleteResp = await client.send(deleteObjectsCommand) + const deleteObjectsCommand = new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: [ + { + Key: 'test-1.jpg', + }, + { + Key: 'test-2.jpg', + }, + { + Key: 'test-3.jpg', + }, + ], + }, + }) - expect(deleteResp.Deleted).toEqual([ - { - Key: 'test-1.jpg', - }, - { - Key: 'test-2.jpg', - }, - { - Key: 'test-3.jpg', - }, - ]) + const deleteResp = await client.send(deleteObjectsCommand) - const listObjectsCommand = new ListObjectsV2Command({ - Bucket: bucketName, - }) + expect(deleteResp.Deleted).toEqual([ + { + Key: 'test-1.jpg', + }, + { + Key: 'test-2.jpg', + }, + { + Key: 'test-3.jpg', + }, + ]) - const resp = await client.send(listObjectsCommand) - expect(resp.Contents).toBe(undefined) + const listObjectsCommand = new ListObjectsV2Command({ + Bucket: bucketName, + }) + + const resp = await client.send(listObjectsCommand) + expect(resp.Contents).toBe(undefined) + + // Verify webhook was called 3 times (once per object) + expect(webhookSpy).toHaveBeenCalledTimes(3) + const deletedKeys = webhookSpy.mock.calls.map((call) => call[0].name).sort() + expect(deletedKeys).toEqual(['test-1.jpg', 'test-2.jpg', 'test-3.jpg']) + + // Verify all calls have the required fields with metadata + webhookSpy.mock.calls.forEach((call) => { + const webhookCall = call[0] as Omit + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: expect.toBeOneOf(['test-1.jpg', 'test-2.jpg', 'test-3.jpg']), + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + }) + } finally { + webhookSpy.mockRestore() + } }) it('try to delete multiple objects that dont exist', async () => { @@ -2044,17 +2217,40 @@ describe('S3 Protocol', () => { describe('CopyObjectCommand', () => { it('will copy an object in the same bucket', async () => { - const bucketName = await createBucket(client) - await uploadFile(client, bucketName, 'test-copy-1.jpg', 1) + const webhookSpy = vi + .spyOn(ObjectCreatedCopyEvent, 'sendWebhook') + .mockResolvedValue(undefined) - const copyObjectCommand = new CopyObjectCommand({ - Bucket: bucketName, - Key: 'test-copied-2.jpg', - CopySource: `${bucketName}/test-copy-1.jpg`, - }) + try { + const bucketName = await createBucket(client) + await uploadFile(client, bucketName, 'test-copy-1.jpg', 1) - const resp = await client.send(copyObjectCommand) - expect(resp.CopyObjectResult?.ETag).toBeTruthy() + const copyObjectCommand = new CopyObjectCommand({ + Bucket: bucketName, + Key: 'test-copied-2.jpg', + CopySource: `${bucketName}/test-copy-1.jpg`, + }) + + const resp = await client.send(copyObjectCommand) + expect(resp.CopyObjectResult?.ETag).toBeTruthy() + + // Verify webhook was called with correct data + expect(webhookSpy).toHaveBeenCalledTimes(1) + const webhookCall = webhookSpy.mock.calls[0][0] as ObjectCreatedEvent + expect(webhookCall).toMatchObject({ + tenant: expect.objectContaining({ ref: tenantId }), + name: 'test-copied-2.jpg', + version: expect.any(String), + bucketId: bucketName, + reqId: expect.any(String), + metadata: expect.any(Object), + uploadType: 's3', + }) + expect(webhookCall.metadata).toBeDefined() + expect(webhookCall.metadata).toHaveProperty('size') + } finally { + webhookSpy.mockRestore() + } }) it('will copy an object in a different bucket', async () => { diff --git a/src/test/webhooks.test.ts b/src/test/webhooks.test.ts index 84f8a48cc..8023d2c37 100644 --- a/src/test/webhooks.test.ts +++ b/src/test/webhooks.test.ts @@ -517,6 +517,165 @@ describe('Webhooks', () => { }, }) }) + + it('will emit webhook for single object deletion', async () => { + const deleteTestBucketName = 'bucket-delete-object-webhook-test' + const authorization = `Bearer ${await serviceKeyAsync}` + + // Create a dedicated bucket for this test + await appInstance.inject({ + method: 'POST', + url: `/bucket`, + headers: { + authorization, + }, + payload: { + name: deleteTestBucketName, + }, + }) + + // Create a single object + const obj = await createObject(pg, deleteTestBucketName) + + // Delete the object via the deleteObject endpoint + const response = await appInstance.inject({ + method: 'DELETE', + url: `/object/${deleteTestBucketName}/${obj.name}`, + headers: { + authorization, + }, + }) + + expect(response.statusCode).toBe(200) + + // Check ObjectRemoved:Delete webhook was sent + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'webhooks', + options: expect.objectContaining({ + deadLetter: 'webhooks-dead-letter', + expireInSeconds: expect.any(Number), + }), + data: expect.objectContaining({ + $version: 'v1', + event: expect.objectContaining({ + $version: 'v1', + type: 'ObjectRemoved:Delete', + applyTime: expect.any(Number), + payload: expect.objectContaining({ + bucketId: deleteTestBucketName, + name: obj.name, + version: obj.version, + metadata: obj.metadata, + tenant: { + host: undefined, + ref: tenantId, + }, + reqId: expect.any(String), + }), + }), + tenant: { + host: undefined, + ref: tenantId, + }, + }), + }) + ) + + // Clean up: delete the bucket + await appInstance.inject({ + method: 'DELETE', + url: `/bucket/${deleteTestBucketName}`, + headers: { + authorization, + }, + }) + }) + + it('will emit webhooks for multiple object deletions', async () => { + const deleteMultipleTestBucketName = 'bucket-delete-objects-webhook-test' + const authorization = `Bearer ${await serviceKeyAsync}` + + // Create a dedicated bucket for this test + await appInstance.inject({ + method: 'POST', + url: `/bucket`, + headers: { + authorization, + }, + payload: { + name: deleteMultipleTestBucketName, + }, + }) + + // Create multiple objects + const objects = await Promise.all([ + createObject(pg, deleteMultipleTestBucketName), + createObject(pg, deleteMultipleTestBucketName), + createObject(pg, deleteMultipleTestBucketName), + ]) + + // Delete the objects via the deleteObjects endpoint + const response = await appInstance.inject({ + method: 'DELETE', + url: `/object/${deleteMultipleTestBucketName}`, + headers: { + authorization, + 'Content-Type': 'application/json', + }, + payload: { + prefixes: objects.map((obj) => obj.name), + }, + }) + + expect(response.statusCode).toBe(200) + + // Check ObjectRemoved:Delete webhooks were sent for each object + expect(sendSpy).toHaveBeenCalledTimes(objects.length) + objects.forEach((obj) => { + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'webhooks', + options: expect.objectContaining({ + deadLetter: 'webhooks-dead-letter', + expireInSeconds: expect.any(Number), + }), + data: expect.objectContaining({ + $version: 'v1', + event: expect.objectContaining({ + $version: 'v1', + type: 'ObjectRemoved:Delete', + applyTime: expect.any(Number), + payload: expect.objectContaining({ + bucketId: deleteMultipleTestBucketName, + name: obj.name, + version: obj.version, + metadata: obj.metadata, + tenant: { + host: undefined, + ref: tenantId, + }, + reqId: expect.any(String), + }), + }), + tenant: { + host: undefined, + ref: tenantId, + }, + }), + }) + ) + }) + + // Clean up: delete the bucket + await appInstance.inject({ + method: 'DELETE', + url: `/bucket/${deleteMultipleTestBucketName}`, + headers: { + authorization, + }, + }) + }) }) async function createObject(pg: TenantConnection, bucketId: string) {