Skip to content

Commit

Permalink
feat: added get shared-with endpoint for workspaces
Browse files Browse the repository at this point in the history
  • Loading branch information
apsantiso committed Oct 2, 2024
1 parent 3ee766f commit 9b38c86
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 29 deletions.
30 changes: 8 additions & 22 deletions src/modules/sharing/sharing.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Headers,
Patch,
UseFilters,
InternalServerErrorException,
} from '@nestjs/common';
import { Response } from 'express';
import {
Expand Down Expand Up @@ -919,39 +920,24 @@ export class SharingController {
})
async getItemsSharedsWith(
@UserDecorator() user: User,
@Query('limit') limit = 0,
@Query('offset') offset = 50,
@Param('itemId') itemId: Sharing['itemId'],
@Param('itemType') itemType: Sharing['itemType'],
@Res({ passthrough: true }) res: Response,
): Promise<{ users: Array<any> } | { error: string }> {
): Promise<{ users: Array<any> }> {
try {
const users = await this.sharingService.getItemSharedWith(
user,
itemId,
itemType,
offset,
limit,
);

return { users };
} catch (error) {
let errorMessage = error.message;

if (error instanceof InvalidSharedFolderError) {
res.status(HttpStatus.BAD_REQUEST);
} else if (error instanceof UserNotInvitedError) {
res.status(HttpStatus.FORBIDDEN);
} else {
Logger.error(
`[SHARING/GETSHAREDWITHME] Error while getting shared with by folder id ${
user.uuid
}, ${error.stack || 'No stack trace'}`,
);
res.status(HttpStatus.INTERNAL_SERVER_ERROR);
errorMessage = 'Internal server error';
}
return { error: errorMessage };
Logger.error(
`[SHARING/GETSHAREDWITHME] Error while getting shared with by folder id ${
user.uuid
}, ${error.stack || 'No stack trace'}`,
);
throw error;
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/modules/sharing/sharing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class PasswordNeededError extends ForbiddenException {
}
}

type SharingInfo = Pick<
export type SharingInfo = Pick<
User,
'name' | 'lastname' | 'uuid' | 'avatar' | 'email'
> & {
Expand Down Expand Up @@ -1888,12 +1888,14 @@ export class SharingService {
};
}

async findSharingsWithRolesByItem(item: File | Folder) {
return this.sharingRepository.findSharingsWithRolesByItem(item);
}

async getItemSharedWith(
user: User,
itemId: Sharing['itemId'],
itemType: Sharing['itemType'],
offset: number,
limit: number,
): Promise<SharingInfo[]> {
let item: Item;

Expand All @@ -1909,8 +1911,7 @@ export class SharingService {
throw new NotFoundException('Item not found');
}

const sharingsWithRoles =
await this.sharingRepository.findSharingsWithRolesByItem(item);
const sharingsWithRoles = await this.findSharingsWithRolesByItem(item);

if (sharingsWithRoles.length === 0) {
throw new BadRequestException('This item is not being shared');
Expand Down
21 changes: 21 additions & 0 deletions src/modules/workspaces/dto/shared-with.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { WorkspaceItemUser } from '../domains/workspace-item-user.domain';
import { WorkspaceItemType } from '../attributes/workspace-items-users.attributes';

export class GetSharedWithDto {
@ApiProperty({
example: 'uuid',
description: 'The uuid of the item to share',
})
@IsNotEmpty()
itemId: WorkspaceItemUser['itemId'];

@ApiProperty({
example: WorkspaceItemType,
description: 'The type of the resource to share',
})
@IsNotEmpty()
@IsEnum(WorkspaceItemType)
itemType: WorkspaceItemUser['itemType'];
}
27 changes: 27 additions & 0 deletions src/modules/workspaces/workspaces.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { ChangeUserAssignedSpaceDto } from './dto/change-user-assigned-space.dto
import { Public } from '../auth/decorators/public.decorator';
import { BasicPaginationDto } from '../../common/dto/basic-pagination.dto';
import { GetSharedItemsDto } from './dto/get-shared-items.dto';
import { GetSharedWithDto } from './dto/shared-with.dto';

@ApiTags('Workspaces')
@Controller('workspaces')
Expand Down Expand Up @@ -693,6 +694,32 @@ export class WorkspacesController {
);
}

@Get('/:workspaceId/shared/:itemType/:itemId/shared-with')
@ApiOperation({
summary: 'Get users and teams an item is shared with',
})
@ApiBearerAuth()
@ApiParam({ name: 'workspaceId', type: String, required: true })
@ApiParam({ name: 'itemType', type: String, required: true })
@ApiParam({ name: 'itemId', type: String, required: true })
@UseGuards(WorkspaceGuard)
@WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.MEMBER)
async getItemSharedWith(
@Param('workspaceId', ValidateUUIDPipe)
workspaceId: WorkspaceAttributes['id'],
@UserDecorator() user: User,
@Param() sharedWithParams: GetSharedWithDto,
) {
const { itemId, itemType } = sharedWithParams;

return this.workspaceUseCases.getItemSharedWith(
user,
workspaceId,
itemId,
itemType,
);
}

@Post('/:workspaceId/folders')
@ApiOperation({
summary: 'Create folder',
Expand Down
192 changes: 191 additions & 1 deletion src/modules/workspaces/workspaces.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
newFile,
newFolder,
newPreCreatedUser,
newRole,
newSharing,
newSharingRole,
newUser,
Expand Down Expand Up @@ -46,7 +47,7 @@ import {
generateTokenWithPlainSecret,
verifyWithDefaultSecret,
} from '../../lib/jwt';
import { Role } from '../sharing/sharing.domain';
import { Role, SharedWithType } from '../sharing/sharing.domain';
import { WorkspaceAttributes } from './attributes/workspace.attributes';
import * as jwtUtils from '../../lib/jwt';
import { PaymentsService } from '../../externals/payments/payments.service';
Expand Down Expand Up @@ -4947,4 +4948,193 @@ describe('WorkspacesUsecases', () => {
});
});
});

