diff --git a/src/modules/gateway/gateway.controller.spec.ts b/src/modules/gateway/gateway.controller.spec.ts index bdfd18ee..66396053 100644 --- a/src/modules/gateway/gateway.controller.spec.ts +++ b/src/modules/gateway/gateway.controller.spec.ts @@ -82,6 +82,49 @@ describe('Gateway Controller', () => { }); }); + describe('POST /workspaces/storage/precheck', () => { + const updateWorkspaceStorageDto: UpdateWorkspaceStorageDto = { + ownerId: v4(), + maxSpaceBytes: 1000000, + numberOfSeats: 5, + }; + + it('When owner passed is not found, then it should throw.', async () => { + jest + .spyOn(gatewayUsecases, 'precheckUpdateWorkspaceStorage') + .mockRejectedValueOnce(new BadRequestException()); + await expect( + gatewayController.precheckUpdateWorkspaceStorage( + updateWorkspaceStorageDto, + ), + ).rejects.toThrow(BadRequestException); + }); + + it('When workspace is not found, then it should throw.', async () => { + jest + .spyOn(gatewayUsecases, 'precheckUpdateWorkspaceStorage') + .mockRejectedValueOnce(new NotFoundException()); + await expect( + gatewayController.precheckUpdateWorkspaceStorage( + updateWorkspaceStorageDto, + ), + ).rejects.toThrow(NotFoundException); + }); + + it('When correct data is passed and workspace completed is found, then it works.', async () => { + await gatewayController.precheckUpdateWorkspaceStorage( + updateWorkspaceStorageDto, + ); + expect( + gatewayUsecases.precheckUpdateWorkspaceStorage, + ).toHaveBeenCalledWith( + updateWorkspaceStorageDto.ownerId, + updateWorkspaceStorageDto.maxSpaceBytes, + updateWorkspaceStorageDto.numberOfSeats, + ); + }); + }); + describe('DELETE /workspaces', () => { const deleteWorkspaceDto: DeleteWorkspaceDto = { ownerId: v4(), diff --git a/src/modules/gateway/gateway.controller.ts b/src/modules/gateway/gateway.controller.ts index 9fe096bf..366e6953 100644 --- a/src/modules/gateway/gateway.controller.ts +++ b/src/modules/gateway/gateway.controller.ts @@ -59,6 +59,23 @@ export class GatewayController { ); } + @Post('/workspaces/storage/precheck') + @ApiOperation({ + summary: 'Precheck for updating a workspace', + }) + @ApiBearerAuth('gateway') + @UseGuards(GatewayGuard) + @ApiOkResponse({ description: 'Returns whether the update is possible' }) + async precheckUpdateWorkspaceStorage( + @Body() updateWorkspaceStorageDto: UpdateWorkspaceStorageDto, + ) { + return this.gatewayUseCases.precheckUpdateWorkspaceStorage( + updateWorkspaceStorageDto.ownerId, + updateWorkspaceStorageDto.maxSpaceBytes, + updateWorkspaceStorageDto.numberOfSeats, + ); + } + @Delete('/workspaces') @ApiOperation({ summary: 'Destroy a workspace', diff --git a/src/modules/gateway/gateway.usecase.spec.ts b/src/modules/gateway/gateway.usecase.spec.ts index 7c8d4785..3de44a0f 100644 --- a/src/modules/gateway/gateway.usecase.spec.ts +++ b/src/modules/gateway/gateway.usecase.spec.ts @@ -196,6 +196,43 @@ describe('GatewayUseCases', () => { }); }); + describe('precheckUpdateWorkspaceStorage', () => { + it('When user is not found, then it should throw', async () => { + jest.spyOn(userRepository, 'findByUuid').mockResolvedValueOnce(null); + await expect( + service.precheckUpdateWorkspaceStorage(v4(), maxSpaceBytes, 4), + ).rejects.toThrow(BadRequestException); + }); + + it('When the workspace is not found, then it should throw', async () => { + jest.spyOn(userRepository, 'findByUuid').mockResolvedValueOnce(owner); + jest.spyOn(workspaceUseCases, 'findOne').mockResolvedValueOnce(null); + + await expect( + service.precheckUpdateWorkspaceStorage(owner.uuid, maxSpaceBytes, 4), + ).rejects.toThrow(NotFoundException); + }); + + it('When owner and workspaces are found, then it should call precheckUpdateWorkspaceLimit', async () => { + const owner = newUser(); + const workspace = newWorkspace({ owner }); + jest.spyOn(userRepository, 'findByUuid').mockResolvedValueOnce(owner); + jest + .spyOn(workspaceUseCases, 'findOne') + .mockResolvedValueOnce(workspace); + + await service.precheckUpdateWorkspaceStorage( + owner.uuid, + maxSpaceBytes, + 4, + ); + + expect( + workspaceUseCases.precheckUpdateWorkspaceLimit, + ).toHaveBeenCalledWith(workspace, maxSpaceBytes, 4); + }); + }); + describe('destroyWorkspace', () => { it('When owner is not found, then it should throw', async () => { jest.spyOn(userRepository, 'findByUuid').mockResolvedValue(null); diff --git a/src/modules/gateway/gateway.usecase.ts b/src/modules/gateway/gateway.usecase.ts index 52ce7849..133b2935 100644 --- a/src/modules/gateway/gateway.usecase.ts +++ b/src/modules/gateway/gateway.usecase.ts @@ -79,6 +79,31 @@ export class GatewayUseCases { } } + async precheckUpdateWorkspaceStorage( + ownerId: string, + maxSpaceBytes: number, + numberOfSeats: number, + ): Promise { + const owner = await this.userRepository.findByUuid(ownerId); + if (!owner) { + throw new BadRequestException(); + } + const workspace = await this.workspaceUseCases.findOne({ + ownerId: owner.uuid, + setupCompleted: true, + }); + + if (!workspace) { + throw new NotFoundException('Workspace not found'); + } + + await this.workspaceUseCases.precheckUpdateWorkspaceLimit( + workspace, + maxSpaceBytes, + workspace.numberOfSeats !== numberOfSeats ? numberOfSeats : undefined, + ); + } + async destroyWorkspace(ownerId: string): Promise { const owner = await this.userRepository.findByUuid(ownerId); if (!owner) { diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 39b6bda3..880a588b 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -1764,6 +1764,134 @@ describe('WorkspacesUsecases', () => { }); }); + describe('calculateWorkspaceLimits', () => { + it('When a new limit is specified, calculate spaceDifference', async () => { + const memberCount = 5; + const workspace = newWorkspace({ + attributes: { + numberOfSeats: memberCount, + }, + }); + const currentSpaceLimit = 10000; + const newSpaceLimit = currentSpaceLimit * 2; + + jest + .spyOn(workspaceRepository, 'getWorkspaceUsersCount') + .mockResolvedValue(memberCount); + jest + .spyOn(service, 'getWorkspaceNetworkLimit') + .mockResolvedValue(currentSpaceLimit); + + const { unusedSpace, spaceDifference } = + await service.calculateWorkspaceLimits(workspace, newSpaceLimit); + + expect(unusedSpace).toBe(0); + + expect(spaceDifference).toBe( + (newSpaceLimit - currentSpaceLimit) / memberCount, + ); + }); + + it('When workspace is not full return unusedSpace diiferent than 0', async () => { + const memberCount = 5; + const numberOfSeats = 7; + const workspace = newWorkspace({ + attributes: { + numberOfSeats, + }, + }); + const spacePerUser = 10000; + const currentSpaceLimit = spacePerUser * numberOfSeats; + const newSpaceLimit = currentSpaceLimit * 2; + + jest + .spyOn(workspaceRepository, 'getWorkspaceUsersCount') + .mockResolvedValue(memberCount); + jest + .spyOn(service, 'getWorkspaceNetworkLimit') + .mockResolvedValue(currentSpaceLimit); + + const { unusedSpace, spaceDifference } = + await service.calculateWorkspaceLimits(workspace, newSpaceLimit); + + expect(unusedSpace).toBe(spacePerUser * (numberOfSeats - memberCount)); + + expect(spaceDifference).toBe(spacePerUser * 2 - spacePerUser); + }); + + it('When a different numberOfSeats is specified, spacedifference should vary', async () => { + const memberCount = 5; + const numberOfSeats = memberCount; + const newNumberOfSeats = numberOfSeats * 2; + const workspace = newWorkspace({ + attributes: { + numberOfSeats, + }, + }); + const spacePerUser = 10000; + const currentSpaceLimit = spacePerUser * numberOfSeats; + const newSpaceLimit = spacePerUser * newNumberOfSeats; + + jest + .spyOn(workspaceRepository, 'getWorkspaceUsersCount') + .mockResolvedValue(memberCount); + jest + .spyOn(service, 'getWorkspaceNetworkLimit') + .mockResolvedValue(currentSpaceLimit); + + const { unusedSpace, spaceDifference } = + await service.calculateWorkspaceLimits( + workspace, + newSpaceLimit, + newNumberOfSeats, + ); + + expect(unusedSpace).toBe(spacePerUser * (newNumberOfSeats - memberCount)); + + expect(spaceDifference).toBe(0); + }); + }); + + describe('precheckUpdateWorkspaceLimit', () => { + it('should throw BadRequestException if unused space is less than owner limit', async () => { + const workspace = newWorkspace(); + const newWorkspaceSpaceLimit = 100; + const newNumberOfSeats = 10; + + jest.spyOn(service, 'getOwnerAvailableSpace').mockResolvedValue(50); + jest + .spyOn(service, 'calculateWorkspaceLimits') + .mockResolvedValue({ unusedSpace: 40, spaceDifference: 0 }); + + await expect( + service.precheckUpdateWorkspaceLimit( + workspace, + newWorkspaceSpaceLimit, + newNumberOfSeats, + ), + ).rejects.toThrow(BadRequestException); + }); + + it('should not throw an exception if unused space is greater than or equal to owner limit', async () => { + const workspace = newWorkspace(); + const newWorkspaceSpaceLimit = 100; + const newNumberOfSeats = 10; + + jest.spyOn(service, 'getOwnerAvailableSpace').mockResolvedValue(50); + jest + .spyOn(service, 'calculateWorkspaceLimits') + .mockResolvedValue({ unusedSpace: 60, spaceDifference: 10 }); + + await expect( + service.precheckUpdateWorkspaceLimit( + workspace, + newWorkspaceSpaceLimit, + newNumberOfSeats, + ), + ).resolves.not.toThrow(); + }); + }); + describe('updateWorkspaceLimit', () => { it('When workspace does not exist, then it should throw', async () => { jest.spyOn(workspaceRepository, 'findById').mockResolvedValue(null); @@ -1805,9 +1933,6 @@ describe('WorkspacesUsecases', () => { jest .spyOn(userRepository, 'findByUuid') .mockResolvedValue(workspaceNetworkUser); - jest - .spyOn(service, 'getWorkspaceNetworkLimit') - .mockResolvedValue(currentSpaceLimit); jest .spyOn(workspaceRepository, 'findWorkspaceUsers') .mockResolvedValue(workspaceUsers); @@ -1822,6 +1947,11 @@ describe('WorkspacesUsecases', () => { currentSpaceLimit - (newSpacePerUser - oldSpacePerUser) * workspaceUsers.length; + jest.spyOn(service, 'calculateWorkspaceLimits').mockResolvedValue({ + unusedSpace, + spaceDifference: newSpacePerUser - oldSpacePerUser, + }); + await service.updateWorkspaceLimit(workspace.id, newSpaceLimit); expect(networkService.setStorage).toHaveBeenCalledWith( @@ -1870,9 +2000,6 @@ describe('WorkspacesUsecases', () => { jest .spyOn(userRepository, 'findByUuid') .mockResolvedValue(workspaceNetworkUser); - jest - .spyOn(service, 'getWorkspaceNetworkLimit') - .mockResolvedValue(currentSpaceLimit); jest .spyOn(workspaceRepository, 'findWorkspaceUsers') .mockResolvedValue(workspaceUsers); @@ -1887,6 +2014,11 @@ describe('WorkspacesUsecases', () => { currentSpaceLimit - (newSpacePerUser - oldSpacePerUser) * workspaceUsers.length; + jest.spyOn(service, 'calculateWorkspaceLimits').mockResolvedValue({ + unusedSpace, + spaceDifference: newSpacePerUser - oldSpacePerUser, + }); + await service.updateWorkspaceLimit( workspace.id, newSpaceLimit, diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 0a68a4b0..55eb2012 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -494,6 +494,51 @@ export class WorkspacesUsecases { }); } + async calculateWorkspaceLimits( + workspace: Workspace, + newWorkspaceSpaceLimit: number, + newNumberOfSeats?: number, + ) { + const currentWorkspaceSpaceLimit = + await this.getWorkspaceNetworkLimit(workspace); + + const currentSpacePerUser = + currentWorkspaceSpaceLimit / workspace.numberOfSeats; + + const newSpacePerUser = + newWorkspaceSpaceLimit / (newNumberOfSeats ?? workspace.numberOfSeats); + const spaceDifference = newSpacePerUser - currentSpacePerUser; + + const memberCount = await this.workspaceRepository.getWorkspaceUsersCount( + workspace.id, + ); + + const unusedSpace = + newWorkspaceSpaceLimit - + currentWorkspaceSpaceLimit - + spaceDifference * memberCount; + + return { unusedSpace, spaceDifference }; + } + + async precheckUpdateWorkspaceLimit( + workspace: Workspace, + newWorkspaceSpaceLimit: number, + newNumberOfSeats?: number, + ) { + const ownerLimit = await this.getOwnerAvailableSpace(workspace); + + const { unusedSpace } = await this.calculateWorkspaceLimits( + workspace, + newWorkspaceSpaceLimit, + newNumberOfSeats, + ); + + if (unusedSpace < ownerLimit) { + throw new BadRequestException('Insufficient space to update workspace'); + } + } + async updateWorkspaceLimit( workspaceId: Workspace['id'], newWorkspaceSpaceLimit: number, @@ -509,15 +554,12 @@ export class WorkspacesUsecases { workspace.workspaceUserId, ); - const currentWorkspaceSpaceLimit = - await this.getWorkspaceNetworkLimit(workspace); - - const currentSpacePerUser = - currentWorkspaceSpaceLimit / workspace.numberOfSeats; - - const newSpacePerUser = - newWorkspaceSpaceLimit / (newNumberOfSeats ?? workspace.numberOfSeats); - const spaceDifference = newSpacePerUser - currentSpacePerUser; + const { unusedSpace, spaceDifference } = + await this.calculateWorkspaceLimits( + workspace, + newWorkspaceSpaceLimit, + newNumberOfSeats, + ); const workspaceUsers = await this.workspaceRepository.findWorkspaceUsers(workspaceId); @@ -531,11 +573,6 @@ export class WorkspacesUsecases { ); } - const unusedSpace = - newWorkspaceSpaceLimit - - currentWorkspaceSpaceLimit - - spaceDifference * workspaceUsers.length; - await this.networkService.setStorage( workspaceNetworkUser.email, newWorkspaceSpaceLimit,