Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/core/users/usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,33 @@ export class UsersUsecase {

await this.usersRepository.removeById(user.id);
}

async findOrCreateBucket(
uuid: User['uuid'],
name: string
): Promise<{ id: string; name: string }> {
const user = await this.usersRepository.findByUuid(uuid);

if (!user) {
throw new UserNotFoundError(uuid);
}

const existing = await this.bucketsRepository.findOne({ userId: uuid, name });

if (existing) {
return { id: existing.id, name: existing.name };
}

const created = await this.bucketsRepository.create({
name,
user: user.id,
userId: user.uuid,
status: 'Active',
transfer: 0,
storage: 0,
encryptionKey: '',
});

return { id: created.id, name: created.name };
}
}
34 changes: 34 additions & 0 deletions lib/server/http/gateway/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type DeleteFilesInBulkResponse = {
} | string
};

type CreateBucketBody = { name: string };
type CreateBucketResponse = { id: string; name: string };

export class HTTPGatewayController {
constructor(
private gatewayUsecase: GatewayUsecase,
Expand Down Expand Up @@ -130,4 +133,35 @@ export class HTTPGatewayController {
return res.status(500).send({ message: 'Internal server error' });
}
}

async createUserBucket(
req: Request<{ uuid: string }, {}, Partial<CreateBucketBody>, {}>,
res: Response<CreateBucketResponse | { message: string }>
) {
const { uuid } = req.params;
const { name } = req.body;

if (!uuid || typeof name !== 'string' || name.length === 0) {
return res.status(400).send({ message: 'name is required' });
}

try {
const bucket = await this.usersUsecase.findOrCreateBucket(uuid, name);

return res.status(200).send(bucket);
} catch (err) {
if (err instanceof UserNotFoundError) {
return res.status(404).send({ message: err.message });
}

this.logger.error(
'[GATEWAY/CREATE_BUCKET] Error creating bucket for user %s: %s. %s',
uuid,
(err as Error).message,
(err as Error).stack || 'NO STACK'
);

return res.status(500).send({ message: 'Internal server error' });
}
}
}
1 change: 1 addition & 0 deletions lib/server/http/gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const createGatewayHTTPRouter = (
router.post('/users', jwtMiddleware, controller.findOrCreateUser.bind(controller));
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.delete('/storage/files', jwtMiddleware, controller.deleteFilesInBulk.bind(controller));

return router;
Expand Down
57 changes: 57 additions & 0 deletions tests/lib/core/users/usecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,63 @@ describe('Users usecases', () => {
});
});

describe('Finding or creating a bucket', () => {
it('When the user does not exist, then it throws UserNotFoundError', async () => {
const findUser = stub(usersRepository, 'findByUuid').resolves(null);

try {
await usecase.findOrCreateBucket('unknown-uuid', 'bucket-name');
expect(true).toBeFalsy();
} catch (err) {
expect(err).toBeInstanceOf(UserNotFoundError);
}

expect(findUser.calledOnce).toBeTruthy();
});

it('When no bucket with that name and for that user exists, then it creates one', async () => {
const user = fixtures.getUser();
const name = 'mail-account-id';
const createdBucket = { id: 'new-bucket-id', name } as any;

stub(usersRepository, 'findByUuid').resolves(user);
const findBucket = stub(bucketsRepository, 'findOne').resolves(null);
const createBucket = stub(bucketsRepository, 'create').resolves(createdBucket);

const result = await usecase.findOrCreateBucket(user.uuid, name);

expect(findBucket.calledOnce).toBeTruthy();
expect(findBucket.firstCall.args).toStrictEqual([{ userId: user.uuid, name }]);
expect(createBucket.calledOnce).toBeTruthy();
expect(createBucket.firstCall.args).toStrictEqual([{
name,
user: user.id,
userId: user.uuid,
status: 'Active',
transfer: 0,
storage: 0,
encryptionKey: '',
}]);
expect(result).toStrictEqual({ id: createdBucket.id, name: createdBucket.name });
});

it('When a bucket with that name and for that user already exists, then it returns the existing one without creating', async () => {
const user = fixtures.getUser();
const name = 'mail-account-id';
const existingBucket = { id: 'existing-bucket-id', name } as any;

stub(usersRepository, 'findByUuid').resolves(user);
const findBucket = stub(bucketsRepository, 'findOne').resolves(existingBucket);
const createBucket = stub(bucketsRepository, 'create');

const result = await usecase.findOrCreateBucket(user.uuid, name);

expect(findBucket.calledOnce).toBeTruthy();
expect(createBucket.called).toBeFalsy();
expect(result).toStrictEqual({ id: existingBucket.id, name: existingBucket.name });
});
});

describe('Confirming user destruction', () => {
it('When confirming a destruction of a user that exists, then it works', async () => {
const user = fixtures.getUser();
Expand Down
71 changes: 71 additions & 0 deletions tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,77 @@ describe('Gateway V2 e2e tests', () => {
})
})

describe('Creating a user bucket', () => {
it('When creating a bucket for a user, then it is persisted with the user uuid and given name', async () => {
const testUser = await createTestUser()
const bucketName = `mail-account-${crypto.randomUUID()}`

const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET)

const response = await testServer
.post(`/v2/gateway/users/${testUser.uuid}/buckets`)
.set('Authorization', `Bearer ${jwt}`)
.send({ name: bucketName })

expect(response.status).toBe(200)
expect(response.body.name).toBe(bucketName)
expect(response.body.id).toBeDefined()

const bucketInDatabase = await databaseConnection.models.Bucket.findOne({ _id: response.body.id })
expect(bucketInDatabase).not.toBeNull()
expect(bucketInDatabase.userId).toBe(testUser.uuid)
expect(bucketInDatabase.name).toBe(bucketName)
})

it('When creating a bucket with a name that already exists, then it returns the existing bucket', async () => {
const testUser = await createTestUser()
const bucketName = `mail-account-${crypto.randomUUID()}`

const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET)

const firstResponse = await testServer
.post(`/v2/gateway/users/${testUser.uuid}/buckets`)
.set('Authorization', `Bearer ${jwt}`)
.send({ name: bucketName })

const secondResponse = await testServer
.post(`/v2/gateway/users/${testUser.uuid}/buckets`)
.set('Authorization', `Bearer ${jwt}`)
.send({ name: bucketName })

expect(firstResponse.status).toBe(200)
expect(secondResponse.status).toBe(200)
expect(secondResponse.body.id).toBe(firstResponse.body.id)

const buckets = await databaseConnection.models.Bucket.find({ userId: testUser.uuid, name: bucketName })
expect(buckets.length).toBe(1)
})

it('When creating a bucket without a name, 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`)
.set('Authorization', `Bearer ${jwt}`)
.send({})

expect(response.status).toBe(400)
})

it('When creating a bucket for an unknown user, then it returns 404', async () => {
const jwt = signRS256JWT('5m', engine._config.gateway.SIGN_JWT_SECRET)

const response = await testServer
.post(`/v2/gateway/users/${crypto.randomUUID()}/buckets`)
.set('Authorization', `Bearer ${jwt}`)
.send({ name: 'mail-account' })

expect(response.status).toBe(404)
})
})

describe('Deleting user files', () => {
let axiosGetStub: sinon.SinonStub
const FAKE_UPLOAD_URL = 'http://fake-upload-url'
Expand Down
Loading