describe('getItemSharedWith', () => {
const user = newUser();
const workspace = newWorkspace({ owner: user });
const itemId = v4();
const itemType = WorkspaceItemType.File;

it('When item is not found in workspace, then it should throw', async () => {
jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(null);
jest.spyOn(workspaceRepository, 'getItemBy').mockResolvedValueOnce(null);

await expect(
service.getItemSharedWith(user, workspace.id, itemId, itemType),
).rejects.toThrow(NotFoundException);
});

it('When item is not being shared, then it should throw', async () => {
const mockFile = newFile({ owner: user });
const mockWorkspaceFile = newWorkspaceItemUser({
itemId: mockFile.uuid,
itemType: WorkspaceItemType.File,
});
jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(mockFile);
jest
.spyOn(workspaceRepository, 'getItemBy')
.mockResolvedValueOnce(mockWorkspaceFile);
jest
.spyOn(sharingUseCases, 'findSharingsWithRolesByItem')
.mockResolvedValueOnce([]);

await expect(
service.getItemSharedWith(user, workspace.id, itemId, itemType),
).rejects.toThrow(BadRequestException);
});

it('When user is not the owner, invited or part of shared team, then it should throw', async () => {
const mockFile = newFile({ owner: newUser() });
const mockWorkspaceFile = newWorkspaceItemUser({
itemId: mockFile.uuid,
itemType: WorkspaceItemType.File,
});
const mockRole = newRole();
const mockSharing = newSharing({
item: mockFile,
});

jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(mockFile);
jest
.spyOn(workspaceRepository, 'getItemBy')
.mockResolvedValueOnce(mockWorkspaceFile);
jest
.spyOn(sharingUseCases, 'findSharingsWithRolesByItem')
.mockResolvedValueOnce([{ ...mockSharing, role: mockRole }]);
jest
.spyOn(service, 'getWorkspaceTeamsUserBelongsTo')
.mockResolvedValueOnce([]);

await expect(
service.getItemSharedWith(user, workspace.id, itemId, itemType),
).rejects.toThrow(ForbiddenException);
});

it('When user is owner, then it should return shared info', async () => {
const file = newFile();
const workspaceFile = newWorkspaceItemUser({
itemId: file.uuid,
itemType: WorkspaceItemType.File,
attributes: { createdBy: user.uuid },
});
const role = newRole();
const sharing = newSharing({
item: file,
});

jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file);
jest
.spyOn(workspaceRepository, 'getItemBy')
.mockResolvedValueOnce(workspaceFile);
jest
.spyOn(sharingUseCases, 'findSharingsWithRolesByItem')
.mockResolvedValueOnce([{ ...sharing, role }]);
jest
.spyOn(service, 'getWorkspaceTeamsUserBelongsTo')
.mockResolvedValueOnce([]);
jest.spyOn(userUsecases, 'findByUuids').mockResolvedValueOnce([user]);
jest
.spyOn(userUsecases, 'getAvatarUrl')
.mockResolvedValueOnce('avatar-url');

const result = await service.getItemSharedWith(
user,
workspace.id,
itemId,
itemType,
);

expect(result.usersWithRoles[1]).toMatchObject({
uuid: user.uuid,
role: {
id: 'NONE',
name: 'OWNER',
createdAt: file.createdAt,
updatedAt: file.createdAt,
},
});
});

it('When user is invited, then it should return shared info', async () => {
const invitedUser = newUser();
const file = newFile();
const workspaceFile = newWorkspaceItemUser({
itemId: file.uuid,
itemType: WorkspaceItemType.File,
});
const role = newRole();
const sharing = newSharing({
item: file,
});
sharing.sharedWith = invitedUser.uuid;

jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file);
jest
.spyOn(workspaceRepository, 'getItemBy')
.mockResolvedValueOnce(workspaceFile);
jest
.spyOn(sharingUseCases, 'findSharingsWithRolesByItem')
.mockResolvedValueOnce([{ ...sharing, role }]);
jest
.spyOn(service, 'getWorkspaceTeamsUserBelongsTo')
.mockResolvedValueOnce([]);
jest
.spyOn(userUsecases, 'findByUuids')
.mockResolvedValueOnce([invitedUser, user]);
jest
.spyOn(userUsecases, 'getAvatarUrl')
.mockResolvedValueOnce('avatar-url');

const result = await service.getItemSharedWith(
invitedUser,
workspace.id,
itemId,
itemType,
);

expect(result.usersWithRoles[0]).toMatchObject({
sharingId: sharing.id,
role: role,
});
});

