From 131525b7db237fd66ae0927597083add3224be08 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Fri, 15 Mar 2024 10:55:12 -0400 Subject: [PATCH 1/3] feat: kickoff middleware workspaces --- .../domains/workspace-team-user.domain.ts | 48 ++++++++ .../domains/workspace-team.domain.ts | 5 + .../domains/workspace-user.domain.ts | 57 +++++++++ .../workspaces/domains/workspaces.domain.ts | 5 + .../workspace-required-access.decorator.ts | 29 +++++ .../workspaces/guards/workspaces.guard.ts | 114 ++++++++++++++++++ .../models/workspace-team-users.model.ts | 2 +- .../workspaces/models/workspace.model.ts | 4 + .../repositories/team.repository.ts | 28 +++++ .../repositories/workspaces.repository.ts | 39 +++++- .../workspaces/workspaces.controller.ts | 15 ++- src/modules/workspaces/workspaces.module.ts | 2 + src/modules/workspaces/workspaces.usecase.ts | 12 ++ 13 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 src/modules/workspaces/domains/workspace-team-user.domain.ts create mode 100644 src/modules/workspaces/domains/workspace-user.domain.ts create mode 100644 src/modules/workspaces/guards/workspace-required-access.decorator.ts create mode 100644 src/modules/workspaces/guards/workspaces.guard.ts diff --git a/src/modules/workspaces/domains/workspace-team-user.domain.ts b/src/modules/workspaces/domains/workspace-team-user.domain.ts new file mode 100644 index 000000000..2a77c693b --- /dev/null +++ b/src/modules/workspaces/domains/workspace-team-user.domain.ts @@ -0,0 +1,48 @@ +import { User } from '../../user/user.domain'; +import { WorkspaceTeam } from './workspace-team.domain'; +import { WorkspaceTeamUserAttributes } from '../attributes/workspace-team-users.attributes'; + +export class WorkspaceTeamUser implements WorkspaceTeamUserAttributes { + id: string; + teamId: string; + memberId: string; + team?: WorkspaceTeam; + createdAt: Date; + updatedAt: Date; + + constructor({ + id, + teamId, + memberId, + team, + createdAt, + updatedAt, + }: WorkspaceTeamUserAttributes & { team?: WorkspaceTeam }) { + this.id = id; + this.teamId = teamId; + this.memberId = memberId; + this.team = team; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + static build( + workspaceTeamUser: WorkspaceTeamUserAttributes & { + team?: WorkspaceTeam; + member?: User; + }, + ): WorkspaceTeamUser { + return new WorkspaceTeamUser(workspaceTeamUser); + } + + toJSON() { + return { + id: this.id, + teamId: this.teamId, + memberId: this.memberId, + team: this.team ? this.team.toJSON() : undefined, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/src/modules/workspaces/domains/workspace-team.domain.ts b/src/modules/workspaces/domains/workspace-team.domain.ts index ff90620b4..7b53cea62 100644 --- a/src/modules/workspaces/domains/workspace-team.domain.ts +++ b/src/modules/workspaces/domains/workspace-team.domain.ts @@ -1,3 +1,4 @@ +import { User } from '../../user/user.domain'; import { WorkspaceTeamAttributes } from '../attributes/workspace-team.attributes'; export class WorkspaceTeam implements WorkspaceTeamAttributes { @@ -24,6 +25,10 @@ export class WorkspaceTeam implements WorkspaceTeamAttributes { this.updatedAt = updatedAt; } + isUserManager(userUuid: User['uuid']) { + return userUuid === this.managerId; + } + static build(teamAttributes: WorkspaceTeamAttributes): WorkspaceTeam { return new WorkspaceTeam(teamAttributes); } diff --git a/src/modules/workspaces/domains/workspace-user.domain.ts b/src/modules/workspaces/domains/workspace-user.domain.ts new file mode 100644 index 000000000..d36d4c102 --- /dev/null +++ b/src/modules/workspaces/domains/workspace-user.domain.ts @@ -0,0 +1,57 @@ +import { WorkspaceUserAttributes } from '../attributes/workspace-users.attributes'; + +export class WorkspaceUser implements WorkspaceUserAttributes { + id: string; + memberId: string; + key: string; + workspaceId: string; + spaceLimit: bigint; + driveUsage: bigint; + backupsUsage: bigint; + deactivated: boolean; + createdAt: Date; + updatedAt: Date; + + constructor({ + id, + memberId, + key, + workspaceId, + spaceLimit, + driveUsage, + backupsUsage, + deactivated, + createdAt, + updatedAt, + }: WorkspaceUserAttributes) { + this.id = id; + this.memberId = memberId; + this.key = key; + this.workspaceId = workspaceId; + this.spaceLimit = spaceLimit; + this.driveUsage = driveUsage; + this.backupsUsage = backupsUsage; + this.deactivated = deactivated; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + static build(workspaceUser: WorkspaceUserAttributes): WorkspaceUser { + return new WorkspaceUser(workspaceUser); + } + + toJSON() { + return { + id: this.id, + memberId: this.memberId, + key: this.key, + workspaceId: this.workspaceId, + spaceLimit: this.spaceLimit.toString(), + driveUsage: this.driveUsage.toString(), + backupsUsage: this.backupsUsage.toString(), + deactivated: this.deactivated, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/src/modules/workspaces/domains/workspaces.domain.ts b/src/modules/workspaces/domains/workspaces.domain.ts index 869c45585..82c4d3131 100644 --- a/src/modules/workspaces/domains/workspaces.domain.ts +++ b/src/modules/workspaces/domains/workspaces.domain.ts @@ -1,3 +1,4 @@ +import { UserAttributes } from '../../user/user.attributes'; import { WorkspaceAttributes } from '../attributes/workspace.attributes'; export class Workspace implements WorkspaceAttributes { @@ -40,6 +41,10 @@ export class Workspace implements WorkspaceAttributes { return new Workspace(user); } + isUserOwner(userUuid: UserAttributes['uuid']) { + return userUuid === this.ownerId; + } + toJSON() { return { id: this.id, diff --git a/src/modules/workspaces/guards/workspace-required-access.decorator.ts b/src/modules/workspaces/guards/workspace-required-access.decorator.ts new file mode 100644 index 000000000..28e090e6b --- /dev/null +++ b/src/modules/workspaces/guards/workspace-required-access.decorator.ts @@ -0,0 +1,29 @@ +import { SetMetadata } from '@nestjs/common'; + +export enum WorkspaceRole { + OWNER = 'owner', + MANAGER = 'manager', + MEMBER = 'member', +} + +export enum AccessContext { + WORKSPACE = 'workspace', + TEAM = 'team', +} + +export const WorkspaceContextIdFieldName = { + [AccessContext.WORKSPACE]: 'workspaceId', + [AccessContext.TEAM]: 'teamId', +}; + +export interface AccessOptions { + accessContext: AccessContext; + requiredRole: WorkspaceRole; + idSource: 'params' | 'body' | 'query'; +} + +export const WorkspaceRequiredAccess = ( + accessContext: AccessContext, + requiredRole: WorkspaceRole, + idSource: 'params' | 'body' | 'query' = 'params', +) => SetMetadata('accessControl', { accessContext, requiredRole, idSource }); diff --git a/src/modules/workspaces/guards/workspaces.guard.ts b/src/modules/workspaces/guards/workspaces.guard.ts new file mode 100644 index 000000000..16a88d2e9 --- /dev/null +++ b/src/modules/workspaces/guards/workspaces.guard.ts @@ -0,0 +1,114 @@ +// flexible-access.guard.ts +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { WorkspacesUsecases } from '../workspaces.usecase'; +import { + AccessContext, + AccessOptions, + WorkspaceContextIdFieldName, + WorkspaceRole, +} from './workspace-required-access.decorator'; + +@Injectable() +export class WorkspaceGuard implements CanActivate { + constructor( + private reflector: Reflector, + private workspaceUseCases: WorkspacesUsecases, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const accessOptions: AccessOptions = this.reflector.get( + 'accessControl', + context.getHandler(), + ); + + if (!accessOptions) { + return true; + } + + const { requiredRole, accessContext, idSource } = accessOptions; + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + const id = this.getIdFromRequest( + request, + idSource, + WorkspaceContextIdFieldName[accessContext], + ); + + if (accessContext === AccessContext.WORKSPACE) { + return this.checkUserWorkspaceRole(user.uuid, id, requiredRole); + } else if (accessContext === AccessContext.TEAM) { + return this.checkUserTeamRole(user.uuid, id, requiredRole); + } + + return false; + } + + private async checkUserWorkspaceRole( + userUuid: string, + workspaceId: string, + role: WorkspaceRole, + ) { + const { workspace, workspaceUser } = + await this.workspaceUseCases.findUserInWorkspace(userUuid, workspaceId); + + if (!workspace) { + throw new NotFoundException('Workspace not found'); + } + + if ( + !workspaceUser || + !(role === WorkspaceRole.OWNER && workspace.isUserOwner(userUuid)) + ) { + Logger.log( + `[WORKSPACES/GUARD]: user has no requiered access to workspace. id: ${workspaceId} userUuid: ${userUuid} `, + ); + throw new ForbiddenException('You have no access to this workspace'); + } + + return !!workspaceUser; + } + + private async checkUserTeamRole( + userUuid: string, + teamId: string, + role: WorkspaceRole, + ) { + const { team, teamUser } = await this.workspaceUseCases.findUserInTeam( + userUuid, + teamId, + ); + + const workspace = await this.workspaceUseCases.findById(team.workspaceId); + if (workspace.isUserOwner(userUuid)) { + return true; + } + + if (teamUser && role === WorkspaceRole.MANAGER) { + return team.isUserManager(userUuid); + } + + if (teamUser && role === WorkspaceRole.MEMBER) { + return true; + } + + throw new ForbiddenException('You have no access to this team'); + } + + private getIdFromRequest( + request, + source: 'params' | 'body' | 'query', + field: string, + ): string | undefined { + return request[source]?.[field]; + } +} diff --git a/src/modules/workspaces/models/workspace-team-users.model.ts b/src/modules/workspaces/models/workspace-team-users.model.ts index 768f462ba..ea3ca5002 100644 --- a/src/modules/workspaces/models/workspace-team-users.model.ts +++ b/src/modules/workspaces/models/workspace-team-users.model.ts @@ -14,7 +14,7 @@ import { WorkspaceTeamUserAttributes } from '../attributes/workspace-team-users. @Table({ underscored: true, timestamps: true, - tableName: 'teams_users', + tableName: 'workspace_teams_users', }) export class WorkspaceTeamUserModel extends Model diff --git a/src/modules/workspaces/models/workspace.model.ts b/src/modules/workspaces/models/workspace.model.ts index fae2e96fc..6bb342ddb 100644 --- a/src/modules/workspaces/models/workspace.model.ts +++ b/src/modules/workspaces/models/workspace.model.ts @@ -6,6 +6,7 @@ import { PrimaryKey, ForeignKey, BelongsTo, + HasMany, } from 'sequelize-typescript'; import { UserModel } from '../../user/user.model'; import { WorkspaceUserModel } from './workspace-users.model'; @@ -60,6 +61,9 @@ export class WorkspaceModel extends Model implements WorkspaceAttributes { @Column(DataType.UUID) workspaceUserId: string; + @HasMany(() => WorkspaceUserModel) + workspaceUsers: WorkspaceUserModel[]; + @Column createdAt: Date; diff --git a/src/modules/workspaces/repositories/team.repository.ts b/src/modules/workspaces/repositories/team.repository.ts index f585067fb..e8615f088 100644 --- a/src/modules/workspaces/repositories/team.repository.ts +++ b/src/modules/workspaces/repositories/team.repository.ts @@ -8,6 +8,8 @@ import { WorkspaceTeamModel } from '../models/workspace-team.model'; import { WorkspaceTeamUserModel } from '../models/workspace-team-users.model'; import { WorkspaceTeam } from '../domains/workspace-team.domain'; import { WorkspaceTeamAttributes } from '../attributes/workspace-team.attributes'; +import { UserAttributes } from '../../user/user.attributes'; +import { WorkspaceTeamUser } from '../domains/workspace-team-user.domain'; @Injectable() export class SequelizeWorkspaceTeamRepository { @@ -44,6 +46,26 @@ export class SequelizeWorkspaceTeamRepository { return result.map((teamUser) => User.build({ ...teamUser.member })); } + async getTeamUserAndTeamByTeamId( + userUuid: UserAttributes['uuid'], + teamId: WorkspaceTeamAttributes['id'], + ) { + const team = await this.teamModel.findOne({ + where: { id: teamId }, + include: { + required: false, + model: WorkspaceTeamUserModel, + where: { memberId: userUuid }, + }, + }); + return { + team: team ? this.toDomain(team) : null, + teamUser: team.teamUsers[0] + ? this.teamUserToDomain(team.teamUsers[0]) + : null, + }; + } + async getTeamById( teamId: WorkspaceTeamAttributes['id'], ): Promise { @@ -85,4 +107,10 @@ export class SequelizeWorkspaceTeamRepository { ...model.toJSON(), }); } + + teamUserToDomain(model: WorkspaceTeamUserModel): WorkspaceTeamUser { + return WorkspaceTeamUser.build({ + ...model.toJSON(), + }); + } } diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index a177438cf..07e5e34a6 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -4,9 +4,11 @@ import { FindOrCreateOptions, Transaction } from 'sequelize/types'; import { WorkspaceAttributes } from '../attributes/workspace.attributes'; import { Workspace } from '../domains/workspaces.domain'; import { WorkspaceModel } from '../models/workspace.model'; +import { WorkspaceUserModel } from '../models/workspace-users.model'; +import { WorkspaceUser } from '../domains/workspace-user.domain'; export interface WorkspaceRepository { - findById(id: number): Promise; + findById(id: WorkspaceAttributes['id']): Promise; findByOwner(ownerId: Workspace['ownerId']): Promise; createTransaction(): Promise; findOrCreate(opts: FindOrCreateOptions): Promise<[Workspace | null, boolean]>; @@ -35,8 +37,10 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository { constructor( @InjectModel(WorkspaceModel) private modelWorkspace: typeof WorkspaceModel, + @InjectModel(WorkspaceUserModel) + private modelWorkspaceUser: typeof WorkspaceUserModel, ) {} - async findById(id: number): Promise { + async findById(id: WorkspaceAttributes['id']): Promise { const workspace = await this.modelWorkspace.findByPk(id); return workspace ? this.toDomain(workspace) : null; } @@ -75,6 +79,29 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository { return workspaces.map((workspace) => this.toDomain(workspace)); } + async findWorkspaceAndUser( + userUuid: string, + workspaceId: string, + ): Promise<{ workspace: Workspace; workspaceUser: WorkspaceUser } | null> { + const workspace = await this.modelWorkspace.findOne({ + where: { id: workspaceId }, + include: { + required: false, + model: WorkspaceUserModel, + where: { + memberId: userUuid, + }, + }, + }); + + return { + workspace: workspace ? this.toDomain(workspace) : null, + workspaceUser: workspace?.workspaceUsers + ? this.workspaceUserToDomain(workspace.workspaceUsers[0]) + : null, + }; + } + async findAllByWithPagination( where: any, limit = 20, @@ -110,8 +137,14 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository { }); } + workspaceUserToDomain(model: WorkspaceUserModel): WorkspaceUser { + return WorkspaceUser.build({ + ...model?.toJSON(), + }); + } + toModel(domain: Workspace): Partial { - return domain.toJSON(); + return domain?.toJSON(); } } export { WorkspaceModel }; diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index f17d94f5d..1a2e3cc4c 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -7,6 +7,7 @@ import { Param, Patch, Post, + UseGuards, } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { WorkspacesUsecases } from './workspaces.usecase'; @@ -15,6 +16,12 @@ import { WorkspaceAttributes } from './attributes/workspace.attributes'; import { User as UserDecorator } from '../auth/decorators/user.decorator'; import { User } from '../user/user.domain'; import { isUUID } from 'class-validator'; +import { WorkspaceGuard } from './guards/workspaces.guard'; +import { + AccessContext, + WorkspaceRequiredAccess, + WorkspaceRole, +} from './guards/workspace-required-access.decorator'; @ApiTags('Workspaces') @Controller('workspaces') @@ -22,6 +29,8 @@ export class WorkspacesController { constructor(private workspaceUseCases: WorkspacesUsecases) {} @Patch('/:workspaceId') + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.OWNER) async setupWorkspace( @Param('workspaceId') workspaceId: WorkspaceAttributes['id'], ) { @@ -35,6 +44,8 @@ export class WorkspacesController { @ApiOkResponse({ description: 'Created team', }) + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.WORKSPACE, WorkspaceRole.OWNER) async createTeam( @Param('workspaceId') workspaceId: WorkspaceAttributes['id'], @Body() createTeamBody: CreateTeamDto, @@ -65,7 +76,9 @@ export class WorkspacesController { return this.workspaceUseCases.getWorkspaceTeams(user, workspaceId); } - @Get('/:workspaceId/teams/:teamId/members') + @Get('/teams/:teamId/members') + @UseGuards(WorkspaceGuard) + @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) async getTeamMembers() { throw new NotImplementedException(); } diff --git a/src/modules/workspaces/workspaces.module.ts b/src/modules/workspaces/workspaces.module.ts index 81840128f..0c4de72ab 100644 --- a/src/modules/workspaces/workspaces.module.ts +++ b/src/modules/workspaces/workspaces.module.ts @@ -11,6 +11,7 @@ import { SequelizeWorkspaceTeamRepository } from './repositories/team.repository import { BridgeModule } from '../../externals/bridge/bridge.module'; import { WorkspaceTeamModel } from './models/workspace-team.model'; import { WorkspaceTeamUserModel } from './models/workspace-team-users.model'; +import { WorkspaceGuard } from './guards/workspaces.guard'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { WorkspaceTeamUserModel } from './models/workspace-team-users.model'; WorkspacesUsecases, SequelizeWorkspaceTeamRepository, SequelizeWorkspaceRepository, + WorkspaceGuard, ], exports: [WorkspacesUsecases, SequelizeModule], }) diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 79f49fe34..897868f76 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -129,4 +129,16 @@ export class WorkspacesUsecases { return teamsWithMemberCount; } + + async findUserInWorkspace(userUuid: string, workspaceId: string) { + return this.workspaceRepository.findWorkspaceAndUser(userUuid, workspaceId); + } + + async findById(workspaceId: string) { + return this.workspaceRepository.findById(workspaceId); + } + + async findUserInTeam(userUuid: string, teamId: string) { + return this.teamRepository.getTeamUserAndTeamByTeamId(userUuid, teamId); + } } From a0feae1cd2a07299aa3f0010e351848fff282d51 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Mon, 18 Mar 2024 21:54:35 -0400 Subject: [PATCH 2/3] feat: add tests for middleware --- .../domains/workspace-team.domain.ts | 4 +- .../workspaces/domains/workspaces.domain.ts | 6 +- .../guards/workspaces.guard.spec.ts | 316 ++++++++++++++++++ .../workspaces/guards/workspaces.guard.ts | 50 ++- .../repositories/team.repository.ts | 8 +- .../repositories/workspaces.repository.ts | 13 +- .../workspaces/workspaces.controller.ts | 2 +- src/modules/workspaces/workspaces.usecase.ts | 20 +- test/fixtures.spec.ts | 79 +++++ test/fixtures.ts | 62 ++++ 10 files changed, 526 insertions(+), 34 deletions(-) create mode 100644 src/modules/workspaces/guards/workspaces.guard.spec.ts diff --git a/src/modules/workspaces/domains/workspace-team.domain.ts b/src/modules/workspaces/domains/workspace-team.domain.ts index 7b53cea62..89f11498c 100644 --- a/src/modules/workspaces/domains/workspace-team.domain.ts +++ b/src/modules/workspaces/domains/workspace-team.domain.ts @@ -25,8 +25,8 @@ export class WorkspaceTeam implements WorkspaceTeamAttributes { this.updatedAt = updatedAt; } - isUserManager(userUuid: User['uuid']) { - return userUuid === this.managerId; + isUserManager(user: User) { + return user.uuid === this.managerId; } static build(teamAttributes: WorkspaceTeamAttributes): WorkspaceTeam { diff --git a/src/modules/workspaces/domains/workspaces.domain.ts b/src/modules/workspaces/domains/workspaces.domain.ts index 82c4d3131..0dae51992 100644 --- a/src/modules/workspaces/domains/workspaces.domain.ts +++ b/src/modules/workspaces/domains/workspaces.domain.ts @@ -1,4 +1,4 @@ -import { UserAttributes } from '../../user/user.attributes'; +import { User } from '../../user/user.domain'; import { WorkspaceAttributes } from '../attributes/workspace.attributes'; export class Workspace implements WorkspaceAttributes { @@ -41,8 +41,8 @@ export class Workspace implements WorkspaceAttributes { return new Workspace(user); } - isUserOwner(userUuid: UserAttributes['uuid']) { - return userUuid === this.ownerId; + isUserOwner(user: User) { + return user.uuid === this.ownerId; } toJSON() { diff --git a/src/modules/workspaces/guards/workspaces.guard.spec.ts b/src/modules/workspaces/guards/workspaces.guard.spec.ts new file mode 100644 index 000000000..c4cbf04c7 --- /dev/null +++ b/src/modules/workspaces/guards/workspaces.guard.spec.ts @@ -0,0 +1,316 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { + ExecutionContext, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { WorkspaceGuard } from './workspaces.guard'; +import { WorkspacesUsecases } from '../workspaces.usecase'; +import { + AccessContext, + WorkspaceRole, +} from './workspace-required-access.decorator'; +import { + newUser, + newWorkspace, + newWorkspaceTeam, +} from '../../../../test/fixtures'; +import { WorkspaceUser } from '../domains/workspace-user.domain'; +import { WorkspaceTeamUser } from '../domains/workspace-team-user.domain'; + +describe('WorkspaceGuard', () => { + let guard: WorkspaceGuard; + let reflector: DeepMocked; + let workspaceUseCases: DeepMocked; + + beforeEach(async () => { + reflector = createMock(); + workspaceUseCases = createMock(); + guard = new WorkspaceGuard(reflector, workspaceUseCases); + }); + + it('Guard should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('When there is no metadata set, then bypass guard', async () => { + jest.spyOn(reflector, 'get').mockReturnValue(undefined); + + const context = createMockExecutionContext(null, null); + + const canUserAccess = await guard.canActivate(context); + + expect(canUserAccess).toBeTruthy(); + }); + + it('When there is no user set, then block access', async () => { + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.OWNER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + const context = createMockExecutionContext(null, null); + + const canUserAccess = await guard.canActivate(context); + + expect(canUserAccess).toBeFalsy(); + }); + + describe('Workspace Permissions', () => { + it('When workspace is missing, then throw ', async () => { + const user = newUser(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + + workspaceUseCases.findUserInWorkspace.mockResolvedValue({ + workspace: null, + workspaceUser: null, + }); + + const context = createMockExecutionContext(user, { + params: { workspaceId: 'nonexistent-workspace-id' }, + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + NotFoundException, + ); + }); + + it('When user is owner of the workspace and required role is owner, then grant access', async () => { + const workspaceOwner = newUser(); + const workspace = newWorkspace({ owner: workspaceOwner }); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.OWNER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + + workspaceUseCases.findUserInWorkspace.mockResolvedValue({ + workspace, + workspaceUser: {} as WorkspaceUser, + }); + + const context = createMockExecutionContext(workspaceOwner, { + params: { workspaceId: workspace.id }, + }); + + await expect(guard.canActivate(context)).resolves.toBeTruthy(); + }); + + it('When user is not owner and required role is owner, then deny access', async () => { + const nonOwnerUser = newUser(); + const workspace = newWorkspace(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.OWNER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + workspaceUseCases.findUserInWorkspace.mockResolvedValue({ + workspace, + workspaceUser: {} as WorkspaceUser, + }); + const context = createMockExecutionContext(nonOwnerUser, { + params: { workspaceId: workspace.id }, + }); + await expect(guard.canActivate(context)).rejects.toThrow( + ForbiddenException, + ); + }); + + it('When user is member and required role is member, then grant access', async () => { + const workspaceMember = newUser(); + const workspace = newWorkspace(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + workspaceUseCases.findUserInWorkspace.mockResolvedValue({ + workspace, + workspaceUser: {} as WorkspaceUser, + }); + const context = createMockExecutionContext(workspaceMember, { + params: { workspaceId: workspace.id }, + }); + const grantAccess = await guard.canActivate(context); + + expect(grantAccess).toBeTruthy(); + }); + + it('When user is not member and required role is member, then deny access', async () => { + const notMember = newUser(); + const workspace = newWorkspace(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.WORKSPACE, + idSource: 'params', + }); + workspaceUseCases.findUserInWorkspace.mockResolvedValue({ + workspace, + workspaceUser: null, + }); + const context = createMockExecutionContext(notMember, { + params: { workspaceId: workspace.id }, + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + ForbiddenException, + ); + }); + }); + + describe('Team Permissions', () => { + it('When Team is not valid, then throw', async () => { + const user = newUser(); + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MANAGER, + accessContext: AccessContext.TEAM, + idSource: 'params', + }); + + workspaceUseCases.findUserInTeam.mockResolvedValue({ + team: null, + teamUser: null, + }); + + const context = createMockExecutionContext(user, { + params: { teamId: 'anyid' }, + query: {}, + body: {}, + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + NotFoundException, + ); + }); + + it('Access Granted for Team Member', async () => { + const user = newUser(); + const workspace = newWorkspace({ owner: user }); + const team = newWorkspaceTeam({ + workspaceId: workspace.id, + manager: user, + }); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.TEAM, + idSource: 'params', + }); + + workspaceUseCases.findUserInTeam.mockResolvedValue({ + team, + teamUser: {} as WorkspaceTeamUser, + }); + + const context = createMockExecutionContext(user, { + params: { teamId: team.id }, + }); + + await expect(guard.canActivate(context)).resolves.toBeTruthy(); + }); + + it('When user is not manager and required role is manager, then deny access', async () => { + const manager = newUser(); + const member = newUser(); + const team = newWorkspaceTeam({ manager }); + const workspace = newWorkspace(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MANAGER, + accessContext: AccessContext.TEAM, + idSource: 'params', + }); + + workspaceUseCases.findUserInTeam.mockResolvedValue({ + team, + teamUser: {} as WorkspaceTeamUser, + }); + + workspaceUseCases.findById.mockResolvedValue(workspace); + + const context = createMockExecutionContext(member, { + params: { teamId: team.id }, + }); + const grantAccess = await guard.canActivate(context); + + expect(grantAccess).toBeFalsy(); + }); + + it('When user is not part of team, then deny access', async () => { + const nonMemberUser = newUser(); + const team = newWorkspaceTeam(); + const workspace = newWorkspace(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.TEAM, + idSource: 'params', + }); + + workspaceUseCases.findById.mockResolvedValue(workspace); + workspaceUseCases.findUserInTeam.mockResolvedValue({ + team, + teamUser: null, + }); + + const context = createMockExecutionContext(nonMemberUser, { + params: { teamId: team.id }, + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + ForbiddenException, + ); + }); + + it('When team workspace does not exist, then throw', async () => { + const nonMemberUser = newUser(); + const team = newWorkspaceTeam(); + + jest.spyOn(reflector, 'get').mockReturnValue({ + requiredRole: WorkspaceRole.MEMBER, + accessContext: AccessContext.TEAM, + idSource: 'params', + }); + + workspaceUseCases.findById.mockResolvedValue(null); + workspaceUseCases.findUserInTeam.mockResolvedValue({ + team, + teamUser: null, + }); + + const context = createMockExecutionContext(nonMemberUser, { + params: { teamId: team.id }, + }); + + await expect(guard.canActivate(context)).rejects.toThrow( + NotFoundException, + ); + }); + }); +}); + +const createMockExecutionContext = ( + user: any, + requestPayload: any, +): ExecutionContext => + ({ + getHandler: () => ({ + name: 'endPointHandler', + }), + switchToHttp: () => ({ + getRequest: () => ({ + user: user, + ...requestPayload, + }), + }), + }) as unknown as ExecutionContext; diff --git a/src/modules/workspaces/guards/workspaces.guard.ts b/src/modules/workspaces/guards/workspaces.guard.ts index 16a88d2e9..f08bcda22 100644 --- a/src/modules/workspaces/guards/workspaces.guard.ts +++ b/src/modules/workspaces/guards/workspaces.guard.ts @@ -15,6 +15,7 @@ import { WorkspaceContextIdFieldName, WorkspaceRole, } from './workspace-required-access.decorator'; +import { User } from '../../user/user.domain'; @Injectable() export class WorkspaceGuard implements CanActivate { @@ -36,7 +37,12 @@ export class WorkspaceGuard implements CanActivate { const { requiredRole, accessContext, idSource } = accessOptions; const request = context.switchToHttp().getRequest(); - const user = request.user; + + if (!request.user) { + return false; + } + + const user = User.build({ ...request.user }); const id = this.getIdFromRequest( request, @@ -45,56 +51,66 @@ export class WorkspaceGuard implements CanActivate { ); if (accessContext === AccessContext.WORKSPACE) { - return this.checkUserWorkspaceRole(user.uuid, id, requiredRole); + return this.verifyWorkspaceAccessByRole(user, id, requiredRole); } else if (accessContext === AccessContext.TEAM) { - return this.checkUserTeamRole(user.uuid, id, requiredRole); + return this.verifyTeamAccessByRole(user, id, requiredRole); } return false; } - private async checkUserWorkspaceRole( - userUuid: string, + private async verifyWorkspaceAccessByRole( + user: User, workspaceId: string, role: WorkspaceRole, ) { const { workspace, workspaceUser } = - await this.workspaceUseCases.findUserInWorkspace(userUuid, workspaceId); + await this.workspaceUseCases.findUserInWorkspace(user.uuid, workspaceId); if (!workspace) { throw new NotFoundException('Workspace not found'); } - if ( - !workspaceUser || - !(role === WorkspaceRole.OWNER && workspace.isUserOwner(userUuid)) - ) { + const isUserNotInWorkspace = !workspaceUser; + const isRequiredOwnerRoleAndUserIsNotOwner = + role === WorkspaceRole.OWNER && !workspace.isUserOwner(user); + + if (isUserNotInWorkspace || isRequiredOwnerRoleAndUserIsNotOwner) { Logger.log( - `[WORKSPACES/GUARD]: user has no requiered access to workspace. id: ${workspaceId} userUuid: ${userUuid} `, + `[WORKSPACES/GUARD]: user has no requiered access to workspace. id: ${workspaceId} userUuid: ${user.uuid} `, ); throw new ForbiddenException('You have no access to this workspace'); } - return !!workspaceUser; + return true; } - private async checkUserTeamRole( - userUuid: string, + private async verifyTeamAccessByRole( + user: User, teamId: string, role: WorkspaceRole, ) { const { team, teamUser } = await this.workspaceUseCases.findUserInTeam( - userUuid, + user.uuid, teamId, ); + if (!team) { + throw new NotFoundException('Team not found'); + } + const workspace = await this.workspaceUseCases.findById(team.workspaceId); - if (workspace.isUserOwner(userUuid)) { + + if (!workspace) { + throw new NotFoundException('Workspace not found'); + } + + if (workspace.isUserOwner(user)) { return true; } if (teamUser && role === WorkspaceRole.MANAGER) { - return team.isUserManager(userUuid); + return team.isUserManager(user); } if (teamUser && role === WorkspaceRole.MEMBER) { diff --git a/src/modules/workspaces/repositories/team.repository.ts b/src/modules/workspaces/repositories/team.repository.ts index e8615f088..747fd77ad 100644 --- a/src/modules/workspaces/repositories/team.repository.ts +++ b/src/modules/workspaces/repositories/team.repository.ts @@ -49,7 +49,10 @@ export class SequelizeWorkspaceTeamRepository { async getTeamUserAndTeamByTeamId( userUuid: UserAttributes['uuid'], teamId: WorkspaceTeamAttributes['id'], - ) { + ): Promise<{ + team: WorkspaceTeam | null; + teamUser: WorkspaceTeamUser | null; + }> { const team = await this.teamModel.findOne({ where: { id: teamId }, include: { @@ -58,9 +61,10 @@ export class SequelizeWorkspaceTeamRepository { where: { memberId: userUuid }, }, }); + return { team: team ? this.toDomain(team) : null, - teamUser: team.teamUsers[0] + teamUser: team?.teamUsers?.[0] ? this.teamUserToDomain(team.teamUsers[0]) : null, }; diff --git a/src/modules/workspaces/repositories/workspaces.repository.ts b/src/modules/workspaces/repositories/workspaces.repository.ts index 07e5e34a6..5c4def2e5 100644 --- a/src/modules/workspaces/repositories/workspaces.repository.ts +++ b/src/modules/workspaces/repositories/workspaces.repository.ts @@ -82,21 +82,22 @@ export class SequelizeWorkspaceRepository implements WorkspaceRepository { async findWorkspaceAndUser( userUuid: string, workspaceId: string, - ): Promise<{ workspace: Workspace; workspaceUser: WorkspaceUser } | null> { + ): Promise<{ + workspace: Workspace | null; + workspaceUser: WorkspaceUser | null; + }> { const workspace = await this.modelWorkspace.findOne({ where: { id: workspaceId }, include: { - required: false, model: WorkspaceUserModel, - where: { - memberId: userUuid, - }, + where: { memberId: userUuid }, + required: false, }, }); return { workspace: workspace ? this.toDomain(workspace) : null, - workspaceUser: workspace?.workspaceUsers + workspaceUser: workspace?.workspaceUsers?.[0] ? this.workspaceUserToDomain(workspace.workspaceUsers[0]) : null, }; diff --git a/src/modules/workspaces/workspaces.controller.ts b/src/modules/workspaces/workspaces.controller.ts index 1a2e3cc4c..1bec64a29 100644 --- a/src/modules/workspaces/workspaces.controller.ts +++ b/src/modules/workspaces/workspaces.controller.ts @@ -76,7 +76,7 @@ export class WorkspacesController { return this.workspaceUseCases.getWorkspaceTeams(user, workspaceId); } - @Get('/teams/:teamId/members') + @Get('/:workspaceId/teams/:teamId/members') @UseGuards(WorkspaceGuard) @WorkspaceRequiredAccess(AccessContext.TEAM, WorkspaceRole.MEMBER) async getTeamMembers() { diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 897868f76..5c6571abc 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -11,6 +11,8 @@ import { BridgeService } from '../../externals/bridge/bridge.service'; import { SequelizeUserRepository } from '../user/user.repository'; import { UserAttributes } from '../user/user.attributes'; import { WorkspaceTeam } from './domains/workspace-team.domain'; +import { WorkspaceUser } from './domains/workspace-user.domain'; +import { WorkspaceTeamUser } from './domains/workspace-team-user.domain'; @Injectable() export class WorkspacesUsecases { @@ -130,15 +132,27 @@ export class WorkspacesUsecases { return teamsWithMemberCount; } - async findUserInWorkspace(userUuid: string, workspaceId: string) { + findUserInWorkspace( + userUuid: string, + workspaceId: string, + ): Promise<{ + workspace: Workspace | null; + workspaceUser: WorkspaceUser | null; + }> { return this.workspaceRepository.findWorkspaceAndUser(userUuid, workspaceId); } - async findById(workspaceId: string) { + findById(workspaceId: string): Promise { return this.workspaceRepository.findById(workspaceId); } - async findUserInTeam(userUuid: string, teamId: string) { + findUserInTeam( + userUuid: string, + teamId: string, + ): Promise<{ + team: WorkspaceTeam | null; + teamUser: WorkspaceTeamUser | null; + }> { return this.teamRepository.getTeamUserAndTeamByTeamId(userUuid, teamId); } } diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 7468946ba..06620663a 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -267,4 +267,83 @@ describe('Testing fixtures tests', () => { expect(limit.value).toEqual('0'); }); }); + + describe("Workspace's fixture", () => { + it('When it generates a workspace, then the identifier should be random', () => { + const workspace = fixtures.newWorkspace(); + const otherWorkspace = fixtures.newWorkspace(); + + expect(workspace.id).toBeTruthy(); + expect(workspace.id).not.toBe(otherWorkspace.id); + }); + + it('When it generates a workspace, then the ownerId should be random', () => { + const workspace = fixtures.newWorkspace(); + const otherWorkspace = fixtures.newWorkspace(); + + expect(workspace.ownerId).toBeTruthy(); + expect(workspace.ownerId).not.toBe(otherWorkspace.ownerId); + }); + + it('When it generates a workspace with an owner, then the ownerId should match the owner', () => { + const owner = fixtures.newUser(); + const workspace = fixtures.newWorkspace({ owner }); + + expect(workspace.ownerId).toBe(owner.uuid); + }); + + it('When it generates a workspace, then the createdAt should be equal or less than updatedAt', () => { + const workspace = fixtures.newWorkspace(); + + expect(workspace.createdAt.getTime()).toBeLessThanOrEqual( + workspace.updatedAt.getTime(), + ); + }); + + it('When it generates a workspace, then the setupCompleted should be a boolean value', () => { + const workspace = fixtures.newWorkspace(); + + expect(typeof workspace.setupCompleted).toBe('boolean'); + }); + }); + + describe("WorkspaceTeam's fixture", () => { + it('When it generates a workspace team, then the identifier should be random', () => { + const team = fixtures.newWorkspaceTeam(); + const otherTeam = fixtures.newWorkspaceTeam(); + + expect(team.id).toBeTruthy(); + expect(team.id).not.toBe(otherTeam.id); + }); + + it('When it generates a workspace team, then the workspaceId should be random', () => { + const team = fixtures.newWorkspaceTeam(); + const otherTeam = fixtures.newWorkspaceTeam(); + + expect(team.workspaceId).toBeTruthy(); + expect(team.workspaceId).not.toBe(otherTeam.workspaceId); + }); + + it('When it generates a workspace team with a manager, then the managerId should match the manager', () => { + const manager = fixtures.newUser(); + const team = fixtures.newWorkspaceTeam({ manager }); + + expect(team.managerId).toBe(manager.uuid); + }); + + it('When it generates a workspace team, then the createdAt should be equal or less than updatedAt', () => { + const team = fixtures.newWorkspaceTeam(); + + expect(team.createdAt.getTime()).toBeLessThanOrEqual( + team.updatedAt.getTime(), + ); + }); + + it('When it generates a workspace team, then the name should be populated', () => { + const team = fixtures.newWorkspaceTeam(); + + expect(team.name).toBeTruthy(); + expect(typeof team.name).toBe('string'); + }); + }); }); diff --git a/test/fixtures.ts b/test/fixtures.ts index 3046e7db7..c677856f8 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -16,6 +16,8 @@ import { LimitTypes, } from '../src/modules/feature-limit/limits.enum'; import { Limit } from '../src/modules/feature-limit/limit.domain'; +import { Workspace } from '../src/modules/workspaces/domains/workspaces.domain'; +import { WorkspaceTeam } from '../src/modules/workspaces/domains/workspace-team.domain'; export const constants = { BUCKET_ID_LENGTH: 24, @@ -245,3 +247,63 @@ export const newFeatureLimit = (bindTo?: { label: bindTo?.label ?? ('' as LimitLabels), }); }; + +export const newWorkspace = (params?: { + attributes?: Partial; + owner?: User; +}): Workspace => { + const randomCreatedAt = randomDataGenerator.date(); + + const workspace = Workspace.build({ + id: v4(), + ownerId: params?.owner?.uuid || v4(), + address: randomDataGenerator.address(), + name: randomDataGenerator.company(), + description: randomDataGenerator.sentence(), + defaultTeamId: v4(), + workspaceUserId: v4(), + setupCompleted: randomDataGenerator.bool(), + createdAt: randomCreatedAt, + updatedAt: new Date( + randomDataGenerator.date({ + min: randomCreatedAt, + }), + ), + }); + + params?.attributes && + Object.keys(params.attributes).forEach((key) => { + workspace[key] = params.attributes[key]; + }); + + return workspace; +}; + +export const newWorkspaceTeam = (params?: { + attributes?: Partial; + workspaceId?: string; + manager?: User; +}): WorkspaceTeam => { + const randomCreatedAt = randomDataGenerator.date(); + const manager = params?.manager || newUser(); + + const team = WorkspaceTeam.build({ + id: v4(), + workspaceId: params?.workspaceId || v4(), + managerId: manager.uuid, + name: randomDataGenerator.word(), + createdAt: randomCreatedAt, + updatedAt: new Date( + randomDataGenerator.date({ + min: randomCreatedAt, + }), + ), + }); + + params?.attributes && + Object.keys(params.attributes).forEach((key) => { + team[key] = params.attributes[key]; + }); + + return team; +}; From 770e5544caa11b48ab72cda839ef8ad336832cbe Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 19 Mar 2024 17:49:45 -0400 Subject: [PATCH 3/3] chore: added fixture for workspaceUser --- .../guards/workspaces.guard.spec.ts | 7 +-- .../workspaces/guards/workspaces.guard.ts | 1 - test/fixtures.spec.ts | 43 +++++++++++++++++++ test/fixtures.ts | 34 +++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/modules/workspaces/guards/workspaces.guard.spec.ts b/src/modules/workspaces/guards/workspaces.guard.spec.ts index c4cbf04c7..bd7cb007e 100644 --- a/src/modules/workspaces/guards/workspaces.guard.spec.ts +++ b/src/modules/workspaces/guards/workspaces.guard.spec.ts @@ -15,6 +15,7 @@ import { newUser, newWorkspace, newWorkspaceTeam, + newWorkspaceUser, } from '../../../../test/fixtures'; import { WorkspaceUser } from '../domains/workspace-user.domain'; import { WorkspaceTeamUser } from '../domains/workspace-team-user.domain'; @@ -125,7 +126,7 @@ describe('WorkspaceGuard', () => { }); it('When user is member and required role is member, then grant access', async () => { - const workspaceMember = newUser(); + const workspaceMember = newWorkspaceUser(); const workspace = newWorkspace(); jest.spyOn(reflector, 'get').mockReturnValue({ @@ -135,7 +136,7 @@ describe('WorkspaceGuard', () => { }); workspaceUseCases.findUserInWorkspace.mockResolvedValue({ workspace, - workspaceUser: {} as WorkspaceUser, + workspaceUser: workspaceMember, }); const context = createMockExecutionContext(workspaceMember, { params: { workspaceId: workspace.id }, @@ -193,7 +194,7 @@ describe('WorkspaceGuard', () => { ); }); - it('Access Granted for Team Member', async () => { + it('When user is team member, then grant access', async () => { const user = newUser(); const workspace = newWorkspace({ owner: user }); const team = newWorkspaceTeam({ diff --git a/src/modules/workspaces/guards/workspaces.guard.ts b/src/modules/workspaces/guards/workspaces.guard.ts index f08bcda22..3ae922139 100644 --- a/src/modules/workspaces/guards/workspaces.guard.ts +++ b/src/modules/workspaces/guards/workspaces.guard.ts @@ -1,4 +1,3 @@ -// flexible-access.guard.ts import { CanActivate, ExecutionContext, diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 06620663a..49a203ea7 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -346,4 +346,47 @@ describe('Testing fixtures tests', () => { expect(typeof team.name).toBe('string'); }); }); + + describe("WorkspaceUser's fixture", () => { + it('When it generates a workspace user, then the identifier should be random', () => { + const user = fixtures.newWorkspaceUser(); + const otherUser = fixtures.newWorkspaceUser(); + expect(user.id).toBeTruthy(); + expect(user.id).not.toBe(otherUser.id); + }); + + it('When it generates a workspace user, then the workspaceId should be random', () => { + const user = fixtures.newWorkspaceUser(); + const otherUser = fixtures.newWorkspaceUser(); + expect(user.workspaceId).toBeTruthy(); + expect(user.workspaceId).not.toBe(otherUser.workspaceId); + }); + + it('When it generates a workspace user with a specified memberId, then the memberId should match', () => { + const memberId = 'anyId'; + const user = fixtures.newWorkspaceUser({ memberId }); + expect(user.memberId).toBe(memberId); + }); + + it('When it generates a workspace user, then driveUsage and backupsUsage should not exceed spaceLimit', () => { + const user = fixtures.newWorkspaceUser(); + expect(Number(user.driveUsage)).toBeLessThanOrEqual( + BigInt(user.spaceLimit), + ); + expect(BigInt(user.backupsUsage)).toBeLessThanOrEqual( + BigInt(user.spaceLimit), + ); + }); + + it('When it generates a workspace user with custom attributes, then those attributes are set correctly', () => { + const customAttributes = { + deactivated: true, + spaceLimit: BigInt(500), + }; + const user = fixtures.newWorkspaceUser({ attributes: customAttributes }); + + expect(user.deactivated).toBe(customAttributes.deactivated); + expect(user.spaceLimit).toBe(customAttributes.spaceLimit); + }); + }); }); diff --git a/test/fixtures.ts b/test/fixtures.ts index c677856f8..af33d106c 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -18,6 +18,7 @@ import { import { Limit } from '../src/modules/feature-limit/limit.domain'; import { Workspace } from '../src/modules/workspaces/domains/workspaces.domain'; import { WorkspaceTeam } from '../src/modules/workspaces/domains/workspace-team.domain'; +import { WorkspaceUser } from '../src/modules/workspaces/domains/workspace-user.domain'; export const constants = { BUCKET_ID_LENGTH: 24, @@ -307,3 +308,36 @@ export const newWorkspaceTeam = (params?: { return team; }; + +export const newWorkspaceUser = (params?: { + workspaceId?: string; + memberId?: string; + attributes?: Partial; +}): WorkspaceUser => { + const randomCreatedAt = randomDataGenerator.date(); + const spaceLimit = randomDataGenerator.natural({ min: 1, max: 1073741824 }); + + const workspaceUser = WorkspaceUser.build({ + id: v4(), + memberId: params?.memberId || v4(), + key: randomDataGenerator.string({ length: 32 }), + workspaceId: params?.workspaceId || v4(), + spaceLimit: BigInt(spaceLimit), + driveUsage: BigInt( + randomDataGenerator.natural({ min: 1, max: spaceLimit }), + ), + backupsUsage: BigInt( + randomDataGenerator.natural({ min: 1, max: spaceLimit }), + ), + deactivated: randomDataGenerator.bool(), + createdAt: randomCreatedAt, + updatedAt: new Date(randomDataGenerator.date({ min: randomCreatedAt })), + }); + + params?.attributes && + Object.keys(params.attributes).forEach((key) => { + workspaceUser[key] = params.attributes[key]; + }); + + return workspaceUser; +};