diff --git a/lib/core/bucketEntries/BucketEntry.ts b/lib/core/bucketEntries/BucketEntry.ts index 547c9fbe..cc08bff8 100644 --- a/lib/core/bucketEntries/BucketEntry.ts +++ b/lib/core/bucketEntries/BucketEntry.ts @@ -8,7 +8,7 @@ export interface BucketEntry { name?: string; // filename is deprecated filename?: string; - index: string; + index?: string; bucket: Bucket['id']; created?: Date; mimetype?: string; diff --git a/lib/core/bucketEntries/usecase.ts b/lib/core/bucketEntries/usecase.ts index 733a3263..f1ec0c93 100644 --- a/lib/core/bucketEntries/usecase.ts +++ b/lib/core/bucketEntries/usecase.ts @@ -11,6 +11,7 @@ import { PointersRepository } from '../pointers/Repository'; import { MirrorsRepository } from '../mirrors/Repository'; import { BucketEntry } from './BucketEntry'; import { UsersRepository } from '../users/Repository'; +import { UserNotFoundError, UserSpaceSnapshot } from '../users'; import { User } from '../users/User'; import { Bucket } from '../buckets/Bucket'; import { FileStateRepository } from '../fileState/Repository'; @@ -181,4 +182,76 @@ export class BucketEntriesUsecase { await this.bucketEntriesRepository.deleteByIds(fileIds); await this.fileStateRepository.deleteByBucketEntryIds(fileIds); } + + private async findBucketOwner( + userUuid: User['uuid'], + bucketId: Bucket['id'] + ): Promise { + const user = await this.usersRepository.findByUuid(userUuid); + + if (!user) { + throw new UserNotFoundError(userUuid); + } + + const bucket = await this.bucketsRepository.findOne({ id: bucketId, userId: userUuid }); + + if (!bucket) { + throw new BucketNotFoundError(); + } + + return user; + } + + async createEntry( + userUuid: User['uuid'], + bucketId: Bucket['id'], + size: number + ): Promise<{ id: BucketEntry['id']; snapshot: UserSpaceSnapshot }> { + const user = await this.findBucketOwner(userUuid, bucketId); + + const entry = await this.bucketEntriesRepository.create({ + bucket: bucketId, + size, + version: 2, + }); + + const totalUsedSpaceBytes = await this.usersRepository.addTotalUsedSpaceBytes(userUuid, size); + + return { + id: entry.id, + snapshot: { + maxSpaceBytes: user.maxSpaceBytes, + totalUsedSpaceBytes, + }, + }; + } + + async removeEntry( + userUuid: User['uuid'], + bucketId: Bucket['id'], + entryId: BucketEntry['id'] + ): Promise { + const user = await this.findBucketOwner(userUuid, bucketId); + + const entry = await this.bucketEntriesRepository.findOne({ id: entryId, bucket: bucketId }); + + if (!entry) { + return { + maxSpaceBytes: user.maxSpaceBytes, + totalUsedSpaceBytes: user.totalUsedSpaceBytes, + }; + } + + await this.removeFilesV2([entry]); + + const totalUsedSpaceBytes = await this.usersRepository.addTotalUsedSpaceBytes( + userUuid, + -(entry.size || 0) + ); + + return { + maxSpaceBytes: user.maxSpaceBytes, + totalUsedSpaceBytes, + }; + } } diff --git a/lib/core/buckets/Repository.ts b/lib/core/buckets/Repository.ts index e24da97d..4dce295f 100644 --- a/lib/core/buckets/Repository.ts +++ b/lib/core/buckets/Repository.ts @@ -9,5 +9,5 @@ export interface BucketsRepository { findUserBucketsFromDate(userId: Bucket['id'], date?: Date, limit?: number): Promise; destroyByUser(userId: Bucket['userId']): Promise; removeAll(where: Partial): Promise; - removeByIdAndUser(bucketId: Bucket['id'], userId: Bucket['userId']): Promise + removeByIdAndUser(bucketId: Bucket['id'], userId: Bucket['userId']): Promise } diff --git a/lib/core/users/MongoDBUsersRepository.ts b/lib/core/users/MongoDBUsersRepository.ts index 7c41e1ae..a2847c87 100644 --- a/lib/core/users/MongoDBUsersRepository.ts +++ b/lib/core/users/MongoDBUsersRepository.ts @@ -100,14 +100,17 @@ export class MongoDBUsersRepository implements UsersRepository { await this.userModel.updateOne({ uuid }, { $set: update }); } - addTotalUsedSpaceBytes( + async addTotalUsedSpaceBytes( uuid: string, totalUsedSpaceBytes: number - ): Promise { - return this.userModel.updateOne( + ): Promise { + const user = await this.userModel.findOneAndUpdate( { uuid }, - { $inc: { totalUsedSpaceBytes } } + { $inc: { totalUsedSpaceBytes } }, + { new: true } ); + + return user?.totalUsedSpaceBytes ?? 0; } removeById(id: User['id']) { diff --git a/lib/core/users/Repository.ts b/lib/core/users/Repository.ts index c36374a2..099fa7c6 100644 --- a/lib/core/users/Repository.ts +++ b/lib/core/users/Repository.ts @@ -7,7 +7,7 @@ export interface UsersRepository { findByEmail(email: User['email']): Promise; findByIds(ids: User['id'][]): Promise; create(data: CreateUserData): Promise; - addTotalUsedSpaceBytes(uuid: User['uuid'], totalUsedSpaceBytes: User['totalUsedSpaceBytes']): Promise; + addTotalUsedSpaceBytes(uuid: User['uuid'], totalUsedSpaceBytes: User['totalUsedSpaceBytes']): Promise; updateById(id: User['id'], update: Partial): Promise; updateByEmail(email: User['email'], update: Partial): Promise; updateByUuid(uuid: User['uuid'], update: Partial): Promise; diff --git a/lib/core/users/usecase.ts b/lib/core/users/usecase.ts index 4ffd06a8..22747574 100644 --- a/lib/core/users/usecase.ts +++ b/lib/core/users/usecase.ts @@ -20,6 +20,11 @@ function isEmailValid(email: string) { export const RESET_PASSWORD_TOKEN_BYTES_LENGTH = 256; export const SHA256_HASH_BYTES_LENGTH = 32; +export interface UserSpaceSnapshot { + maxSpaceBytes: number; + totalUsedSpaceBytes: number; +} + export class UserAlreadyExistsError extends Error { constructor() { super('User already exists'); diff --git a/lib/server/http/gateway/controller.ts b/lib/server/http/gateway/controller.ts index 37b0d088..4d7a83c2 100644 --- a/lib/server/http/gateway/controller.ts +++ b/lib/server/http/gateway/controller.ts @@ -1,7 +1,8 @@ import { Request, Response } from 'express'; import { Logger } from 'winston'; -import { EmailIsAlreadyInUseError, InvalidDataFormatError, UserAlreadyExistsError, UserNotFoundError, UsersUsecase } from '../../../core'; +import { EmailIsAlreadyInUseError, InvalidDataFormatError, UserAlreadyExistsError, UserNotFoundError, UserSpaceSnapshot, UsersUsecase } from '../../../core'; import { BucketEntriesUsecase } from '../../../core/bucketEntries/usecase'; +import { BucketNotFoundError } from '../../../core/buckets/usecase'; import { GatewayUsecase } from '../../../core/gateway/Usecase'; import { EventBus, EventBusEvents, UserStorageChangedPayload } from '../../eventBus'; @@ -16,6 +17,14 @@ type DeleteFilesInBulkResponse = { type CreateBucketBody = { name: string }; type CreateBucketResponse = { id: string; name: string }; +type CreateBucketEntryBody = { size: number }; +type CreateBucketEntryResponse = UserSpaceSnapshot & { id: string }; + +const OBJECT_ID_PATTERN = /^[a-f0-9]{24}$/i; + +const isValidEntrySize = (value: unknown): value is number => + typeof value === 'number' && Number.isSafeInteger(value) && value > 0; + export class HTTPGatewayController { constructor( private gatewayUsecase: GatewayUsecase, @@ -164,4 +173,77 @@ export class HTTPGatewayController { return res.status(500).send({ message: 'Internal server error' }); } } + + async createBucketEntry( + req: Request<{ uuid: string; id: string }, {}, Partial, {}>, + res: Response + ) { + const { uuid, id } = req.params; + const { size } = req.body; + + if (!uuid || !id || !OBJECT_ID_PATTERN.test(id)) { + return res.status(400).send({ message: 'Invalid params' }); + } + + if (!isValidEntrySize(size)) { + return res + .status(400) + .send({ message: 'size must be a positive integer' }); + } + + try { + const { id: entryId, snapshot } = await this.bucketEntriesUsecase.createEntry( + uuid, + id, + size + ); + + return res.status(200).send({ id: entryId, ...snapshot }); + } catch (err) { + if (err instanceof UserNotFoundError || err instanceof BucketNotFoundError) { + return res.status(404).send({ message: err.message }); + } + + this.logger.error( + '[GATEWAY/CREATE_ENTRY] Error creating entry on bucket %s of user %s: %s. %s', + id, + uuid, + (err as Error).message, + (err as Error).stack || 'NO STACK' + ); + + return res.status(500).send({ message: 'Internal server error' }); + } + } + + async deleteBucketEntry( + req: Request<{ uuid: string; id: string; entryId: string }>, + res: Response + ) { + const { uuid, id, entryId } = req.params; + + if (!uuid || !id || !OBJECT_ID_PATTERN.test(id) || !OBJECT_ID_PATTERN.test(entryId)) { + return res.status(400).send({ message: 'Invalid params' }); + } + + try { + const snapshot = await this.bucketEntriesUsecase.removeEntry(uuid, id, entryId); + + return res.status(200).send(snapshot); + } catch (err) { + if (err instanceof UserNotFoundError || err instanceof BucketNotFoundError) { + return res.status(404).send({ message: err.message }); + } + + this.logger.error( + '[GATEWAY/DELETE_ENTRY] Error deleting entry on bucket %s of user %s: %s. %s', + id, + uuid, + (err as Error).message, + (err as Error).stack || 'NO STACK' + ); + + return res.status(500).send({ message: 'Internal server error' }); + } + } } diff --git a/lib/server/http/gateway/index.ts b/lib/server/http/gateway/index.ts index ad9155e3..cf851cb6 100644 --- a/lib/server/http/gateway/index.ts +++ b/lib/server/http/gateway/index.ts @@ -11,6 +11,8 @@ export const createGatewayHTTPRouter = ( router.patch('/users/:uuid', jwtMiddleware, controller.updateUserEmail.bind(controller)); router.put('/storage/users/:uuid', jwtMiddleware, controller.changeStorage.bind(controller)); router.post('/users/:uuid/buckets', jwtMiddleware, controller.createUserBucket.bind(controller)); + router.post('/users/:uuid/buckets/:id/entries', jwtMiddleware, controller.createBucketEntry.bind(controller)); + router.delete('/users/:uuid/buckets/:id/entries/:entryId', jwtMiddleware, controller.deleteBucketEntry.bind(controller)); router.delete('/storage/files', jwtMiddleware, controller.deleteFilesInBulk.bind(controller)); return router; diff --git a/tests/lib/core/bucketentries/usecase.test.ts b/tests/lib/core/bucketentries/usecase.test.ts index c37f3898..6cac4614 100644 --- a/tests/lib/core/bucketentries/usecase.test.ts +++ b/tests/lib/core/bucketentries/usecase.test.ts @@ -19,7 +19,7 @@ import { MongoDBMirrorsRepository } from '../../../../lib/core/mirrors/MongoDBMi import { MongoDBPointersRepository } from '../../../../lib/core/pointers/MongoDBPointersRepository'; import { MongoDBShardsRepository } from '../../../../lib/core/shards/MongoDBShardsRepository'; import { MongoDBBucketEntryShardsRepository } from '../../../../lib/core/bucketEntryShards/MongoDBBucketEntryShardsRepository'; -import { MongoDBUsersRepository } from '../../../../lib/core/users'; +import { MongoDBUsersRepository, UserNotFoundError } from '../../../../lib/core/users'; import { ShardsUsecase } from '../../../../lib/core/shards/usecase'; import fixtures from '../fixtures'; @@ -658,4 +658,99 @@ describe('BucketEntriesUsecase', function () { expect(removeFileStub.calledWith(fileId)).toBeTruthy(); }); }); + + describe('createEntry()', () => { + it('When the user does not exist, then it throws UserNotFoundError', async () => { + stub(usersRepository, 'findByUuid').resolves(null); + const create = stub(bucketEntriesRepository, 'create'); + + try { + await bucketEntriesUsecase.createEntry('unknown-uuid', 'bucket-id', 100); + expect(true).toBeFalsy(); + } catch (err) { + expect(err).toBeInstanceOf(UserNotFoundError); + } + + expect(create.called).toBeFalsy(); + }); + + it('When the bucket does not belong to the user or does not exist, then it throws BucketNotFoundError', async () => { + const user = fixtures.getUser(); + + stub(usersRepository, 'findByUuid').resolves(user); + const findBucket = stub(bucketsRepository, 'findOne').resolves(null); + const create = stub(bucketEntriesRepository, 'create'); + + try { + await bucketEntriesUsecase.createEntry(user.uuid, 'bucket-id', 100); + expect(true).toBeFalsy(); + } catch (err) { + expect(err).toBeInstanceOf(BucketNotFoundError); + } + + expect(findBucket.calledOnceWithExactly({ id: 'bucket-id', userId: user.uuid })).toBeTruthy(); + expect(create.called).toBeFalsy(); + }); + + it('When the entry is created, then it adds the size to the user total and returns the entry id', async () => { + const user = fixtures.getUser({ maxSpaceBytes: 10000, totalUsedSpaceBytes: 4000 }); + const bucket = fixtures.getBucket({ userId: user.uuid }); + const entry = fixtures.getBucketEntry({ bucket: bucket.id, size: 500 }); + + stub(usersRepository, 'findByUuid').resolves(user); + stub(bucketsRepository, 'findOne').resolves(bucket); + const create = stub(bucketEntriesRepository, 'create').resolves(entry); + const addUsage = stub(usersRepository, 'addTotalUsedSpaceBytes').resolves(4500); + + const result = await bucketEntriesUsecase.createEntry(user.uuid, bucket.id, 500); + + expect(create.calledOnceWithExactly( + { bucket: bucket.id, size: 500, version: 2 } + )).toBeTruthy(); + expect(addUsage.calledOnceWithExactly(user.uuid, 500)).toBeTruthy(); + expect(result).toStrictEqual({ + id: entry.id, + snapshot: { maxSpaceBytes: 10000, totalUsedSpaceBytes: 4500 }, + }); + }); + }); + + describe('removeEntry()', () => { + it('When the entry does not exist, then it is a no-op and the snapshot is unchanged', async () => { + const user = fixtures.getUser({ maxSpaceBytes: 10000, totalUsedSpaceBytes: 4000 }); + const bucket = fixtures.getBucket({ userId: user.uuid }); + const entryId = 'aaaaaaaaaaaaaaaaaaaaaaaa'; + + stub(usersRepository, 'findByUuid').resolves(user); + stub(bucketsRepository, 'findOne').resolves(bucket); + const findEntry = stub(bucketEntriesRepository, 'findOne').resolves(null); + const removeFilesV2 = stub(bucketEntriesUsecase, 'removeFilesV2'); + const addUsage = stub(usersRepository, 'addTotalUsedSpaceBytes'); + + const snapshot = await bucketEntriesUsecase.removeEntry(user.uuid, bucket.id, entryId); + + expect(findEntry.calledOnceWithExactly({ id: entryId, bucket: bucket.id })).toBeTruthy(); + expect(removeFilesV2.called).toBeFalsy(); + expect(addUsage.called).toBeFalsy(); + expect(snapshot).toStrictEqual({ maxSpaceBytes: 10000, totalUsedSpaceBytes: 4000 }); + }); + + it('When the entry exists, then it removes it and returns the decremented snapshot', async () => { + const user = fixtures.getUser({ maxSpaceBytes: 10000, totalUsedSpaceBytes: 4000 }); + const bucket = fixtures.getBucket({ userId: user.uuid }); + const entry = fixtures.getBucketEntry({ bucket: bucket.id, size: 500 }); + + stub(usersRepository, 'findByUuid').resolves(user); + stub(bucketsRepository, 'findOne').resolves(bucket); + stub(bucketEntriesRepository, 'findOne').resolves(entry); + const removeFilesV2 = stub(bucketEntriesUsecase, 'removeFilesV2').resolves(); + const addUsage = stub(usersRepository, 'addTotalUsedSpaceBytes').resolves(3500); + + const snapshot = await bucketEntriesUsecase.removeEntry(user.uuid, bucket.id, entry.id); + + expect(removeFilesV2.calledOnceWithExactly([entry])).toBeTruthy(); + expect(addUsage.calledOnceWithExactly(user.uuid, -500)).toBeTruthy(); + expect(snapshot).toStrictEqual({ maxSpaceBytes: 10000, totalUsedSpaceBytes: 3500 }); + }); + }); }); diff --git a/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts b/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts index 6b0c4aa4..c53858af 100644 --- a/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts +++ b/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts @@ -115,6 +115,205 @@ describe('Gateway V2 e2e tests', () => { }) }) + describe('Bucket entries', () => { + const createBucketForUser = async (userUuid: string, jwt: string) => { + const { body } = await testServer + .post(`/v2/gateway/users/${userUuid}/buckets`) + .set('Authorization', `Bearer ${jwt}`) + .send({ name: `mail-account-${crypto.randomUUID()}` }) + + return body as { id: string; name: string } + } + + const randomObjectId = () => crypto.randomBytes(12).toString('hex') + + it('When creating an entry, then it is persisted as a shard-less v2 entry and the user total grows by its size', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const userBefore = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + + const response = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + expect(response.status).toBe(200) + expect(response.body.id).toBeDefined() + + const entryInDatabase = await databaseConnection.models.BucketEntry.findOne({ _id: response.body.id }) + expect(entryInDatabase).not.toBeNull() + expect(entryInDatabase.bucket.toString()).toBe(bucket.id) + expect(entryInDatabase.size).toBe(5000) + expect(entryInDatabase.version).toBe(2) + + const userAfter = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + expect(userAfter.totalUsedSpaceBytes).toBe(userBefore.totalUsedSpaceBytes + 5000) + expect(response.body.maxSpaceBytes).toBe(userAfter.maxSpaceBytes) + expect(response.body.totalUsedSpaceBytes).toBe(userAfter.totalUsedSpaceBytes) + }) + + it('When posting the same object twice, then each call mints a distinct entry and the total grows by the sum (dedup is the caller\'s job)', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const userBefore = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + + const first = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + const second = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + expect(first.status).toBe(200) + expect(second.status).toBe(200) + expect(second.body.id).not.toBe(first.body.id) + + const entries = await databaseConnection.models.BucketEntry.find({ bucket: bucket.id }) + expect(entries.length).toBe(2) + + const userAfter = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + expect(userAfter.totalUsedSpaceBytes).toBe(userBefore.totalUsedSpaceBytes + 10000) + }) + + it('When deleting an entry, then it is removed and the user total shrinks by its size', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const created = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + const userBefore = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + + const response = await testServer + .delete(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries/${created.body.id}`) + .set('Authorization', `Bearer ${jwt}`) + + expect(response.status).toBe(200) + + const entryInDatabase = await databaseConnection.models.BucketEntry.findOne({ _id: created.body.id }) + expect(entryInDatabase).toBeNull() + + const userAfter = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + expect(userAfter.totalUsedSpaceBytes).toBe(userBefore.totalUsedSpaceBytes - 5000) + expect(response.body).toEqual({ + maxSpaceBytes: userAfter.maxSpaceBytes, + totalUsedSpaceBytes: userAfter.totalUsedSpaceBytes, + }) + }) + + it('When deleting an entry that does not exist, then it is a no-op and the user total is unchanged', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const userBefore = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + + const response = await testServer + .delete(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries/${randomObjectId()}`) + .set('Authorization', `Bearer ${jwt}`) + + expect(response.status).toBe(200) + + const userAfter = await databaseConnection.models.User.findOne({ uuid: testUser.uuid }) + expect(userAfter.totalUsedSpaceBytes).toBe(userBefore.totalUsedSpaceBytes) + }) + + it('When deleting with a malformed entry id, then it returns 400', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const response = await testServer + .delete(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries/${encodeURIComponent('1:404')}`) + .set('Authorization', `Bearer ${jwt}`) + + expect(response.status).toBe(400) + }) + + it('When creating an entry on a bucket of another user, then it returns 404 and changes nothing', async () => { + const owner = await createTestUser() + const otherUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(owner.uuid, jwt) + + const response = await testServer + .post(`/v2/gateway/users/${otherUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + expect(response.status).toBe(404) + + const entries = await databaseConnection.models.BucketEntry.find({ bucket: bucket.id }) + expect(entries.length).toBe(0) + }) + + it('When the entry size is invalid, then it returns 400 and creates nothing', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + const bucket = await createBucketForUser(testUser.uuid, jwt) + + const missingSize = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({}) + + const zeroSize = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 0 }) + + const negativeSize = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: -1 }) + + const nonIntegerSize = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${bucket.id}/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 10.5 }) + + expect(missingSize.status).toBe(400) + expect(zeroSize.status).toBe(400) + expect(negativeSize.status).toBe(400) + expect(nonIntegerSize.status).toBe(400) + + const entries = await databaseConnection.models.BucketEntry.find({ bucket: bucket.id }) + expect(entries.length).toBe(0) + }) + + it('When the bucket id is malformed, then it returns 400', async () => { + const testUser = await createTestUser() + const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET) + + const response = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/not-an-object-id/entries`) + .set('Authorization', `Bearer ${jwt}`) + .send({ size: 5000 }) + + expect(response.status).toBe(400) + }) + + it('When no auth token is provided, then it returns 401', async () => { + const testUser = await createTestUser() + + const response = await testServer + .post(`/v2/gateway/users/${testUser.uuid}/buckets/${'a'.repeat(24)}/entries`) + .send({ size: 1000 }) + + expect(response.status).toBe(401) + }) + }) + describe('Deleting user files', () => { let axiosGetStub: sinon.SinonStub const FAKE_UPLOAD_URL = 'http://fake-upload-url'