it('When user belongs to a shared team, then it should return shared info', async () => {
const userAsignedToTeam = newUser();
const invitedTeam = newWorkspaceTeam();
const file = newFile();
const workspaceFile = newWorkspaceItemUser({
itemId: file.uuid,
itemType: WorkspaceItemType.File,
});
const role = newRole();
const sharing = newSharing({
item: file,
});
sharing.sharedWith = invitedTeam.id;
sharing.sharedWithType = SharedWithType.WorkspaceTeam;

jest.spyOn(fileUseCases, 'getByUuid').mockResolvedValueOnce(file);
jest
.spyOn(workspaceRepository, 'getItemBy')
.mockResolvedValueOnce(workspaceFile);
jest
.spyOn(sharingUseCases, 'findSharingsWithRolesByItem')
.mockResolvedValueOnce([{ ...sharing, role }]);
jest
.spyOn(service, 'getWorkspaceTeamsUserBelongsTo')
.mockResolvedValueOnce([invitedTeam]);

const result = await service.getItemSharedWith(
userAsignedToTeam,
workspace.id,
itemId,
itemType,
);

expect(result.teamsWithRoles[0]).toMatchObject({
sharingId: sharing.id,
role: role,
});
});
});
});
Loading

0 comments on commit 9b38c86

Please sign in to comment.